Skip to content

Commit ed86959

Browse files
committed
Add more options to titleposition.
This commit changes titleposition to be of the form 'top left', 'top center', ... I added unit tests for correct placement in all possible positions. I also added mocks using multiline titles.
1 parent 79b00ab commit ed86959

19 files changed

+511
-86
lines changed

src/traces/pie/attributes.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,16 @@ module.exports = {
192192
},
193193
titleposition: {
194194
valType: 'enumerated',
195-
values: ['inhole', 'outside'],
196-
dflt: 'outside',
195+
values: [
196+
'top left', 'top center', 'top right',
197+
'middle center',
198+
'bottom left', 'bottom center', 'bottom right'
199+
],
200+
dflt: 'top center',
197201
role: 'info',
198202
editType: 'calc',
199203
description: [
200204
'Specifies the location of the `title`.',
201-
'If `inhole` and the chart is a donut, the text is scaled',
202-
'and displayed inside the hole.',
203-
'If `outside`, the text is shown above the pie chart.'
204205
].join(' ')
205206
},
206207
titlefont: extendFlat({}, textFontAttrs, {

src/traces/pie/defaults.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
6565
}
6666
}
6767

68+
handleDomainDefaults(traceOut, layout, coerce);
69+
70+
var hole = coerce('hole');
6871
var title = coerce('title');
6972
if(title) {
7073
var titlePosition = coerce('titleposition');
7174

72-
if(titlePosition === 'inhole' || titlePosition === 'outside') {
73-
coerceFont(coerce, 'titlefont', layout.font);
74-
}
75+
if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
76+
coerceFont(coerce, 'titlefont', layout.font);
7577
}
7678

77-
handleDomainDefaults(traceOut, layout, coerce);
78-
79-
coerce('hole');
80-
8179
coerce('sort');
8280
coerce('direction');
8381
coerce('rotation');

src/traces/pie/plot.js

Lines changed: 91 additions & 56 deletions
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) {
@@ -309,11 +310,8 @@ module.exports = function plot(gd, cdpie) {
309310
});
310311

311312
// add the title
312-
var hasTitle = trace.title &&
313-
((trace.titleposition === 'inhole' && trace.hole > 0) ||
314-
(trace.titleposition === 'outside'));
315313
var titleTextGroup = d3.select(this).selectAll('g.titletext')
316-
.data(hasTitle ? [0] : []);
314+
.data(trace.title ? [0] : []);
317315

318316
titleTextGroup.enter().append('g')
319317
.classed('titletext', true);
@@ -329,30 +327,23 @@ module.exports = function plot(gd, cdpie) {
329327
.attr({
330328
'class': 'titletext',
331329
transform: '',
332-
'text-anchor': 'middle'
330+
'text-anchor': 'middle',
333331
})
334332
.call(Drawing.font, trace.titlefont)
335333
.call(svgTextUtils.convertToTspans, gd);
336334

337-
338-
var titleBB = Drawing.bBox(titleText.node());
339-
// translation and scaling for the title text box.
340-
// The translation is for the center point.
341335
var transform;
342336

343-
if(trace.titleposition === 'outside') {
344-
transform = positionTitleOutside(titleBB, cd0, fullLayout._size);
337+
if(trace.titleposition === 'middle center') {
338+
transform = positionTitleInside(cd0);
345339
} else {
346-
transform = positionTitleInside(titleBB, cd0);
340+
transform = positionTitleOutside(cd0, fullLayout._size);
347341
}
348342

349343
titleText.attr('transform',
350344
'translate(' + transform.x + ',' + transform.y + ')' +
351345
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
352-
'translate(' +
353-
(-(titleBB.left + titleBB.right) / 2) + ',' +
354-
(-(titleBB.top + titleBB.bottom) / 2) +
355-
')');
346+
'translate(' + transform.tx + ',' + transform.ty + ')');
356347
});
357348

358349
// now make sure no labels overlap (at least within one pie)
@@ -418,6 +409,28 @@ module.exports = function plot(gd, cdpie) {
418409
}, 0);
419410
};
420411

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+
}
421434

422435
function transformInsideText(textBB, pt, cd0) {
423436
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
@@ -501,59 +514,76 @@ function transformOutsideText(textBB, pt) {
501514
};
502515
}
503516

