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 1 commit
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
11 changes: 6 additions & 5 deletions src/traces/pie/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,16 @@ module.exports = {
},
titleposition: {
valType: 'enumerated',
values: ['inhole', 'outside'],
dflt: 'outside',
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`.',
'If `inhole` and the chart is a donut, the text is scaled',
'and displayed inside the hole.',
'If `outside`, the text is shown above the pie chart.'
].join(' ')
},
titlefont: extendFlat({}, textFontAttrs, {
Expand Down
12 changes: 5 additions & 7 deletions src/traces/pie/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}
}

handleDomainDefaults(traceOut, layout, coerce);

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(titlePosition === 'inhole' || titlePosition === 'outside') {
coerceFont(coerce, 'titlefont', layout.font);
}
if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
coerceFont(coerce, 'titlefont', layout.font);
}

handleDomainDefaults(traceOut, layout, coerce);

coerce('hole');

coerce('sort');
coerce('direction');
coerce('rotation');
Expand Down
147 changes: 91 additions & 56 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 @@ -309,11 +310,8 @@ module.exports = function plot(gd, cdpie) {
});

// add the title
var hasTitle = trace.title &&
((trace.titleposition === 'inhole' && trace.hole > 0) ||
(trace.titleposition === 'outside'));
var titleTextGroup = d3.select(this).selectAll('g.titletext')
.data(hasTitle ? [0] : []);
.data(trace.title ? [0] : []);

titleTextGroup.enter().append('g')
.classed('titletext', true);
Expand All @@ -329,30 +327,23 @@ module.exports = function plot(gd, cdpie) {
.attr({
'class': 'titletext',
transform: '',
'text-anchor': 'middle'
'text-anchor': 'middle',
})
.call(Drawing.font, trace.titlefont)
.call(svgTextUtils.convertToTspans, gd);


var titleBB = Drawing.bBox(titleText.node());
// translation and scaling for the title text box.
// The translation is for the center point.
var transform;

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

titleText.attr('transform',
'translate(' + transform.x + ',' + transform.y + ')' +
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
'translate(' +
(-(titleBB.left + titleBB.right) / 2) + ',' +
(-(titleBB.top + titleBB.bottom) / 2) +
')');
'translate(' + transform.tx + ',' + transform.ty + ')');
});

// now make sure no labels overlap (at least within one pie)
Expand Down Expand Up @@ -418,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 @@ -501,59 +514,76 @@ function transformOutsideText(textBB, pt) {
};
}

function positionTitleInside(titleBB, cd0) {
var textDiameter = Math.sqrt(titleBB.width * titleBB.width + titleBB.height * titleBB.height);
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
scale: cd0.trace.hole * cd0.r * 2 / textDiameter,
tx: 0,
ty: - cd0.titleBox.height / 2 + cd0.trace.titlefont.size
};
}

function positionTitleOutside(titleBB, cd0, plotSize) {
var scaleX, scaleY, chartWidth, titleSpace, titleShift, maxPull;
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);
chartWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
scaleX = chartWidth / titleBB.width;
if(isSinglePie(trace)) {
titleShift = trace.titlefont.size / 2;
// we need to leave enough free space for an outside label
if(trace.outsidetextfont) titleShift += 3 * trace.outsidetextfont.size / 2;
else titleShift += trace.titlefont.size / 4;
return {
x: cd0.cx,
y: cd0.cy - (1 + maxPull) * cd0.r - titleShift,
scale: scaleX
};

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;
}
titleSpace = getTitleSpace(trace, plotSize);
// we previously left a free space of height titleSpace.
// The text must fit in this space.
scaleY = titleSpace / titleBB.height;
scaleX = maxWidth / cd0.titleBox.width;
scaleY = getTitleSpace(cd0, plotSize) / cd0.titleBox.height;
return {
x: cd0.cx,
y: cd0.cy - (1 + maxPull) * cd0.r - (titleSpace / 2),
scale: Math.min(scaleX, scaleY)
x: topMiddle.x,
y: topMiddle.y,
scale: Math.min(scaleX, scaleY),
tx: translate.tx,
ty: translate.ty
};
}

function isSinglePie(trace) {
// check if there is a single pie per y-column
if(trace.domain.y[0] === 0 && trace.domain.y[1] === 1) return true;
return false;
}

function getTitleSpace(trace, plotSize) {
var chartHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
// leave 3/2 * titlefont.size free space. We need at least titlefont.size
// space, and the 1/2 * titlefont.size is a small buffer to avoid the text
// touching the pie.
var titleSpace = (trace.title && trace.titleposition === 'outside') ?
(3 * trace.titlefont.size / 2) : 0;
if(chartHeight > titleSpace) return titleSpace;
else return chartHeight / 2;
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) {
Expand Down Expand Up @@ -687,14 +717,19 @@ function scalePies(cdpie, plotSize) {
pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
// leave some space for the title, if it will be displayed outside
if(!isSinglePie(trace)) pieBoxHeight -= getTitleSpace(trace, plotSize);
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 * (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 removed test/image/baselines/pie_title_inhole.png
Binary file not shown.
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 modified 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 modified 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"values": [955, 405, 360, 310, 295],
"labels": ["Mandarin", "Spanish", "English", "Hindi", "Arabic"],
"textinfo": "label+percent",
"hole": 0.3,
"hole": 0.1,
"title": "Num. speakers",
"titleposition": "inhole",
"titleposition": "middle center",
"type": "pie"
}
],
Expand Down
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