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 = [ {