diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index fcc44024ba0..b675866b517 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -290,6 +290,17 @@ function plot(gd, data, layout, config) { subroutines.drawMarginPushers(gd); Axes.allowAutoMargin(gd); + // TODO can this be moved elsewhere? + if(fullLayout._has('pie')) { + var fullData = gd._fullData; + for(var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + if(trace.type === 'pie' && trace.automargin) { + Plots.allowAutoMargin(gd, 'pie.' + trace.uid + '.automargin'); + } + } + } + Plots.doAutoMargin(gd); return Plots.previousPromises(gd); } diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index 04518282f6a..a1ec2203f6d 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -187,6 +187,15 @@ module.exports = { outsidetextfont: extendFlat({}, textFontAttrs, { description: 'Sets the font used for `textinfo` lying outside the sector.' }), + automargin: { + valType: 'boolean', + dflt: false, + role: 'info', + editType: 'plot', + description: [ + 'Determines whether outside text labels can push the margins.' + ].join(' ') + }, title: { text: { diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index c25b46b7671..0d7cfc47343 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -64,6 +64,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout moduleHasTextangle: false, moduleHasInsideanchor: false }); + + var hasBoth = Array.isArray(textposition) || textposition === 'auto'; + var hasOutside = hasBoth || textposition === 'outside'; + if(hasOutside) { + coerce('automargin'); + } } handleDomainDefaults(traceOut, layout, coerce); diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 96d39004028..519c8d3a821 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -10,6 +10,7 @@ var d3 = require('d3'); +var Plots = require('../../plots/plots'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); @@ -22,9 +23,10 @@ var isValidTextValue = require('../../lib').isValidTextValue; function plot(gd, cdModule) { var fullLayout = gd._fullLayout; + var gs = fullLayout._size; prerenderTitles(cdModule, gd); - layoutAreas(cdModule, fullLayout._size); + layoutAreas(cdModule, gs); var plotGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdModule, 'trace').each(function(cd) { var plotGroup = d3.select(this); @@ -228,7 +230,7 @@ function plot(gd, cdModule) { if(trace.title.position === 'middle center') { transform = positionTitleInside(cd0); } else { - transform = positionTitleOutside(cd0, fullLayout._size); + transform = positionTitleOutside(cd0, gs); } titleText.attr('transform', @@ -241,6 +243,31 @@ function plot(gd, cdModule) { if(hasOutsideText) scootLabels(quadrants, trace); plotTextLines(slices, trace); + + if(hasOutsideText && trace.automargin) { + // TODO if we ever want to improve perf, + // we could reuse the textBB computed above together + // with the sliceText transform info + var traceBbox = Drawing.bBox(plotGroup.node()); + + var domain = trace.domain; + var vpw = gs.w * (domain.x[1] - domain.x[0]); + var vph = gs.h * (domain.y[1] - domain.y[0]); + var xgap = (0.5 * vpw - cd0.r) / gs.w; + var ygap = (0.5 * vph - cd0.r) / gs.h; + + Plots.autoMargin(gd, 'pie.' + trace.uid + '.automargin', { + xl: domain.x[0] - xgap, + xr: domain.x[1] + xgap, + yb: domain.y[0] - ygap, + yt: domain.y[1] + ygap, + l: Math.max(cd0.cx - cd0.r - traceBbox.left, 0), + r: Math.max(traceBbox.right - (cd0.cx + cd0.r), 0), + b: Math.max(traceBbox.bottom - (cd0.cy + cd0.r), 0), + t: Math.max(cd0.cy - cd0.r - traceBbox.top, 0), + pad: 5 + }); + } }); }); diff --git a/test/image/baselines/pie_automargin-margin0.png b/test/image/baselines/pie_automargin-margin0.png new file mode 100644 index 00000000000..5d357496836 Binary files /dev/null and b/test/image/baselines/pie_automargin-margin0.png differ diff --git a/test/image/baselines/pie_automargin.png b/test/image/baselines/pie_automargin.png new file mode 100644 index 00000000000..b15533e1074 Binary files /dev/null and b/test/image/baselines/pie_automargin.png differ diff --git a/test/image/mocks/pie_automargin-margin0.json b/test/image/mocks/pie_automargin-margin0.json new file mode 100644 index 00000000000..298fb10eabb --- /dev/null +++ b/test/image/mocks/pie_automargin-margin0.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "values": [1, 2, 3, 4, 5], + "labels": ["apples", "oranges", "blueberries", "lemons", "watermelon"], + "marker": { + "colors": ["rgba(255,0,0,0.5)", "orange", "blue", "yellow", "green"] + }, + "text": ["red delicious", "mandarin", "high bush", "meyer", "jubilee"], + "textinfo": "label+text+value+percent", + "textposition": "outside", + "automargin": true, + "type": "pie" + } + ], + "layout": { + "height": 400, + "width": 400, + "margin": {"t": 0, "b": 0, "l": 0, "r": 0}, + "showlegend": false + } +} diff --git a/test/image/mocks/pie_automargin.json b/test/image/mocks/pie_automargin.json new file mode 100644 index 00000000000..2c66dd37c3d --- /dev/null +++ b/test/image/mocks/pie_automargin.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "type": "pie", + "labels": ["Olive Development", "Gap Development", "Gap Extension", "Gap Wildcat", "Olive Injection", "Olive Wildcat"], + "values": [39.4, 54.1, 3.08, 2.98, 0.411, 0.685], + "textinfo": "label+percent", + "hole": 0.5, + "automargin": true + } + ], + "layout": { + "legend": { + "orientation": "h", + "y": 1.2, + "yanchor": "bottom", + "x": 0.5, + "xanchor": "center" + }, + "width": 700, + "height": 450 + } +} diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 7cbe3941506..b8303f19897 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -72,6 +72,20 @@ describe('Pie defaults', function() { var out = _supply({type: 'pie', values: [1, 2], textfont: {color: 'blue'}}, {font: {color: 'red'}}); expect(out.insidetextfont.color).toBe('blue'); }); + + it('should only coerce *automargin* if there are outside labels', function() { + var out = _supply({type: 'pie', labels: ['A', 'B'], values: [1, 2], automargin: true}); + expect(out.automargin).toBe(true, 'dflt textposition'); + + var out2 = _supply({type: 'pie', labels: ['A', 'B'], values: [1, 2], automargin: true, textposition: 'inside'}); + expect(out2.automargin).toBe(undefined, 'textposition inside'); + + var out3 = _supply({type: 'pie', labels: ['A', 'B'], values: [1, 2], automargin: true, textposition: 'none'}); + expect(out3.automargin).toBe(undefined, 'textposition none'); + + var out4 = _supply({type: 'pie', labels: ['A', 'B'], values: [1, 2], automargin: true, textposition: 'outside'}); + expect(out4.automargin).toBe(true, 'textposition outside'); + }); }); describe('Pie traces', function() { @@ -864,6 +878,82 @@ describe('Pie traces', function() { .then(done); }); + it('should grow and shrink margins under *automargin:true*', function(done) { + var data = [{ + type: 'pie', + values: [1, 2, 3, 4, 5], + labels: ['apples', 'oranges', 'blueberries', 'lemons', 'watermelon'], + textinfo: 'label+text+value+percent', + textposition: 'outside', + automargin: false + }]; + var layout = { + width: 400, height: 400, + margin: {t: 0, b: 0, l: 0, r: 0}, + showlegend: false + }; + + var previousSize; + function assertSize(msg, actual, exp) { + for(var k in exp) { + var parts = exp[k].split('|'); + var op = parts[0]; + + var method = { + '=': 'toBe', + '~=': 'toBeWithin', + grew: 'toBeGreaterThan', + shrunk: 'toBeLessThan' + }[op]; + + var val = previousSize[k]; + var msgk = msg + ' ' + k + (parts[1] ? ' |' + parts[1] : ''); + var args = op === '~=' ? [val, 1.1, msgk] : [val, msgk, '']; + + expect(actual[k])[method](args[0], args[1], args[2]); + } + } + + function check(msg, restyleObj, exp) { + return function() { + return Plotly.restyle(gd, restyleObj).then(function() { + var gs = Lib.extendDeep({}, gd._fullLayout._size); + assertSize(msg, gs, exp); + previousSize = gs; + }); + }; + } + + Plotly.plot(gd, data, layout) + .then(function() { + var gs = gd._fullLayout._size; + previousSize = Lib.extendDeep({}, gs); + }) + .then(check('automargin:true', {automargin: true}, { + t: 'grew', l: 'grew', + b: 'grew', r: 'grew' + })) + .then(check('smaller font size', {'outsidetextfont.size': 8}, { + t: 'shrunk', l: 'shrunk', + b: 'shrunk', r: 'shrunk' + })) + .then(check('arrayOk textposition', { + textposition: [['outside', 'outside', 'inside', 'inside', 'outside']], + 'outsidetextfont.size': 12 + }, { + t: '~=', l: 'shrunk', + b: 'grew', r: 'grew' + })) + .then(check('automargin:false', {automargin: false}, { + t: 'shrunk', l: 'shrunk', + b: 'shrunk', r: 'shrunk' + })) + .catch(failTest) + .then(done); + }); +}); + +describe('Pie texttemplate:', function() { checkTextTemplate([{ type: 'pie', values: [1, 5, 3, 2],