504-
function positionTitleInside(titleBB, cd0) {
505-
var textDiameter = Math.sqrt(titleBB.width * titleBB.width + titleBB.height * titleBB.height);
517+
function positionTitleInside(cd0) {
518+
var textDiameter =
519+
Math.sqrt(cd0.titleBox.width * cd0.titleBox.width + cd0.titleBox.height * cd0.titleBox.height);
506520
return {
507521
x: cd0.cx,
508522
y: cd0.cy,
509-
scale: cd0.trace.hole * cd0.r * 2 / textDiameter
523+
scale: cd0.trace.hole * cd0.r * 2 / textDiameter,
524+
tx: 0,
525+
ty: - cd0.titleBox.height / 2 + cd0.trace.titlefont.size
510526
};
511527
}
512528

513-
function positionTitleOutside(titleBB, cd0, plotSize) {
514-
var scaleX, scaleY, chartWidth, titleSpace, titleShift, maxPull;
529+
function positionTitleOutside(cd0, plotSize) {
530+
var scaleX = 1, scaleY = 1, maxWidth, maxPull;
515531
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+
};
516544

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;
517549
maxPull = getMaxPull(trace);
518-
chartWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
519-
scaleX = chartWidth / titleBB.width;
520-
if(isSinglePie(trace)) {
521-
titleShift = trace.titlefont.size / 2;
522-
// we need to leave enough free space for an outside label
523-
if(trace.outsidetextfont) titleShift += 3 * trace.outsidetextfont.size / 2;
524-
else titleShift += trace.titlefont.size / 4;
525-
return {
526-
x: cd0.cx,
527-
y: cd0.cy - (1 + maxPull) * cd0.r - titleShift,
528-
scale: scaleX
529-
};
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;
530570
}
531-
titleSpace = getTitleSpace(trace, plotSize);
532-
// we previously left a free space of height titleSpace.
533-
// The text must fit in this space.
534-
scaleY = titleSpace / titleBB.height;
571+
scaleX = maxWidth / cd0.titleBox.width;
572+
scaleY = getTitleSpace(cd0, plotSize) / cd0.titleBox.height;
535573
return {
536-
x: cd0.cx,
537-
y: cd0.cy - (1 + maxPull) * cd0.r - (titleSpace / 2),
538-
scale: Math.min(scaleX, scaleY)
574+
x: topMiddle.x,
575+
y: topMiddle.y,
576+
scale: Math.min(scaleX, scaleY),
577+
tx: translate.tx,
578+
ty: translate.ty
539579
};
540580
}
541581

542-
function isSinglePie(trace) {
543-
// check if there is a single pie per y-column
544-
if(trace.domain.y[0] === 0 && trace.domain.y[1] === 1) return true;
545-
return false;
546-
}
547-
548-
function getTitleSpace(trace, plotSize) {
549-
var chartHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
550-
// leave 3/2 * titlefont.size free space. We need at least titlefont.size
551-
// space, and the 1/2 * titlefont.size is a small buffer to avoid the text
552-
// touching the pie.
553-
var titleSpace = (trace.title && trace.titleposition === 'outside') ?
554-
(3 * trace.titlefont.size / 2) : 0;
555-
if(chartHeight > titleSpace) return titleSpace;
556-
else return chartHeight / 2;
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 pie
586+
return Math.min(cd0.titleBox.height, pieBoxHeight / 2);
557587
}
558588

559589
function getMaxPull(trace) {
@@ -687,14 +717,19 @@ function scalePies(cdpie, plotSize) {
687717
pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]);
688718
pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]);
689719
// leave some space for the title, if it will be displayed outside
690-
if(!isSinglePie(trace)) pieBoxHeight -= getTitleSpace(trace, plotSize);
720+
if(trace.title && trace.titleposition !== 'middle center') {
721+
pieBoxHeight -= getTitleSpace(cd0, plotSize);
722+
}
691723

692724
maxPull = getMaxPull(trace);
693725

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

696728
cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2;
697729
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+
}
698733

699734
if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) {
700735
scaleGroups.push(trace.scalegroup);
21.6 KB
Loading
-37.8 KB
Binary file not shown.
36.7 KB
Loading
Loading
-1.76 KB
Loading
1.05 KB
Loading
13.5 KB
Loading

0 commit comments

Comments
 (0)