Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pie title #2987

Merged
merged 6 commits into from
Sep 28, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/traces/pie/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,34 @@ module.exports = {
description: 'Sets the font used for `textinfo` lying outside the pie.'
}),

title: {
valType: 'string',
dflt: '',
role: 'info',
editType: 'calc',
description: [
'Sets the title of the pie chart.',
'If it is empty, no title is displayed.'
].join(' ')
},
titleposition: {
valType: 'enumerated',
values: [
'top left', 'top center', 'top right',
'middle center',
'bottom left', 'bottom center', 'bottom right'
],
dflt: 'top center',
role: 'info',
editType: 'calc',
description: [
'Specifies the location of the `title`.',
].join(' ')
},
titlefont: extendFlat({}, textFontAttrs, {
description: 'Sets the font used for `title`.'
}),

// position and shape
domain: domainAttrs({name: 'pie', trace: true, editType: 'calc'}),

Expand Down
9 changes: 8 additions & 1 deletion src/traces/pie/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout

handleDomainDefaults(traceOut, layout, coerce);

coerce('hole');
var hole = coerce('hole');
var title = coerce('title');
if(title) {
var titlePosition = coerce('titleposition');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we should make the default be 'middle center' when there's a hole (ie var titlePosition = coerce('titleposition', hole ? 'middle center' : 'top center'); and remove attributes.titleposition.dflt) - or maybe when hole > 0.5 or something... would be strange to put the title in the center when the hole is very small!

But I'm not wedded to that idea; if you've considered it and still think 'top center' should be the default in all cases we can keep it that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
coerceFont(coerce, 'titlefont', layout.font);
}

coerce('sort');
coerce('direction');
Expand Down
161 changes: 153 additions & 8 deletions src/traces/pie/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var eventData = require('./event_data');
module.exports = function plot(gd, cdpie) {
var fullLayout = gd._fullLayout;

prerenderTitles(cdpie, gd);
scalePies(cdpie, fullLayout._size);

var pieGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdpie, 'trace').each(function(cd) {
Expand Down Expand Up @@ -308,6 +309,43 @@ module.exports = function plot(gd, cdpie) {
});
});

// add the title
var titleTextGroup = d3.select(this).selectAll('g.titletext')
.data(trace.title ? [0] : []);

titleTextGroup.enter().append('g')
.classed('titletext', true);
titleTextGroup.exit().remove();

titleTextGroup.each(function() {
var titleText = Lib.ensureSingle(d3.select(this), 'text', '', function(s) {
// prohibit tex interpretation as above
s.attr('data-notex', 1);
});

titleText.text(trace.title)
.attr({
'class': 'titletext',
transform: '',
'text-anchor': 'middle',
})
.call(Drawing.font, trace.titlefont)
.call(svgTextUtils.convertToTspans, gd);

var transform;

if(trace.titleposition === 'middle center') {
transform = positionTitleInside(cd0);
} else {
transform = positionTitleOutside(cd0, fullLayout._size);
}

titleText.attr('transform',
'translate(' + transform.x + ',' + transform.y + ')' +
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
'translate(' + transform.tx + ',' + transform.ty + ')');
});

// now make sure no labels overlap (at least within one pie)
if(hasOutsideText) scootLabels(quadrants, trace);
slices.each(function(pt) {
Expand Down Expand Up @@ -371,6 +409,28 @@ module.exports = function plot(gd, cdpie) {
}, 0);
};

function prerenderTitles(cdpie, gd) {
var cd0, trace;
// Determine the width and height of the title for each pie.
for(var i = 0; i < cdpie.length; i++) {
cd0 = cdpie[i][0];
trace = cd0.trace;

if(trace.title) {
var dummyTitle = Drawing.tester.append('text')
.attr('data-notex', 1)
.text(trace.title)
.call(Drawing.font, trace.titlefont)
.call(svgTextUtils.convertToTspans, gd);
var bBox = Drawing.bBox(dummyTitle.node(), true);
cd0.titleBox = {
width: bBox.width,
height: bBox.height,
};
dummyTitle.remove();
}
}
}

