Skip to content

Commit c2f868c

Browse files
authored
Merge pull request #2987 from codrut3/pie_title
Pie title
2 parents 67a05c7 + bce5add commit c2f868c

18 files changed

+738
-9
lines changed

src/traces/pie/attributes.js

+27
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,33 @@ module.exports = {
180180
description: 'Sets the font used for `textinfo` lying outside the pie.'
181181
}),
182182

183+
title: {
184+
valType: 'string',
185+
dflt: '',
186+
role: 'info',
187+
editType: 'calc',
188+
description: [
189+
'Sets the title of the pie chart.',
190+
'If it is empty, no title is displayed.'
191+
].join(' ')
192+
},
193+
titleposition: {
194+
valType: 'enumerated',
195+
values: [
196+
'top left', 'top center', 'top right',
197+
'middle center',
198+
'bottom left', 'bottom center', 'bottom right'
199+
],
200+
role: 'info',
201+
editType: 'calc',
202+
description: [
203+
'Specifies the location of the `title`.',
204+
].join(' ')
205+
},
206+
titlefont: extendFlat({}, textFontAttrs, {
207+
description: 'Sets the font used for `title`.'
208+
}),
209+
183210
// position and shape
184211
domain: domainAttrs({name: 'pie', trace: true, editType: 'calc'}),
185212

src/traces/pie/defaults.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
6767

6868
handleDomainDefaults(traceOut, layout, coerce);
6969

70-
coerce('hole');
70+
var hole = coerce('hole');
71+
var title = coerce('title');
72+
if(title) {
73+
var titlePosition = coerce('titleposition', hole ? 'middle center' : 'top center');
74+
if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
75+
coerceFont(coerce, 'titlefont', layout.font);
76+
}
7177

7278
coerce('sort');
7379
coerce('direction');

src/traces/pie/plot.js

