Skip to content

Commit 16d5d22

Browse files
authored
Merge pull request #6653 from plotly/shape-legends
add options to include shapes and `newshape` in legends
2 parents 9c9d5d0 + 9123425 commit 16d5d22

25 files changed

+860
-252
lines changed

Diff for: draftlogs/6653_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- add options to include shapes and `newshape` in legends [[#6653](https://github.com/plotly/plotly.js/pull/6653)]

Diff for: src/components/colorbar/draw.js

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function makeColorBarData(gd) {
117117
for(var i = 0; i < calcdata.length; i++) {
118118
var cd = calcdata[i];
119119
trace = cd[0].trace;
120+
if(!trace._module) continue;
120121
var moduleOpts = trace._module.colorbar;
121122

122123
if(trace.visible === true && moduleOpts) {

Diff for: src/components/legend/defaults.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
4141
var legendReallyHasATrace = false;
4242
var defaultOrder = 'normal';
4343

44-
var allLegendItems = fullData.filter(function(d) {
44+
var shapesWithLegend = (layoutOut.shapes || []).filter(function(d) { return d.showlegend; });
45+
46+
var allLegendItems = fullData.concat(shapesWithLegend).filter(function(d) {
4547
return legendId === (d.legend || 'legend');
4648
});
4749

@@ -50,6 +52,8 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
5052

5153
if(!trace.visible) continue;
5254

55+
var isShape = trace._isShape;
56+
5357
// Note that we explicitly count any trace that is either shown or
5458
// *would* be shown by default, toward the two traces you need to
5559
// ensure the legend is shown by default, because this can still help
@@ -67,7 +71,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
6771
legendReallyHasATrace = true;
6872
// Always show the legend by default if there's a pie,
6973
// or if there's only one trace but it's explicitly shown
70-
if(Registry.traceIs(trace, 'pie-like') ||
74+
if(!isShape && Registry.traceIs(trace, 'pie-like') ||
7175
trace._input.showlegend === true
7276
) {
7377
legendTraceCount++;
@@ -77,7 +81,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
7781
Lib.coerceFont(traceCoerce, 'legendgrouptitle.font', grouptitlefont);
7882
}
7983

80-
if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
84+
if((!isShape && Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
8185
['tonextx', 'tonexty'].indexOf(trace.fill) !== -1) {
8286
defaultOrder = helpers.isGrouped({traceorder: defaultOrder}) ?
8387
'grouped+reversed' : 'reversed';
@@ -199,17 +203,37 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
199203

200204
module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
201205
var i;
202-
var legends = ['legend'];
203206

204-
for(i = 0; i < fullData.length; i++) {
205-
Lib.pushUnique(legends, fullData[i].legend);
207+
var allLegendsData = fullData.slice();
208+
209+
// shapes could also show up in legends
210+
var shapes = layoutOut.shapes;
211+
if(shapes) {
212+
for(i = 0; i < shapes.length; i++) {
213+
var shape = shapes[i];
214+
if(!shape.showlegend) continue;
215+
216+
var mockTrace = {
217+
_input: shape._input,
218+
visible: shape.visible,
219+
showlegend: shape.showlegend,
220+
legend: shape.legend
221+
};
222+
223+
allLegendsData.push(mockTrace);
224+
}
225+
}
226+
227+
var legends = ['legend'];
228+
for(i = 0; i < allLegendsData.length; i++) {
229+
Lib.pushUnique(legends, allLegendsData[i].legend);
206230
}
207231

208232
layoutOut._legends = [];
209233
for(i = 0; i < legends.length; i++) {
210234
var legendId = legends[i];
211235

212-
groupDefaults(legendId, layoutIn, layoutOut, fullData);
236+
groupDefaults(legendId, layoutIn, layoutOut, allLegendsData);
213237

214238
if(
215239
layoutOut[legendId] &&

Diff for: src/components/legend/draw.js

+45-5
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,44 @@ function drawOne(gd, opts) {
7777

7878
var legendData;
7979
if(!inHover) {
80-
if(!gd.calcdata) return;
81-
legendData = fullLayout.showlegend && getLegendData(gd.calcdata, legendObj, fullLayout._legends.length > 1);
80+
var calcdata = (gd.calcdata || []).slice();
81+
82+
var shapes = fullLayout.shapes;
83+
for(var i = 0; i < shapes.length; i++) {
84+
var shape = shapes[i];
85+
if(!shape.showlegend) continue;
86+
87+
var shapeLegend = {
88+
_isShape: true,
89+
_fullInput: shape,
90+
index: shape._index,
91+
name: shape.name || shape.label.text || ('shape ' + shape._index),
92+
legend: shape.legend,
93+
legendgroup: shape.legendgroup,
94+
legendgrouptitle: shape.legendgrouptitle,
95+
legendrank: shape.legendrank,
96+
legendwidth: shape.legendwidth,
97+
showlegend: shape.showlegend,
98+
visible: shape.visible,
99+
opacity: shape.opacity,
100+
mode: shape.type === 'line' ? 'lines' : 'markers',
101+
line: shape.line,
102+
marker: {
103+
line: shape.line,
104+
color: shape.fillcolor,
105+
size: 12,
106+
symbol:
107+
shape.type === 'rect' ? 'square' :
108+
shape.type === 'circle' ? 'circle' :
109+
// case of path
110+
'hexagon2'
111+
},
112+
};
113+
114+
calcdata.push([{ trace: shapeLegend }]);
115+
}
116+
if(!calcdata.length) return;
117+
legendData = fullLayout.showlegend && getLegendData(calcdata, legendObj, fullLayout._legends.length > 1);
82118
} else {
83119
if(!legendObj.entries) return;
84120
legendData = getLegendData(legendObj.entries, legendObj);
@@ -491,9 +527,9 @@ function drawTexts(g, gd, legendObj) {
491527

492528
if(Registry.hasTransform(fullInput, 'groupby')) {
493529
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
494-
var index = groupbyIndices[groupbyIndices.length - 1];
530+
var _index = groupbyIndices[groupbyIndices.length - 1];
495531

496-
var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
532+
var kcont = Lib.keyedContainer(fullInput, 'transforms[' + _index + '].styles', 'target', 'value.name');
497533

498534
kcont.set(legendItem.trace._group, newName);
499535

@@ -502,7 +538,11 @@ function drawTexts(g, gd, legendObj) {
502538
update.name = newName;
503539
}
504540

505-
return Registry.call('_guiRestyle', gd, update, trace.index);
541+
if(fullInput._isShape) {
542+
return Registry.call('_guiRelayout', gd, 'shapes[' + trace.index + '].name', update.name);
543+
} else {
544+
return Registry.call('_guiRestyle', gd, update, trace.index);
545+
}
506546
});
507547
} else {
508548
textLayout(textEl, g, gd, legendObj);

Diff for: src/components/legend/handle_click.js

+77-40
Original file line numberDiff line numberDiff line change
@@ -39,43 +39,65 @@ module.exports = function handleClick(g, gd, numClicks) {
3939
if(legendItem.groupTitle && legendItem.noClick) return;
4040

4141
var fullData = gd._fullData;
42+
var shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; });
43+
var allLegendItems = fullData.concat(shapesWithLegend);
44+
4245
var fullTrace = legendItem.trace;
46+
if(fullTrace._isShape) {
47+
fullTrace = fullTrace._fullInput;
48+
}
49+
4350
var legendgroup = fullTrace.legendgroup;
4451

4552
var i, j, kcont, key, keys, val;
46-
var attrUpdate = {};
47-
var attrIndices = [];
53+
var dataUpdate = {};
54+
var dataIndices = [];
4855
var carrs = [];
4956
var carrIdx = [];
5057

51-
function insertUpdate(traceIndex, key, value) {
52-
var attrIndex = attrIndices.indexOf(traceIndex);
53-
var valueArray = attrUpdate[key];
58+
function insertDataUpdate(traceIndex, value) {
59+
var attrIndex = dataIndices.indexOf(traceIndex);
60+
var valueArray = dataUpdate.visible;
5461
if(!valueArray) {
55-
valueArray = attrUpdate[key] = [];
62+
valueArray = dataUpdate.visible = [];
5663
}
5764

58-
if(attrIndices.indexOf(traceIndex) === -1) {
59-
attrIndices.push(traceIndex);
60-
attrIndex = attrIndices.length - 1;
65+
if(dataIndices.indexOf(traceIndex) === -1) {
66+
dataIndices.push(traceIndex);
67+
attrIndex = dataIndices.length - 1;
6168
}
6269

6370
valueArray[attrIndex] = value;
6471

6572
return attrIndex;
6673
}
6774

75+
var updatedShapes = (fullLayout.shapes || []).map(function(d) {
76+
return d._input;
77+
});
78+
79+
var shapesUpdated = false;
80+
81+
function insertShapesUpdate(shapeIndex, value) {
82+
updatedShapes[shapeIndex].visible = value;
83+
shapesUpdated = true;
84+
}
85+
6886
function setVisibility(fullTrace, visibility) {
6987
if(legendItem.groupTitle && !toggleGroup) return;
7088

71-
var fullInput = fullTrace._fullInput;
89+
var fullInput = fullTrace._fullInput || fullTrace;
90+
var isShape = fullInput._isShape;
91+
var index = fullInput.index;
92+
if(index === undefined) index = fullInput._index;
93+
7294
if(Registry.hasTransform(fullInput, 'groupby')) {
73-
var kcont = carrs[fullInput.index];
95+
var kcont = carrs[index];
7496
if(!kcont) {
7597
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
7698
var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1];
7799
kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible');
78-
carrs[fullInput.index] = kcont;
100+
carrs[index] = kcont;
79101
}
80102

81103
var curState = kcont.get(fullTrace._group);
@@ -93,20 +115,27 @@ module.exports = function handleClick(g, gd, numClicks) {
93115
// true -> legendonly. All others toggle to true:
94116
kcont.set(fullTrace._group, visibility);
95117
}
96-
carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true);
118+
carrIdx[index] = insertDataUpdate(index, fullInput.visible === false ? false : true);
97119
} else {
98120
// false -> false (not possible since will not be visible in legend)
99121
// true -> legendonly
100122
// legendonly -> true
101123
var nextVisibility = fullInput.visible === false ? false : visibility;
102124

103-
insertUpdate(fullInput.index, 'visible', nextVisibility);
125+
if(isShape) {
126+
insertShapesUpdate(index, nextVisibility);
127+
} else {
128+
insertDataUpdate(index, nextVisibility);
129+
}
104130
}
105131
}
106132

107133
var thisLegend = fullTrace.legend;
108134

109-
if(Registry.traceIs(fullTrace, 'pie-like')) {
135+
var fullInput = fullTrace._fullInput;
136+
var isShape = fullInput && fullInput._isShape;
137+
138+
if(!isShape && Registry.traceIs(fullTrace, 'pie-like')) {
110139
var thisLabel = legendItem.label;
111140
var thisLabelIndex = hiddenSlices.indexOf(thisLabel);
112141

@@ -149,8 +178,8 @@ module.exports = function handleClick(g, gd, numClicks) {
149178
var traceIndicesInGroup = [];
150179
var tracei;
151180
if(hasLegendgroup) {
152-
for(i = 0; i < fullData.length; i++) {
153-
tracei = fullData[i];
181+
for(i = 0; i < allLegendItems.length; i++) {
182+
tracei = allLegendItems[i];
154183
if(!tracei.visible) continue;
155184
if(tracei.legendgroup === legendgroup) {
156185
traceIndicesInGroup.push(i);
@@ -175,9 +204,10 @@ module.exports = function handleClick(g, gd, numClicks) {
175204

176205
if(hasLegendgroup) {
177206
if(toggleGroup) {
178-
for(i = 0; i < fullData.length; i++) {
179-
if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) {
180-
setVisibility(fullData[i], nextVisibility);
207+
for(i = 0; i < allLegendItems.length; i++) {
208+
var item = allLegendItems[i];
209+
if(item.visible !== false && item.legendgroup === legendgroup) {
210+
setVisibility(item, nextVisibility);
181211
}
182212
}
183213
} else {
@@ -189,40 +219,43 @@ module.exports = function handleClick(g, gd, numClicks) {
189219
} else if(mode === 'toggleothers') {
190220
// Compute the clicked index. expandedIndex does what we want for expanded traces
191221
// but also culls hidden traces. That means we have some work to do.
192-
var isClicked, isInGroup, notInLegend, otherState;
222+
var isClicked, isInGroup, notInLegend, otherState, _item;
193223
var isIsolated = true;
194-
for(i = 0; i < fullData.length; i++) {
195-
isClicked = fullData[i] === fullTrace;
196-
notInLegend = fullData[i].showlegend !== true;
224+
for(i = 0; i < allLegendItems.length; i++) {
225+
_item = allLegendItems[i];
226+
isClicked = _item === fullTrace;
227+
notInLegend = _item.showlegend !== true;
197228
if(isClicked || notInLegend) continue;
198229

199-
isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup);
230+
isInGroup = (hasLegendgroup && _item.legendgroup === legendgroup);
200231

201-
if(fullData[i].legend === thisLegend && !isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
232+
if(!isInGroup && _item.legend === thisLegend && _item.visible === true && !Registry.traceIs(_item, 'notLegendIsolatable')) {
202233
isIsolated = false;
203234
break;
204235
}
205236
}
206237

207-
for(i = 0; i < fullData.length; i++) {
238+
for(i = 0; i < allLegendItems.length; i++) {
239+
_item = allLegendItems[i];
240+
208241
// False is sticky; we don't change it. Also ensure we don't change states of itmes in other legend
209-
if(fullData[i].visible === false || fullData[i].legend !== thisLegend) continue;
242+
if(_item.visible === false || _item.legend !== thisLegend) continue;
210243

211-
if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
244+
if(Registry.traceIs(_item, 'notLegendIsolatable')) {
212245
continue;
213246
}
214247

215248
switch(fullTrace.visible) {
216249
case 'legendonly':
217-
setVisibility(fullData[i], true);
250+
setVisibility(_item, true);
218251
break;
219252
case true:
220253
otherState = isIsolated ? true : 'legendonly';
221-
isClicked = fullData[i] === fullTrace;
254+
isClicked = _item === fullTrace;
222255
// N.B. consider traces that have a set legendgroup as toggleable
223-
notInLegend = (fullData[i].showlegend !== true && !fullData[i].legendgroup);
224-
isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup);
225-
setVisibility(fullData[i], (isInGroup || notInLegend) ? true : otherState);
256+
notInLegend = (_item.showlegend !== true && !_item.legendgroup);
257+
isInGroup = isClicked || (hasLegendgroup && _item.legendgroup === legendgroup);
258+
setVisibility(_item, (isInGroup || notInLegend) ? true : otherState);
226259
break;
227260
}
228261
}
@@ -236,7 +269,7 @@ module.exports = function handleClick(g, gd, numClicks) {
236269
var updateKeys = Object.keys(update);
237270
for(j = 0; j < updateKeys.length; j++) {
238271
key = updateKeys[j];
239-
val = attrUpdate[key] = attrUpdate[key] || [];
272+
val = dataUpdate[key] = dataUpdate[key] || [];
240273
val[carrIdx[i]] = update[key];
241274
}
242275
}
@@ -245,17 +278,21 @@ module.exports = function handleClick(g, gd, numClicks) {
245278
// values should be explicitly undefined for them to get properly culled
246279
// as updates and not accidentally reset to the default value. This fills
247280
// out sparse arrays with the required number of undefined values:
248-
keys = Object.keys(attrUpdate);
281+
keys = Object.keys(dataUpdate);
249282
for(i = 0; i < keys.length; i++) {
250283
key = keys[i];
251-
for(j = 0; j < attrIndices.length; j++) {
284+
for(j = 0; j < dataIndices.length; j++) {
252285
// Use hasOwnProperty to protect against falsy values:
253-
if(!attrUpdate[key].hasOwnProperty(j)) {
254-
attrUpdate[key][j] = undefined;
286+
if(!dataUpdate[key].hasOwnProperty(j)) {
287+
dataUpdate[key][j] = undefined;
255288
}
256289
}
257290
}
258291

259-
Registry.call('_guiRestyle', gd, attrUpdate, attrIndices);
292+
if(shapesUpdated) {
293+
Registry.call('_guiUpdate', gd, dataUpdate, {shapes: updatedShapes}, dataIndices);
294+
} else {
295+
Registry.call('_guiRestyle', gd, dataUpdate, dataIndices);
296+
}
260297
}
261298
};

0 commit comments

Comments
 (0)