function transformInsideText(textBB, pt, cd0) {
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
Expand Down Expand Up @@ -454,6 +514,89 @@ function transformOutsideText(textBB, pt) {
};
}

function positionTitleInside(cd0) {
var textDiameter =
Math.sqrt(cd0.titleBox.width * cd0.titleBox.width + cd0.titleBox.height * cd0.titleBox.height);
return {
x: cd0.cx,
y: cd0.cy,
scale: cd0.trace.hole * cd0.r * 2 / textDiameter,
tx: 0,
ty: - cd0.titleBox.height / 2 + cd0.trace.titlefont.size
};
}

function positionTitleOutside(cd0, plotSize) {
var scaleX = 1, scaleY = 1, maxWidth, maxPull;
var trace = cd0.trace;
// position of the baseline point of the text box in the plot, before scaling.
// we anchored the text in the middle, so the baseline is on the bottom middle
// of the first line of text.
var topMiddle = {
x: cd0.cx,
y: cd0.cy
};
// relative translation of the text box after scaling
var translate = {
tx: 0,
ty: 0
};

// we reason below as if the baseline is the top middle point of the text box.
// so we must add the font size to approximate the y-coord. of the top.
// note that this correction must happen after scaling.
translate.ty += trace.titlefont.size;
maxPull = getMaxPull(trace);

if(trace.titleposition.indexOf('top') !== -1) {
topMiddle.y -= (1 + maxPull) * cd0.r;
translate.ty -= cd0.titleBox.height;
}
else if(trace.titleposition.indexOf('bottom') !== -1) {
topMiddle.y += (1 + maxPull) * cd0.r;
}

if(trace.titleposition.indexOf('left') !== -1) {
// we start the text at the left edge of the pie
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
topMiddle.x -= (1 + maxPull) * cd0.r;
translate.tx += cd0.titleBox.width / 2;
} else if(trace.titleposition.indexOf('center') !== -1) {
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
} else if(trace.titleposition.indexOf('right') !== -1) {
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
topMiddle.x += (1 + maxPull) * cd0.r;
translate.tx -= cd0.titleBox.width / 2;
}
scaleX = maxWidth / cd0.titleBox.width;
scaleY = getTitleSpace(cd0, plotSize) / cd0.titleBox.height;
return {
x: topMiddle.x,
y: topMiddle.y,
scale: Math.min(scaleX, scaleY),
tx: translate.tx,
ty: translate.ty
};
}

function getTitleSpace(cd0, plotSize) {
var trace = cd0.trace;
var pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
// use at most half of the plot for the pie
return Math.min(cd0.titleBox.height, pieBoxHeight / 2);
}

function getMaxPull(trace) {
var maxPull = trace.pull, j;
if(Array.isArray(maxPull)) {
maxPull = 0;
for(j = 0; j < trace.pull.length; j++) {
if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
}
}
return maxPull;
}