+153-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var eventData = require('./event_data');
2222
module.exports = function plot(gd, cdpie) {
2323
var fullLayout = gd._fullLayout;
2424

25+
prerenderTitles(cdpie, gd);
2526
scalePies(cdpie, fullLayout._size);
2627

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

312+
// add the title
313+
var titleTextGroup = d3.select(this).selectAll('g.titletext')
314+
.data(trace.title ? [0] : []);
315+
316+
titleTextGroup.enter().append('g')
317+
.classed('titletext', true);
318+
titleTextGroup.exit().remove();
319+
320+
titleTextGroup.each(function() {
321+
var titleText = Lib.ensureSingle(d3.select(this), 'text', '', function(s) {
322+
// prohibit tex interpretation as above
323+
s.attr('data-notex', 1);
324+
});
325+
326+
titleText.text(trace.title)
327+
.attr({
328+
'class': 'titletext',
329+
transform: '',
330+
'text-anchor': 'middle',
331+
})
332+
.call(Drawing.font, trace.titlefont)
333+
.call(svgTextUtils.convertToTspans, gd);
334+
335+
var transform;
336+
337+
if(trace.titleposition === 'middle center') {
338+
transform = positionTitleInside(cd0);
339+
} else {
340+
transform = positionTitleOutside(cd0, fullLayout._size);
341+
}
342+
343+
titleText.attr('transform',
344+
'translate(' + transform.x + ',' + transform.y + ')' +
345+
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
346+
'translate(' + transform.tx + ',' + transform.ty + ')');
347+
});
348+
311349
// now make sure no labels overlap (at least within one pie)
312350
if(hasOutsideText) scootLabels(quadrants, trace);
313351
slices.each(function(pt) {
@@ -371,6 +409,28 @@ module.exports = function plot(gd, cdpie) {
371409
}, 0);
372410
};
373411

412+
function prerenderTitles(cdpie, gd) {
413+
var cd0, trace;
414+
// Determine the width and height of the title for each pie.
415+
for(var i = 0; i < cdpie.length; i++) {
416+
cd0 = cdpie[i][0];
417+
trace = cd0.trace;
418+
419+
if(trace.title) {
420+
var dummyTitle = Drawing.tester.append('text')
421+
.attr('data-notex', 1)
422+
.text(trace.title)
423+
.call(Drawing.font, trace.titlefont)
424+
.call(svgTextUtils.convertToTspans, gd);
425+
var bBox = Drawing.bBox(dummyTitle.node(), true);
426+
cd0.titleBox = {
427+
width: bBox.width,
428+
height: bBox.height,
429+
};
430+
dummyTitle.remove();
431+
}
432+
}
433+
}
374434

375435
function transformInsideText(textBB, pt, cd0) {
376436
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
@@ -454,6 +514,89 @@ function transformOutsideText(textBB, pt) {
454514
};
455515
}
456516

517+
function positionTitleInside(cd0) {
518+
var textDiameter =
519+
Math.sqrt(cd0.titleBox.width * cd0.titleBox.width + cd0.titleBox.height * cd0.titleBox.height);
520+
return {
521+
x: cd0.cx,
522+
y: cd0.cy,
523+
scale: cd0.trace.hole * cd0.r * 2 / textDiameter,
524+
tx: 0,
525+
ty: - cd0.titleBox.height / 2 + cd0.trace.titlefont.size
526+
};
527+
}
528+
529+
function positionTitleOutside(cd0, plotSize) {
530+
var scaleX = 1, scaleY = 1, maxWidth, maxPull;
531+
var trace = cd0.trace;
532+
// position of the baseline point of the text box in the plot, before scaling.
533+
// we anchored the text in the middle, so the baseline is on the bottom middle
534+
// of the first line of text.
535+
var topMiddle = {
536+
x: cd0.cx,
537+
y: cd0.cy
538+
};
539+
// relative translation of the text box after scaling
540+
var translate = {
541+
tx: 0,
542+
ty: 0
543+
};
544+
545+
// we reason below as if the baseline is the top middle point of the text box.
546+
// so we must add the font size to approximate the y-coord. of the top.
547+
// note that this correction must happen after scaling.
548+
translate.ty += trace.titlefont.size;
549+
maxPull = getMaxPull(trace);
550+
551+
if(trace.titleposition.indexOf('top') !== -1) {
552+
topMiddle.y -= (1 + maxPull) * cd0.r;
553+
translate.ty -= cd0.titleBox.height;
554+
}
555+
else if(trace.titleposition.indexOf('bottom') !== -1) {
556+
topMiddle.y += (1 + maxPull) * cd0.r;
557+
}
558+
559+
if(trace.titleposition.indexOf('left') !== -1) {
560+
// we start the text at the left edge of the pie
561+
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
562+
topMiddle.x -= (1 + maxPull) * cd0.r;
563+
translate.tx += cd0.titleBox.width / 2;
564+
} else if(trace.titleposition.indexOf('center') !== -1) {
565+
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
566+
} else if(trace.titleposition.indexOf('right') !== -1) {
567+
maxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]) / 2 + cd0.r;
568+
topMiddle.x += (1 + maxPull) * cd0.r;
569+
translate.tx -= cd0.titleBox.width / 2;
570+
}
571+
scaleX = maxWidth / cd0.titleBox.width;
572+
scaleY = getTitleSpace(cd0, plotSize) / cd0.titleBox.height;
573+
return {
574+
x: topMiddle.x,
575+
y: topMiddle.y,
576+
scale: Math.min(scaleX, scaleY),
577+
tx: translate.tx,
578+
ty: translate.ty
579+
};
580+
}
581+
582+
function getTitleSpace(cd0, plotSize) {
583+
var trace = cd0.trace;
584+
var pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
585+
// use at most half of the plot for the title
586+
return Math.min(cd0.titleBox.height, pieBoxHeight / 2);
587+
}
588+
589+
function getMaxPull(trace) {
590+
var maxPull = trace.pull, j;
591+
if(Array.isArray(maxPull)) {
592+
maxPull = 0;
593+
for(j = 0; j < trace.pull.length; j++) {
594+
if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
595+
}
596+
}
597+
return maxPull;
598+
}
599+
457600
function scootLabels(quadrants, trace) {
458601
var xHalf, yHalf, equatorFirst, farthestX, farthestY,
459602
xDiffSign, yDiffSign, thisQuad, oppositeQuad,
@@ -570,21 +713,23 @@ function scalePies(cdpie, plotSize) {
570713
for(i = 0; i < cdpie.length; i++) {
571714
cd0 = cdpie[i][0];
572715
trace = cd0.trace;
716+
573717
pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
574718
pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
575-
576-
maxPull = trace.pull;
577-
if(Array.isArray(maxPull)) {
578-
maxPull = 0;
579-
for(j = 0; j < trace.pull.length; j++) {
580-
if(trace.pull[j] > maxPull) maxPull = trace.pull[j];
581-
}
719+
// leave some space for the title, if it will be displayed outside
720+
if(trace.title && trace.titleposition !== 'middle center') {
721+
pieBoxHeight -= getTitleSpace(cd0, plotSize);
582722
}
583723

724+
maxPull = getMaxPull(trace);
725+
584726
cd0.r = Math.min(pieBoxWidth, pieBoxHeight) / (2 + 2 * maxPull);
585727

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

589734
if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) {
590735
scaleGroups.push(trace.scalegroup);
21.6 KB
Loading
36.7 KB
Loading
Loading
26.5 KB
Loading
29.9 KB
Loading
13.5 KB
Loading
24.2 KB
Loading
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"data": [
3+
{
4+
"values": [118, 107, 98, 90, 87],
5+
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
6+
"type": "pie",
7+
"name": "Starry Night",
8+
"marker": {
9+
"colors": ["rgb(56, 75, 126)", "rgb(18, 36, 37)", "rgb(34, 53, 101)", "rgb(36, 55, 57)", "rgb(6, 4, 4)"]
10+
},
11+
"domain": {
12+
"x": [0, 0.48],
13+
"y": [0, 0.49]
14+
},
15+
"textinfo": "none",
16+
"title": "Starry Night",
17+
"titleposition": "bottom center",
18+
"scalegroup": "1"
19+
},
20+
{
21+
"values": [28, 26, 21, 15, 10],
22+
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
23+
"type": "pie",
24+
"name": "Sunflowers",
25+
"marker": {
26+
"colors": ["rgb(177, 127, 38)", "rgb(205, 152, 36)", "rgb(99, 79, 37)", "rgb(129, 180, 179)", "rgb(124, 103, 37)"]
27+
},
28+
"domain": {
29+
"x": [0.52, 1],
30+
"y": [0, 0.49]
31+
},
32+
"textinfo": "none",
33+
"title": "Sunflowers",
34+
"titleposition": "top center",
35+
"titlefont": {
36+
"size": 12
37+
},
38+
"scalegroup": "2"
39+
},
40+
{
41+
"values": [108, 109, 96, 84, 73],
42+
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
43+
"type": "pie",
44+
"name": "Irises",
45+
"marker": {
46+
"colors": ["rgb(33, 75, 99)", "rgb(79, 129, 102)", "rgb(151, 179, 100)", "rgb(175, 49, 35)", "rgb(36, 73, 147)"]
47+
},
48+
"domain": {
49+
"x": [0, 0.48],
50+
"y": [0.51, 1]
51+
},
52+
"textinfo": "none",
53+
"title": "Irises",
54+
"titlefont": {
55+
"size": 12
56+
},
57+
"scalegroup": "2"
58+
},
59+
{
60+
"values": [31, 24, 19, 18, 8],
61+
"labels": ["1st", "2nd", "3rd", "4th", "5th"],
62+
"type": "pie",
63+
"name": "The Night Cafe",
64+
"titlefont": {
65+
"size": 50
66+
},
67+
"marker": {
68+
"colors": ["rgb(146, 123, 21)", "rgb(177, 180, 34)", "rgb(206, 206, 40)", "rgb(175, 51, 21)", "rgb(35, 36, 21)"]
69+
},
70+
"domain": {
71+
"x": [0.52, 1],
72+
"y": [0.52, 1]
73+
},
74+
"textinfo": "none",
75+
"title": "The<br>Night<br>Cafe",
76+
"scalegroup": "1"
77+
}
78+
],
79+
"layout": {
80+
"height": 400,
81+
"width": 500
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"data": [
3+
{
4+
"values": [955, 405, 360, 310, 295],
5+
"labels": ["Mandarin", "Spanish", "English", "Hindi", "Arabic"],
6+
"textinfo": "label+percent",
7+
"hole": 0.1,
8+
"title": "Num. speakers",
9+
"titleposition": "middle center",
10+
"type": "pie"
11+
}
12+
],
13+
"layout": {
14+
"title": "Top 5 languages by number of native speakers (2010, est.)",
15+
"height": 600,
16+
"width": 600
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"data": [
3+
{
4+
"values": [955, 405, 360, 310, 295],
5+
"labels": ["Mandarin", "Spanish", "English", "Hindi", "Arabic"],
6+
"textinfo": "label+percent",
7+
"hole": 0.4,
8+
"title": "Number<br>of<br>speakers",
9+
"titleposition": "middle center",
10+
"type": "pie"
11+
}
12+
],
13+
"layout": {
14+
"title": "Top 5 languages by number of native speakers (2010, est.)",
15+
"height": 600,
16+
"width": 600
17+
}
18+
}

0 commit comments

Comments
 (0)