diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js
index 9a901c61735..d81ba172554 100644
--- a/src/traces/pie/attributes.js
+++ b/src/traces/pie/attributes.js
@@ -180,6 +180,33 @@ 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'
+ ],
+ 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'}),
diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js
index a19b5e097c2..2a03a68075a 100644
--- a/src/traces/pie/defaults.js
+++ b/src/traces/pie/defaults.js
@@ -67,7 +67,13 @@ 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', hole ? 'middle center' : 'top center');
+ if(!hole && titlePosition === 'middle center') traceOut.titleposition = 'top center';
+ coerceFont(coerce, 'titlefont', layout.font);
+ }
coerce('sort');
coerce('direction');
diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js
index 2e71d08674b..27c3518a705 100644
--- a/src/traces/pie/plot.js
+++ b/src/traces/pie/plot.js
@@ -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) {
@@ -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) {
@@ -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);
@@ -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 title
+ 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,
@@ -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);
diff --git a/test/image/baselines/pie_title_groupscale.png b/test/image/baselines/pie_title_groupscale.png
new file mode 100644
index 00000000000..26ef09139f0
Binary files /dev/null and b/test/image/baselines/pie_title_groupscale.png differ
diff --git a/test/image/baselines/pie_title_middle_center.png b/test/image/baselines/pie_title_middle_center.png
new file mode 100644
index 00000000000..6ea5b0179c3
Binary files /dev/null and b/test/image/baselines/pie_title_middle_center.png differ
diff --git a/test/image/baselines/pie_title_middle_center_multiline.png b/test/image/baselines/pie_title_middle_center_multiline.png
new file mode 100644
index 00000000000..2e04b1330d0
Binary files /dev/null and b/test/image/baselines/pie_title_middle_center_multiline.png differ
diff --git a/test/image/baselines/pie_title_multiple.png b/test/image/baselines/pie_title_multiple.png
new file mode 100644
index 00000000000..388c923d820
Binary files /dev/null and b/test/image/baselines/pie_title_multiple.png differ
diff --git a/test/image/baselines/pie_title_pull.png b/test/image/baselines/pie_title_pull.png
new file mode 100644
index 00000000000..83945eeb9e9
Binary files /dev/null and b/test/image/baselines/pie_title_pull.png differ
diff --git a/test/image/baselines/pie_title_subscript.png b/test/image/baselines/pie_title_subscript.png
new file mode 100644
index 00000000000..04fb7b3e0df
Binary files /dev/null and b/test/image/baselines/pie_title_subscript.png differ
diff --git a/test/image/baselines/pie_title_variations.png b/test/image/baselines/pie_title_variations.png
new file mode 100644
index 00000000000..abc8877c0af
Binary files /dev/null and b/test/image/baselines/pie_title_variations.png differ
diff --git a/test/image/mocks/pie_title_groupscale.json b/test/image/mocks/pie_title_groupscale.json
new file mode 100644
index 00000000000..9ea1aa5b4d4
--- /dev/null
+++ b/test/image/mocks/pie_title_groupscale.json
@@ -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
Night
Cafe",
+ "scalegroup": "1"
+ }
+ ],
+ "layout": {
+ "height": 400,
+ "width": 500
+ }
+}
diff --git a/test/image/mocks/pie_title_middle_center.json b/test/image/mocks/pie_title_middle_center.json
new file mode 100644
index 00000000000..064e5af3f61
--- /dev/null
+++ b/test/image/mocks/pie_title_middle_center.json
@@ -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
+ }
+}
diff --git a/test/image/mocks/pie_title_middle_center_multiline.json b/test/image/mocks/pie_title_middle_center_multiline.json
new file mode 100644
index 00000000000..1000b76d4bf
--- /dev/null
+++ b/test/image/mocks/pie_title_middle_center_multiline.json
@@ -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
of
speakers",
+ "titleposition": "middle center",
+ "type": "pie"
+ }
+ ],
+ "layout": {
+ "title": "Top 5 languages by number of native speakers (2010, est.)",
+ "height": 600,
+ "width": 600
+ }
+}
diff --git a/test/image/mocks/pie_title_multiple.json b/test/image/mocks/pie_title_multiple.json
new file mode 100644
index 00000000000..8eaf1ab8835
--- /dev/null
+++ b/test/image/mocks/pie_title_multiple.json
@@ -0,0 +1,62 @@
+{
+ "data": [
+ {
+ "values": [38600, 83100, 11100, 15400, 1740, 77],
+ "labels": ["Platinum", "Palladium", "Rhodium", "Ruthenium", "Iridium", "Osmium"],
+ "type": "pie",
+ "name": "Year 2013",
+ "title": "Year
2013",
+ "titleposition": "top left",
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0.51, 1]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },{
+ "values": [45800, 92900, 11100, 11000, 1960, 322],
+ "labels": ["Platinum", "Palladium", "Rhodium", "Ruthenium", "Iridium", "Osmium"],
+ "type": "pie",
+ "name": "Year 2014",
+ "title": "Year
2014",
+ "titleposition": "top right",
+ "domain": {
+ "x": [0.51, 1],
+ "y": [0.51, 1]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },{
+ "values": [42700, 85300, 10600, 8230, 1010, 8],
+ "labels": ["Platinum", "Palladium", "Rhodium", "Ruthenium", "Iridium", "Osmium"],
+ "type": "pie",
+ "name": "Year 2015",
+ "title": "Year
2015",
+ "titleposition": "bottom left",
+ "domain": {
+ "x": [0, 0.5],
+ "y": [0, 0.5]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ },{
+ "values": [42300, 80400, 10700, 8410, 1300, 27],
+ "labels": ["Platinum", "Palladium", "Rhodium", "Ruthenium", "Iridium", "Osmium"],
+ "type": "pie",
+ "name": "Year 2016",
+ "title": "Year
2016",
+ "titleposition": "bottom right",
+ "domain": {
+ "x": [0.51, 1],
+ "y": [0, 0.5]
+ },
+ "hoverinfo": "label+percent+name",
+ "textinfo": "none"
+ }
+ ],
+ "layout": {
+ "title": "U.S. Imports for Platinum-group Metals",
+ "height": 400,
+ "width": 500
+ }
+}
diff --git a/test/image/mocks/pie_title_pull.json b/test/image/mocks/pie_title_pull.json
new file mode 100644
index 00000000000..53566b94456
--- /dev/null
+++ b/test/image/mocks/pie_title_pull.json
@@ -0,0 +1,24 @@
+{
+ "data": [
+ {
+ "values": [
+ 50, 49, 48, 47, 46, 45, 44, 43, 42, 41
+ ],
+ "pull": [
+ 0, 0.5, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0.5
+ ],
+ "sort": false,
+ "type": "pie",
+ "textposition": "inside",
+ "title": "Withering
Flower",
+ "titlefont": {
+ "size": 16
+ }
+ }
+ ],
+ "layout": {
+ "height": 600,
+ "width": 400,
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/pie_title_subscript.json b/test/image/mocks/pie_title_subscript.json
new file mode 100644
index 00000000000..1df4bcab424
--- /dev/null
+++ b/test/image/mocks/pie_title_subscript.json
@@ -0,0 +1,20 @@
+{
+ "data": [
+ {
+ "values": [2, 3, 5, 7, 9],
+ "labels": ["2", "3", "5", "7", "9"],
+ "type": "pie",
+ "name": "Prime numbers",
+ "textinfo": "none",
+ "title": "CO2
H2O",
+ "titlefont": {
+ "size": 14
+ },
+ "titleposition": "top center"
+ }
+ ],
+ "layout": {
+ "height": 400,
+ "width": 400
+ }
+}
diff --git a/test/image/mocks/pie_title_variations.json b/test/image/mocks/pie_title_variations.json
new file mode 100644
index 00000000000..9d20c4e1db8
--- /dev/null
+++ b/test/image/mocks/pie_title_variations.json
@@ -0,0 +1,79 @@
+{
+ "data": [
+ {
+ "values": [38, 27, 18, 10, 7],
+ "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 (following a well-known painting by Vincent Van Gogh)",
+ "titleposition": "bottom left"
+ },
+ {
+ "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 right",
+ "titlefont": {
+ "size": 14
+ }
+ },
+ {
+ "values": [38, 19, 16, 14, 13],
+ "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": 14
+ }
+ },
+ {
+ "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
Night
Cafe"
+ }
+ ],
+ "layout": {
+ "height": 400,
+ "width": 500
+ }
+}
diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js
index 2c54323809b..2880a514f5b 100644
--- a/test/jasmine/tests/pie_test.js
+++ b/test/jasmine/tests/pie_test.js
@@ -185,6 +185,253 @@ describe('Pie traces:', function() {
.then(done);
});
+ it('shows multiline title in hole', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ hole: 0.5,
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(function() {
+ var title = d3.selectAll('.titletext text');
+ expect(title.size()).toBe(1);
+ var titlePos = getClientPosition('g.titletext');
+ var pieCenterPos = getClientPosition('g.trace');
+ expect(Math.abs(titlePos[0] - pieCenterPos[0])).toBeLessThan(2);
+ expect(Math.abs(titlePos[1] - pieCenterPos[1])).toBeLessThan(2);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ function _verifyPointInCircle(x, y, circleCenter, radius) {
+ var dist = Math.pow(x - circleCenter[0], 2) + Math.pow(y - circleCenter[1], 2);
+ return Math.abs(Math.sqrt(dist) - radius);
+ }
+
+ it('scales multiline title to fit in hole', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ titleposition: 'middle center',
+ titlefont: {
+ size: 60
+ },
+ hole: 0.1,
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(function() {
+ var title = d3.selectAll('.titletext text');
+ expect(title.size()).toBe(1);
+ var titleBox = d3.select('g.titletext').node().getBoundingClientRect();
+ var pieBox = d3.select('g.trace').node().getBoundingClientRect();
+ var radius = 0.1 * Math.min(pieBox.width / 2, pieBox.height / 2);
+ var pieCenterPos = getClientPosition('g.trace');
+ // unfortunately boundingClientRect is inaccurate and so we allow an error of 2
+ expect(_verifyPointInCircle(titleBox.left, titleBox.top, pieCenterPos, radius))
+ .toBeLessThan(2);
+ expect(_verifyPointInCircle(titleBox.right, titleBox.top, pieCenterPos, radius))
+ .toBeLessThan(2);
+ expect(_verifyPointInCircle(titleBox.left, titleBox.bottom, pieCenterPos, radius))
+ .toBeLessThan(2);
+ expect(_verifyPointInCircle(titleBox.right, titleBox.bottom, pieCenterPos, radius))
+ .toBeLessThan(2);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ function _verifyTitle(checkLeft, checkRight, checkTop, checkBottom, checkMiddleX) {
+ return function() {
+ var title = d3.selectAll('.titletext text');
+ expect(title.size()).toBe(1);
+ var titleBox = d3.select('g.titletext').node().getBoundingClientRect();
+ var pieBox = d3.select('g.trace').node().getBoundingClientRect();
+ // check that margins agree. we leave an error margin of 2.
+ if(checkLeft) expect(Math.abs(titleBox.left - pieBox.left)).toBeLessThan(2);
+ if(checkRight) expect(Math.abs(titleBox.right - pieBox.right)).toBeLessThan(2);
+ if(checkTop) expect(Math.abs(titleBox.top - pieBox.top)).toBeLessThan(2);
+ if(checkBottom) expect(Math.abs(titleBox.bottom - pieBox.bottom)).toBeLessThan(2);
+ if(checkMiddleX) {
+ expect(Math.abs(titleBox.left + titleBox.right - pieBox.left - pieBox.right))
+ .toBeLessThan(2);
+ }
+ };
+ }
+
+ it('shows title top center if hole is zero', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ titleposition: 'middle center',
+ titlefont: {
+ size: 12
+ },
+ hole: 0,
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top center if titleposition is undefined and no hole', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top center', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [1, 1, 1, 1, 2],
+ title: 'Test
Title',
+ titleposition: 'top center',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top left', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [3, 2, 1],
+ title: 'Test
Title',
+ titleposition: 'top left',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(true, false, true, false, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title top right', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [4, 5, 6, 5],
+ title: 'Test
Title',
+ titleposition: 'top right',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, true, true, false, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title bottom left', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [4, 5, 6, 5],
+ title: 'Test
Title',
+ titleposition: 'bottom left',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(true, false, false, true, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title bottom center', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [4, 5, 6, 5],
+ title: 'Test
Title',
+ titleposition: 'bottom center',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, false, true, true))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('shows title bottom right', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [4, 5, 6, 5],
+ title: 'Test
Title',
+ titleposition: 'bottom right',
+ titlefont: {
+ size: 12
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, true, false, true, false))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('does not intersect pulled slices', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [2, 2, 2, 2],
+ title: 'Test
Title',
+ titleposition: 'top center',
+ titlefont: {
+ size: 14
+ },
+ pull: [0.9, 0.9, 0.9, 0.9],
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(function() {
+ var title = d3.selectAll('.titletext text');
+ expect(title.size()).toBe(1);
+ var titleBox = d3.select('g.titletext').node().getBoundingClientRect();
+ var minSliceTop = Infinity;
+ d3.selectAll('g.slice').each(function() {
+ var sliceTop = d3.select(this).node().getBoundingClientRect().top;
+ minSliceTop = Math.min(minSliceTop, sliceTop);
+ });
+ expect(titleBox.bottom).toBeLessThan(minSliceTop);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('correctly positions large title', function(done) {
+ Plotly.newPlot(gd, [{
+ values: [1, 3, 4, 1, 2],
+ title: 'Test
Title',
+ titleposition: 'top center',
+ titlefont: {
+ size: 60
+ },
+ type: 'pie',
+ textinfo: 'none'
+ }], {height: 300, width: 300})
+ .then(_verifyTitle(false, false, true, false, true))
+ .catch(failTest)
+ .then(done);
+ });
+
it('supports separate stroke width values per slice', function(done) {
var data = [
{