diff --git a/draftlogs/7580_add.md b/draftlogs/7580_add.md new file mode 100644 index 00000000000..1eb098ef851 --- /dev/null +++ b/draftlogs/7580_add.md @@ -0,0 +1 @@ +- Add support for arrays for the pie properties `showlegend` and `legend`, so that these can be configured per slice. [[#7580](https://github.com/plotly/plotly.js/pull/7580)], with thanks to @my-tien for the contribution! diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index ec8e91bca69..f8be07461f4 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -45,8 +45,39 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) { var shapesWithLegend = (layoutOut.shapes || []).filter(function(d) { return d.showlegend; }); + function isPieWithLegendArray(trace) { + return Registry.traceIs(trace, 'pie-like') + && trace._length != null + && (Array.isArray(trace.legend) || Array.isArray(trace.showlegend)); + }; + fullData + .filter(isPieWithLegendArray) + .forEach(function (trace) { + if (trace.visible) { + legendTraceCount++; + } + for(var index = 0; index < trace._length; index++) { + var legend = (Array.isArray(trace.legend) ? trace.legend[index] : trace.legend) || 'legend'; + if(legend === legendId) { + // showlegend can be boolean or a boolean array. + // will fall back to default if array index is out-of-range + const showInLegend = Array.isArray(trace.showlegend) ? trace.showlegend[index] : trace.showlegend; + if (showInLegend || trace._dfltShowLegend) { + legendReallyHasATrace = true; + legendTraceCount++; + } + } + } + if(legendId === 'legend' && trace._length > trace.legend.length) { + for(var idx = trace.legend.length; idx < trace._length; idx++) { + legendReallyHasATrace = true; + legendTraceCount++; + } + } + }); + var allLegendItems = fullData.concat(shapesWithLegend).filter(function(d) { - return legendId === (d.legend || 'legend'); + return !isPieWithLegendArray(trace) && legendId === (d.legend || 'legend'); }); for(var i = 0; i < allLegendItems.length; i++) { @@ -82,7 +113,6 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) { Lib.coerceFont(traceCoerce, 'legendgrouptitle.font', grouptitlefont); } - if((!isShape && Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') || ['tonextx', 'tonexty'].indexOf(trace.fill) !== -1) { defaultOrder = helpers.isGrouped({ traceorder: defaultOrder }) ? @@ -95,9 +125,15 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) { } } - var showLegend = Lib.coerce(layoutIn, layoutOut, - basePlotLayoutAttributes, 'showlegend', - legendReallyHasATrace && (legendTraceCount > (legendId === 'legend' ? 1 : 0))); + var showLegend = Lib.coerce( + layoutIn, + layoutOut, + basePlotLayoutAttributes, + 'showlegend', + layoutOut.showlegend || + (legendReallyHasATrace && + legendTraceCount > (legendId === 'legend' ? 1 : 0)) + ); // delete legend if(showLegend === false) layoutOut[legendId] = undefined; @@ -230,7 +266,11 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { var legends = ['legend']; for(i = 0; i < allLegendsData.length; i++) { - Lib.pushUnique(legends, allLegendsData[i].legend); + if (Array.isArray(allLegendsData[i].legend)) { + legends = legends.concat(allLegendsData[i].legend); + } else { + Lib.pushUnique(legends, allLegendsData[i].legend); + } } layoutOut._legends = []; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6a8106bbcc1..49c793d3f12 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -633,7 +633,11 @@ function textLayout(s, g, gd, legendObj, aTitle) { function computeTextDimensions(g, gd, legendObj, aTitle) { var legendItem = g.data()[0][0]; - if(!legendObj._inHover && legendItem && !legendItem.trace.showlegend) { + var showlegend = legendItem && legendItem.trace.showlegend; + if (Array.isArray(showlegend)) { + showlegend = showlegend[legendItem.i] !== false; + } + if(!legendObj._inHover && legendItem && !showlegend) { g.remove(); return; } diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index 4bb7ec60f27..c389ad4d726 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -47,9 +47,17 @@ module.exports = function getLegendData(calcdata, opts, hasMultipleLegends) { if(!inHover && (!trace.visible || !trace.showlegend)) continue; if(Registry.traceIs(trace, 'pie-like')) { + var legendPerSlice = Array.isArray(trace.legend); + var showlegendPerSlice = Array.isArray(trace.showlegend); if(!slicesShown[lgroup]) slicesShown[lgroup] = {}; for(j = 0; j < cd.length; j++) { + if (showlegendPerSlice && trace.showlegend[cd[j].i] === false) { + continue; + } + if (legendPerSlice) { + lid = trace.legend[cd[j].i] || 'legend'; + } var labelj = cd[j].label; if(!slicesShown[lgroup][labelj]) { diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 9fa59255bc1..cbfef0147a7 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -86,10 +86,14 @@ exports.valObjectMeta = { boolean: { description: 'A boolean (true/false) value.', requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(v === true || v === false) propOut.set(v); - else propOut.set(dflt); + otherOpts: ['dflt', 'arrayOk'], + coerceFunction: function(v, propOut, dflt, opts) { + const isBoolean = value => value === true || value === false; + if (isBoolean(v) || (opts.arrayOk && Array.isArray(v) && v.length > 0 && v.every(isBoolean))) { + propOut.set(v); + } else { + propOut.set(dflt); + } } }, number: { @@ -225,14 +229,15 @@ exports.valObjectMeta = { '\'geo\', \'geo2\', \'geo3\', ...' ].join(' '), requiredOpts: ['dflt'], - otherOpts: ['regex'], + otherOpts: ['regex', 'arrayOk'], coerceFunction: function(v, propOut, dflt, opts) { var regex = opts.regex || counterRegex(dflt); - if(typeof v === 'string' && regex.test(v)) { + const isSubplotId = value => typeof value === 'string' && regex.test(value); + if (isSubplotId(v) || (opts.arrayOk && isArrayOrTypedArray(v) && v.length > 0 && v.every(isSubplotId))) { propOut.set(v); - return; + } else { + propOut.set(dflt); } - propOut.set(dflt); }, validateFunction: function(v, opts) { var dflt = opts.dflt; diff --git a/src/plots/plots.js b/src/plots/plots.js index 6550eb353a8..e68454a5196 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1254,8 +1254,10 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac _module.attributes.showlegend ? _module.attributes : plots.attributes, 'showlegend' ); - - coerce('legend'); + Lib.coerce(traceIn, traceOut, + _module.attributes.legend ? _module.attributes : plots.attributes, + 'legend' + ); coerce('legendwidth'); coerce('legendgroup'); coerce('legendgrouptitle.text'); diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index a1f5fca864d..7816082dce3 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -180,7 +180,24 @@ module.exports = { editType: 'plot', description: ['Determines whether outside text labels can push the margins.'].join(' ') }, - + showlegend: extendFlat({}, baseAttrs.showlegend, { + arrayOk: true, + description: [ + 'Determines whether or not items corresponding to the pie slices are shown in the', + 'legend. Can be an array if `values` is set. In that case, each entry specifies', + 'appearance in the legend for one slice.' + ].join(' ') + }), + legend: extendFlat({}, baseAttrs.legend, { + arrayOk: true, + description: [ + 'Sets the reference to a legend to show the pie slices in. Can be an array if `values`', + 'is set. In that case, each entry specifies the legend reference for one slice.', + 'References to these legends are *legend*, *legend2*, *legend3*, etc.', + 'Settings for these legends are set in the layout, under', + '`layout.legend`, `layout.legend2`, etc.' + ].join(' ') + }), title: { text: { valType: 'string', diff --git a/test/image/baselines/zz-pie-slice-legend.png b/test/image/baselines/zz-pie-slice-legend.png new file mode 100644 index 00000000000..2f9ae6a1641 Binary files /dev/null and b/test/image/baselines/zz-pie-slice-legend.png differ diff --git a/test/image/baselines/zz-pie-slice-legend2.png b/test/image/baselines/zz-pie-slice-legend2.png new file mode 100644 index 00000000000..89adfd0506d Binary files /dev/null and b/test/image/baselines/zz-pie-slice-legend2.png differ diff --git a/test/image/baselines/zz-pie-slice-legend3.png b/test/image/baselines/zz-pie-slice-legend3.png new file mode 100644 index 00000000000..54680394fd4 Binary files /dev/null and b/test/image/baselines/zz-pie-slice-legend3.png differ diff --git a/test/image/mocks/zz-pie-slice-legend.json b/test/image/mocks/zz-pie-slice-legend.json new file mode 100644 index 00000000000..6a44ff5b65e --- /dev/null +++ b/test/image/mocks/zz-pie-slice-legend.json @@ -0,0 +1,58 @@ +{ + "data": [ + { + "labels": [ + "with", + "pie", + "multiple", + "hidden", + "legends", + "A", + "hidden too" + ], + "values": [3, 4, 2, 7, 1, 5, 8], + "type": "pie", + "showlegend": [true, true, true, false, true, true, false], + "legend": [ + "legend2", + "legend", + "legend2", + "legend4", + "legend3", + "legend", + "legend4" + ] + } + ], + "layout": { + "title": { + "text": "Test array version of 'showlegend' and 'legend'" + }, + "width": 500, + "height": 400, + "legend": { + "title": { + "text": "L1" + }, + "y": 1 + }, + "legend2": { + "title": { + "text": "L2" + }, + "y": 0.7 + }, + "legend3": { + "title": { + "text": "L3" + }, + "y": 0.2 + }, + "legend4": { + "title": { + "text": "L4" + }, + "y": 0 + } + } +} diff --git a/test/image/mocks/zz-pie-slice-legend2.json b/test/image/mocks/zz-pie-slice-legend2.json new file mode 100644 index 00000000000..4dc91f5ff65 --- /dev/null +++ b/test/image/mocks/zz-pie-slice-legend2.json @@ -0,0 +1,57 @@ +{ + "data": [ + { + "labels": [ + "with", + "multiple", + "hidden", + "legends", + "A", + "hidden too", + "pie" + ], + "values": [3, 2, 7, 1, 5, 8, 4], + "type": "pie", + "showlegend": [true, true, false, true, true, false], + "legend": [ + "legend2", + "legend2", + "legend4", + "legend3", + "legend", + "legend4" + ] + } + ], + "layout": { + "title": { + "text": "Test short array version of 'showlegend' and 'legend'" + }, + "width": 500, + "height": 400, + "legend": { + "title": { + "text": "L1" + }, + "y": 1 + }, + "legend2": { + "title": { + "text": "L2" + }, + "y": 0.7 + }, + "legend3": { + "title": { + "text": "L3" + }, + "y": 0.2 + }, + "legend4": { + "title": { + "text": "L4" + }, + "y": 0 + } + } +} diff --git a/test/image/mocks/zz-pie-slice-legend3.json b/test/image/mocks/zz-pie-slice-legend3.json new file mode 100644 index 00000000000..d617f59789e --- /dev/null +++ b/test/image/mocks/zz-pie-slice-legend3.json @@ -0,0 +1,45 @@ +{ + "data": [ + { + "label0": 1, + "type": "pie", + "values": [1, 2, 3, 4], + "showlegend": [true, false, true], + "legend": ["legend", "legend2", "legend3"] + } + ], + "layout": { + "title": { + "text": "Slice 2 is hidden, slice 4 is assigned
default legend and default visibility" + }, + "width": 400, + "height": 400, + "legend": { + "title": { + "text": "legend" + }, + "xanchor": "left", + "yanchor": "top", + "x": -0.2, + "y": -0.1 + }, + "legend2": { + "title": { + "text": "legend2: I should be hidden!" + }, + "xanchor": "left", + "yanchor": "top", + "x": 0.1, + "y": -0.1 + }, + "legend3": { + "title": { + "text": "legend3" + }, + "xanchor": "left", + "yanchor": "top", + "x": 0.4, + "y": -0.1 + } + } +} diff --git a/test/plot-schema.json b/test/plot-schema.json index 53d05bd9ee5..211da680a56 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -448,7 +448,8 @@ "boolean": { "description": "A boolean (true/false) value.", "otherOpts": [ - "dflt" + "dflt", + "arrayOk" ], "requiredOpts": [] }, @@ -549,7 +550,8 @@ "subplotid": { "description": "An id string of a subplot type (given by dflt), optionally followed by an integer >1. e.g. if dflt='geo', we can have 'geo', 'geo2', 'geo3', ...", "otherOpts": [ - "regex" + "regex", + "arrayOk" ], "requiredOpts": [ "dflt" @@ -56612,7 +56614,8 @@ "valType": "string" }, "legend": { - "description": "Sets the reference to a legend to show this trace in. References to these legends are *legend*, *legend2*, *legend3*, etc. Settings for these legends are set in the layout, under `layout.legend`, `layout.legend2`, etc.", + "arrayOk": true, + "description": "Sets the reference to a legend to show the pie slices in. Can be an array if `values` is set. In that case, each entry specifies the legend reference for one slice. References to these legends are *legend*, *legend2*, *legend3*, etc. Settings for these legends are set in the layout, under `layout.legend`, `layout.legend2`, etc.", "dflt": "legend", "editType": "style", "valType": "subplotid" @@ -56728,6 +56731,11 @@ "editType": "style", "valType": "number" }, + "legendsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `legend`.", + "editType": "none", + "valType": "string" + }, "legendwidth": { "description": "Sets the width (in px or fraction) of the legend for this trace.", "editType": "style", @@ -57075,11 +57083,17 @@ "valType": "string" }, "showlegend": { - "description": "Determines whether or not an item corresponding to this trace is shown in the legend.", + "arrayOk": true, + "description": "Determines whether or not items corresponding to the pie slices are shown in the legend. Can be an array if `values` is set. In that case, each entry specifies appearance in the legend for one slice.", "dflt": true, "editType": "style", "valType": "boolean" }, + "showlegendsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `showlegend`.", + "editType": "none", + "valType": "string" + }, "sort": { "description": "Determines whether or not the sectors are reordered from largest to smallest.", "dflt": true,