function scootLabels(quadrants, trace) {
var xHalf, yHalf, equatorFirst, farthestX, farthestY,
xDiffSign, yDiffSign, thisQuad, oppositeQuad,
Expand Down Expand Up @@ -570,21 +713,23 @@ function scalePies(cdpie, plotSize) {
for(i = 0; i < cdpie.length; i++) {
cd0 = cdpie[i][0];
trace = cd0.trace;

pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);

maxPull = trace.pull;
if(Array.isArray(maxPull)) {
maxPull = 0;
for(j = 0; j < trace.pull.length; j++) {
if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
}
// leave some space for the title, if it will be displayed outside
if(trace.title && trace.titleposition !== 'middle center') {
pieBoxHeight -= getTitleSpace(cd0, plotSize);
}

maxPull = getMaxPull(trace);

cd0.r = Math.min(pieBoxWidth, pieBoxHeight) / (2 + 2 * maxPull);

cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2;
cd0.cy = plotSize.t + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0]) / 2;
cd0.cy = plotSize.t + plotSize.h * (1 - trace.domain.y[0]) - pieBoxHeight / 2;
if(trace.title && trace.titleposition.indexOf('bottom') !== -1) {
cd0.cy -= getTitleSpace(cd0, plotSize);
}

if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) {
scaleGroups.push(trace.scalegroup);
Expand Down
Binary file added test/image/baselines/pie_title_groupscale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/pie_title_middle_center.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/pie_title_multiple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/pie_title_pull.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/pie_title_subscript.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/pie_title_variations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions test/image/mocks/pie_title_groupscale.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"data": [
{
"values": [118, 107, 98, 90, 87],
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
"type": "pie",
"name": "Starry Night",
"marker": {
"colors": ["rgb(56, 75, 126)", "rgb(18, 36, 37)", "rgb(34, 53, 101)", "rgb(36, 55, 57)", "rgb(6, 4, 4)"]
},
"domain": {
"x": [0, 0.48],
"y": [0, 0.49]
},
"textinfo": "none",
"title": "Starry Night",
"titleposition": "bottom center",
"scalegroup": "1"
},
{
"values": [28, 26, 21, 15, 10],
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
"type": "pie",
"name": "Sunflowers",
"marker": {
"colors": ["rgb(177, 127, 38)", "rgb(205, 152, 36)", "rgb(99, 79, 37)", "rgb(129, 180, 179)", "rgb(124, 103, 37)"]
},
"domain": {
"x": [0.52, 1],
"y": [0, 0.49]
},
"textinfo": "none",
"title": "Sunflowers",
"titleposition": "top center",
"titlefont": {
"size": 12
},
"scalegroup": "2"
},
{
"values": [108, 109, 96, 84, 73],
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
"type": "pie",
"name": "Irises",
"marker": {
"colors": ["rgb(33, 75, 99)", "rgb(79, 129, 102)", "rgb(151, 179, 100)", "rgb(175, 49, 35)", "rgb(36, 73, 147)"]
},
"domain": {
"x": [0, 0.48],
"y": [0.51, 1]
},
"textinfo": "none",
"title": "Irises",
"titlefont": {
"size": 12
},
"scalegroup": "2"
},
{
"values": [31, 24, 19, 18, 8],
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
"type": "pie",
"name": "The Night Cafe",
"titlefont": {
"size": 50
},
"marker": {
"colors": ["rgb(146, 123, 21)", "rgb(177, 180, 34)", "rgb(206, 206, 40)", "rgb(175, 51, 21)", "rgb(35, 36, 21)"]
},
"domain": {
"x": [0.52, 1],
"y": [0.52, 1]
},
"textinfo": "none",
"title": "The<br>Night<br>Cafe",
"scalegroup": "1"
}
],
"layout": {
"height": 400,
"width": 500
}
}
18 changes: 18 additions & 0 deletions test/image/mocks/pie_title_middle_center.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"data": [
{
"values": [955, 405, 360, 310, 295],
"labels": ["Mandarin", "Spanish", "English", "Hindi", "Arabic"],
"textinfo": "label+percent",
"hole": 0.1,
"title": "Num. speakers",
"titleposition": "middle center",
"type": "pie"
}
],
"layout": {
"title": "Top 5 languages by number of native speakers (2010, est.)",
"height": 600,
"width": 600
}
}
18 changes: 18 additions & 0 deletions test/image/mocks/pie_title_middle_center_multiline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"data": [
{
"values": [955, 405, 360, 310, 295],
"labels": ["Mandarin", "Spanish", "English", "Hindi", "Arabic"],
"textinfo": "label+percent",
"hole": 0.4,
"title": "Number<br>of<br>speakers",
"titleposition": "middle center",
"type": "pie"
}
],
"layout": {
"title": "Top 5 languages by number of native speakers (2010, est.)",
"height": 600,
"width": 600
}
}
Loading