Skip to content

Commit dbefbad

Browse files
authored
Merge pull request #7813 from plotly/cam/6771/fix-legendgroup-toggling-shapes
fix: Properly include shapes when toggling legend visibility
2 parents e8fb789 + 76296e8 commit dbefbad

4 files changed

Lines changed: 126 additions & 74 deletions

File tree

draftlogs/7813_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Include shapes with `legendgroup` specified when handling legend visibility toggling [[#7813](https://github.com/plotly/plotly.js/pull/7813)]

src/components/legend/handle_click.js

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,36 @@ var SHOWISOLATETIP = true;
2020
exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
2121
var fullLayout = gd._fullLayout;
2222

23-
if(gd._dragged || gd._editing) return;
23+
if (gd._dragged || gd._editing) return;
2424

2525
var legendItem = g.data()[0][0];
26-
if(legendItem.groupTitle && legendItem.noClick) return;
26+
if (legendItem.groupTitle && legendItem.noClick) return;
2727

2828
var groupClick = legendObj.groupclick;
2929

3030
// Show isolate tip on first single click when default behavior is active
31-
if(mode === 'toggle' && legendObj.itemdoubleclick === 'toggleothers' &&
32-
SHOWISOLATETIP && gd.data && gd._context.showTips
31+
if (
32+
mode === 'toggle' &&
33+
legendObj.itemdoubleclick === 'toggleothers' &&
34+
SHOWISOLATETIP &&
35+
gd.data &&
36+
gd._context.showTips
3337
) {
3438
Lib.notifier(Lib._(gd, 'Double-click on legend to isolate one trace'), 'long', gd);
3539
SHOWISOLATETIP = false;
3640
}
3741

3842
var toggleGroup = groupClick === 'togglegroup';
3943

40-
var hiddenSlices = fullLayout.hiddenlabels ?
41-
fullLayout.hiddenlabels.slice() :
42-
[];
44+
var hiddenSlices = fullLayout.hiddenlabels ? fullLayout.hiddenlabels.slice() : [];
4345

4446
var fullData = gd._fullData;
45-
var shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; });
46-
var allLegendItems = fullData.concat(shapesWithLegend);
47+
// legendgroup membership matters even when showlegend is false, so togglegroup reaches hidden group peers.
48+
const shapesInLegend = (fullLayout.shapes || []).filter((d) => d.showlegend || d.legendgroup);
49+
var allLegendItems = fullData.concat(shapesInLegend);
4750

4851
var fullTrace = legendItem.trace;
49-
if(fullTrace._isShape) {
52+
if (fullTrace._isShape) {
5053
fullTrace = fullTrace._fullInput;
5154
}
5255

@@ -61,11 +64,11 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
6164
function insertDataUpdate(traceIndex, value) {
6265
var attrIndex = dataIndices.indexOf(traceIndex);
6366
var valueArray = dataUpdate.visible;
64-
if(!valueArray) {
67+
if (!valueArray) {
6568
valueArray = dataUpdate.visible = [];
6669
}
6770

68-
if(dataIndices.indexOf(traceIndex) === -1) {
71+
if (dataIndices.indexOf(traceIndex) === -1) {
6972
dataIndices.push(traceIndex);
7073
attrIndex = dataIndices.length - 1;
7174
}
@@ -75,7 +78,7 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
7578
return attrIndex;
7679
}
7780

78-
var updatedShapes = (fullLayout.shapes || []).map(function(d) {
81+
var updatedShapes = (fullLayout.shapes || []).map(function (d) {
7982
return d._input;
8083
});
8184

@@ -87,19 +90,19 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
8790
}
8891

8992
function setVisibility(fullTrace, visibility) {
90-
if(legendItem.groupTitle && !toggleGroup) return;
93+
if (legendItem.groupTitle && !toggleGroup) return;
9194

9295
var fullInput = fullTrace._fullInput || fullTrace;
9396
var isShape = fullInput._isShape;
9497
var index = fullInput.index;
95-
if(index === undefined) index = fullInput._index;
98+
if (index === undefined) index = fullInput._index;
9699

97100
// false -> false (not possible since will not be visible in legend)
98101
// true -> legendonly
99102
// legendonly -> true
100103
var nextVisibility = fullInput.visible === false ? false : visibility;
101104

102-
if(isShape) {
105+
if (isShape) {
103106
insertShapesUpdate(index, nextVisibility);
104107
} else {
105108
insertDataUpdate(index, nextVisibility);
@@ -111,37 +114,37 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
111114
var fullInput = fullTrace._fullInput;
112115
var isShape = fullInput && fullInput._isShape;
113116

114-
if(!isShape && Registry.traceIs(fullTrace, 'pie-like')) {
117+
if (!isShape && Registry.traceIs(fullTrace, 'pie-like')) {
115118
var thisLabel = legendItem.label;
116119
var thisLabelIndex = hiddenSlices.indexOf(thisLabel);
117120

118-
if(mode === 'toggle') {
119-
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
121+
if (mode === 'toggle') {
122+
if (thisLabelIndex === -1) hiddenSlices.push(thisLabel);
120123
else hiddenSlices.splice(thisLabelIndex, 1);
121-
} else if(mode === 'toggleothers') {
124+
} else if (mode === 'toggleothers') {
122125
var changed = thisLabelIndex !== -1;
123126
var unhideList = [];
124-
for(i = 0; i < gd.calcdata.length; i++) {
127+
for (i = 0; i < gd.calcdata.length; i++) {
125128
var cdi = gd.calcdata[i];
126-
for(j = 0; j < cdi.length; j++) {
129+
for (j = 0; j < cdi.length; j++) {
127130
var d = cdi[j];
128131
var dLabel = d.label;
129132

130133
// ensure we toggle slices that are in this legend)
131-
if(thisLegend === cdi[0].trace.legend) {
132-
if(thisLabel !== dLabel) {
133-
if(hiddenSlices.indexOf(dLabel) === -1) changed = true;
134+
if (thisLegend === cdi[0].trace.legend) {
135+
if (thisLabel !== dLabel) {
136+
if (hiddenSlices.indexOf(dLabel) === -1) changed = true;
134137
pushUnique(hiddenSlices, dLabel);
135138
unhideList.push(dLabel);
136139
}
137140
}
138141
}
139142
}
140143

141-
if(!changed) {
142-
for(var q = 0; q < unhideList.length; q++) {
144+
if (!changed) {
145+
for (var q = 0; q < unhideList.length; q++) {
143146
var pos = hiddenSlices.indexOf(unhideList[q]);
144-
if(pos !== -1) {
147+
if (pos !== -1) {
145148
hiddenSlices.splice(pos, 1);
146149
}
147150
}
@@ -153,20 +156,20 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
153156
var hasLegendgroup = legendgroup && legendgroup.length;
154157
var traceIndicesInGroup = [];
155158
var tracei;
156-
if(hasLegendgroup) {
157-
for(i = 0; i < allLegendItems.length; i++) {
159+
if (hasLegendgroup) {
160+
for (i = 0; i < allLegendItems.length; i++) {
158161
tracei = allLegendItems[i];
159-
if(!tracei.visible) continue;
160-
if(tracei.legendgroup === legendgroup) {
162+
if (!tracei.visible) continue;
163+
if (tracei.legendgroup === legendgroup) {
161164
traceIndicesInGroup.push(i);
162165
}
163166
}
164167
}
165168

166-
if(mode === 'toggle') {
169+
if (mode === 'toggle') {
167170
var nextVisibility;
168171

169-
switch(fullTrace.visible) {
172+
switch (fullTrace.visible) {
170173
case true:
171174
nextVisibility = 'legendonly';
172175
break;
@@ -178,11 +181,11 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
178181
break;
179182
}
180183

181-
if(hasLegendgroup) {
182-
if(toggleGroup) {
183-
for(i = 0; i < allLegendItems.length; i++) {
184+
if (hasLegendgroup) {
185+
if (toggleGroup) {
186+
for (i = 0; i < allLegendItems.length; i++) {
184187
var item = allLegendItems[i];
185-
if(item.visible !== false && item.legendgroup === legendgroup) {
188+
if (item.visible !== false && item.legendgroup === legendgroup) {
186189
setVisibility(item, nextVisibility);
187190
}
188191
}
@@ -192,58 +195,63 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
192195
} else {
193196
setVisibility(fullTrace, nextVisibility);
194197
}
195-
} else if(mode === 'toggleothers') {
198+
} else if (mode === 'toggleothers') {
196199
// Compute the clicked index. expandedIndex does what we want for expanded traces
197200
// but also culls hidden traces. That means we have some work to do.
198201
var isClicked, isInGroup, notInLegend, otherState, _item;
199202
var isIsolated = true;
200-
for(i = 0; i < allLegendItems.length; i++) {
203+
for (i = 0; i < allLegendItems.length; i++) {
201204
_item = allLegendItems[i];
202205
isClicked = _item === fullTrace;
203206
notInLegend = _item.showlegend !== true;
204-
if(isClicked || notInLegend) continue;
207+
if (isClicked || notInLegend) continue;
205208

206-
isInGroup = (hasLegendgroup && _item.legendgroup === legendgroup);
209+
isInGroup = hasLegendgroup && _item.legendgroup === legendgroup;
207210

208-
if(!isInGroup && _item.legend === thisLegend && _item.visible === true && !Registry.traceIs(_item, 'notLegendIsolatable')) {
211+
if (
212+
!isInGroup &&
213+
_item.legend === thisLegend &&
214+
_item.visible === true &&
215+
!Registry.traceIs(_item, 'notLegendIsolatable')
216+
) {
209217
isIsolated = false;
210218
break;
211219
}
212220
}
213221

214-
for(i = 0; i < allLegendItems.length; i++) {
222+
for (i = 0; i < allLegendItems.length; i++) {
215223
_item = allLegendItems[i];
216224

217225
// False is sticky; we don't change it. Also ensure we don't change states of itmes in other legend
218-
if(_item.visible === false || _item.legend !== thisLegend) continue;
226+
if (_item.visible === false || _item.legend !== thisLegend) continue;
219227

220-
if(Registry.traceIs(_item, 'notLegendIsolatable')) {
228+
if (Registry.traceIs(_item, 'notLegendIsolatable')) {
221229
continue;
222230
}
223231

224-
switch(fullTrace.visible) {
232+
switch (fullTrace.visible) {
225233
case 'legendonly':
226234
setVisibility(_item, true);
227235
break;
228236
case true:
229237
otherState = isIsolated ? true : 'legendonly';
230238
isClicked = _item === fullTrace;
231239
// N.B. consider traces that have a set legendgroup as toggleable
232-
notInLegend = (_item.showlegend !== true && !_item.legendgroup);
240+
notInLegend = _item.showlegend !== true && !_item.legendgroup;
233241
isInGroup = isClicked || (hasLegendgroup && _item.legendgroup === legendgroup);
234-
setVisibility(_item, (isInGroup || notInLegend) ? true : otherState);
242+
setVisibility(_item, isInGroup || notInLegend ? true : otherState);
235243
break;
236244
}
237245
}
238246
}
239247

240-
for(i = 0; i < carrs.length; i++) {
248+
for (i = 0; i < carrs.length; i++) {
241249
kcont = carrs[i];
242-
if(!kcont) continue;
250+
if (!kcont) continue;
243251
var update = kcont.constructUpdate();
244252

245253
var updateKeys = Object.keys(update);
246-
for(j = 0; j < updateKeys.length; j++) {
254+
for (j = 0; j < updateKeys.length; j++) {
247255
key = updateKeys[j];
248256
val = dataUpdate[key] = dataUpdate[key] || [];
249257
val[carrIdx[i]] = update[key];
@@ -255,18 +263,18 @@ exports.handleItemClick = function handleItemClick(g, gd, legendObj, mode) {
255263
// as updates and not accidentally reset to the default value. This fills
256264
// out sparse arrays with the required number of undefined values:
257265
keys = Object.keys(dataUpdate);
258-
for(i = 0; i < keys.length; i++) {
266+
for (i = 0; i < keys.length; i++) {
259267
key = keys[i];
260-
for(j = 0; j < dataIndices.length; j++) {
268+
for (j = 0; j < dataIndices.length; j++) {
261269
// Use hasOwnProperty to protect against falsy values:
262-
if(!dataUpdate[key].hasOwnProperty(j)) {
270+
if (!dataUpdate[key].hasOwnProperty(j)) {
263271
dataUpdate[key][j] = undefined;
264272
}
265273
}
266274
}
267275

268-
if(shapesUpdated) {
269-
Registry.call('_guiUpdate', gd, dataUpdate, {shapes: updatedShapes}, dataIndices);
276+
if (shapesUpdated) {
277+
Registry.call('_guiUpdate', gd, dataUpdate, { shapes: updatedShapes }, dataIndices);
270278
} else {
271279
Registry.call('_guiRestyle', gd, dataUpdate, dataIndices);
272280
}
@@ -286,8 +294,8 @@ exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) {
286294
const fullLayout = gd._fullLayout;
287295
const fullData = gd._fullData;
288296
const legendId = helpers.getId(legendObj);
289-
const shapesWithLegend = (fullLayout.shapes || []).filter(function(d) { return d.showlegend; });
290-
const allLegendItems = fullData.concat(shapesWithLegend);
297+
const shapesInLegend = (fullLayout.shapes || []).filter((d) => d.showlegend || d.legendgroup);
298+
const allLegendItems = fullData.concat(shapesInLegend);
291299

292300
function isInLegend(item) {
293301
return (item.legend || 'legend') === legendId;
@@ -296,17 +304,17 @@ exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) {
296304
var toggleThisLegend;
297305
var toggleOtherLegends;
298306

299-
if(mode === 'toggle') {
307+
if (mode === 'toggle') {
300308
// If any item is visible in this legend, hide all. If all are hidden, show all
301-
const anyVisibleHere = allLegendItems.some(function(item) {
309+
const anyVisibleHere = allLegendItems.some(function (item) {
302310
return isInLegend(item) && item.visible === true;
303311
});
304312

305313
toggleThisLegend = !anyVisibleHere;
306314
toggleOtherLegends = false;
307315
} else {
308316
// isolate this legend or set all legends to visible
309-
const anyVisibleElsewhere = allLegendItems.some(function(item) {
317+
const anyVisibleElsewhere = allLegendItems.some(function (item) {
310318
return !isInLegend(item) && item.visible === true && item.showlegend !== false;
311319
});
312320

@@ -316,26 +324,28 @@ exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) {
316324

317325
const dataUpdate = { visible: [] };
318326
const dataIndices = [];
319-
const updatedShapes = (fullLayout.shapes || []).map(function(d) { return d._input; });
327+
const updatedShapes = (fullLayout.shapes || []).map(function (d) {
328+
return d._input;
329+
});
320330
var shapesUpdated = false;
321331

322-
for(var i = 0; i < allLegendItems.length; i++) {
332+
for (var i = 0; i < allLegendItems.length; i++) {
323333
const item = allLegendItems[i];
324334
const inThisLegend = isInLegend(item);
325335

326336
// If item is not in this legend, skip if in toggle mode
327337
// or if item is not displayed in the legend
328-
if(!inThisLegend) {
329-
const notDisplayed = (item.showlegend !== true && !item.legendgroup);
330-
if(mode === 'toggle' || notDisplayed) continue;
338+
if (!inThisLegend) {
339+
const notDisplayed = item.showlegend !== true && !item.legendgroup;
340+
if (mode === 'toggle' || notDisplayed) continue;
331341
}
332342

333343
const shouldShow = inThisLegend ? toggleThisLegend : toggleOtherLegends;
334344
const newVis = shouldShow ? true : 'legendonly';
335345

336346
// Only update if visibility would actually change
337-
if((item.visible !== false) && (item.visible !== newVis)) {
338-
if(item._isShape) {
347+
if (item.visible !== false && item.visible !== newVis) {
348+
if (item._isShape) {
339349
updatedShapes[item._index].visible = newVis;
340350
shapesUpdated = true;
341351
} else {
@@ -345,9 +355,9 @@ exports.handleTitleClick = function handleTitleClick(gd, legendObj, mode) {
345355
}
346356
}
347357

348-
if(shapesUpdated) {
349-
Registry.call('_guiUpdate', gd, dataUpdate, {shapes: updatedShapes}, dataIndices);
350-
} else if(dataIndices.length) {
358+
if (shapesUpdated) {
359+
Registry.call('_guiUpdate', gd, dataUpdate, { shapes: updatedShapes }, dataIndices);
360+
} else if (dataIndices.length) {
351361
Registry.call('_guiRestyle', gd, dataUpdate, dataIndices);
352362
}
353363
};

src/components/shapes/defaults.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {
3838
if (!visible) return;
3939

4040
var showlegend = coerce('showlegend');
41+
// Coerce legend/legendgroup even when showlegend is false so hidden group members still toggle with the group.
42+
coerce('legend');
43+
coerce('legendgroup');
4144
if (showlegend) {
42-
coerce('legend');
4345
coerce('legendwidth');
44-
coerce('legendgroup');
4546
coerce('legendgrouptitle.text');
4647
Lib.coerceFont(coerce, 'legendgrouptitle.font');
4748
coerce('legendrank');

0 commit comments

Comments
 (0)