Skip to content

Commit d78b965

Browse files
authored
Merge pull request #2022 from plotly/legend-visibility-toggle-rewrite
Rewrite legend visibility toggling
2 parents cb5a465 + d23c24d commit d78b965

File tree

3 files changed

+441
-116
lines changed

3 files changed

+441
-116
lines changed

src/components/legend/draw.js

+7-98
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var d3 = require('d3');
@@ -19,6 +18,7 @@ var dragElement = require('../dragelement');
1918
var Drawing = require('../drawing');
2019
var Color = require('../color');
2120
var svgTextUtils = require('../../lib/svg_text_utils');
21+
var handleClick = require('./handle_click');
2222

2323
var constants = require('./constants');
2424
var interactConstants = require('../../constants/interactions');
@@ -29,7 +29,6 @@ var style = require('./style');
2929
var helpers = require('./helpers');
3030
var anchorUtils = require('./anchor_utils');
3131

32-
var SHOWISOLATETIP = true;
3332
var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY;
3433

3534
module.exports = function draw(gd) {
@@ -413,23 +412,24 @@ function drawTexts(g, gd) {
413412
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
414413
var index = groupbyIndices[groupbyIndices.length - 1];
415414

416-
var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
415+
var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
417416

418417
if(origText === '') {
419-
carr.remove(legendItem.trace._group);
418+
kcont.remove(legendItem.trace._group);
420419
} else {
421-
carr.set(legendItem.trace._group, text);
420+
kcont.set(legendItem.trace._group, text);
422421
}
423422

424-
update = carr.constructUpdate();
423+
update = kcont.constructUpdate();
425424
} else {
426425
update.name = text;
427426
}
428427

429428
return Plotly.restyle(gd, update, traceIndex);
430429
});
430+
} else {
431+
text.call(textLayout);
431432
}
432-
else text.call(textLayout);
433433
}
434434

435435
function setupTraceToggle(g, gd) {
@@ -478,97 +478,6 @@ function setupTraceToggle(g, gd) {
478478
});
479479
}
480480

481-
function handleClick(g, gd, numClicks) {
482-
if(gd._dragged || gd._editing) return;
483-
var hiddenSlices = gd._fullLayout.hiddenlabels ?
484-
gd._fullLayout.hiddenlabels.slice() :
485-
[];
486-
487-
var legendItem = g.data()[0][0],
488-
fullData = gd._fullData,
489-
trace = legendItem.trace,
490-
legendgroup = trace.legendgroup,
491-
traceIndicesInGroup = [],
492-
tracei,
493-
newVisible;
494-
495-
496-
if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) {
497-
Lib.notifier('Double click on legend to isolate individual trace', 'long');
498-
SHOWISOLATETIP = false;
499-
} else {
500-
SHOWISOLATETIP = false;
501-
}
502-
if(Registry.traceIs(trace, 'pie')) {
503-
var thisLabel = legendItem.label,
504-
thisLabelIndex = hiddenSlices.indexOf(thisLabel);
505-
506-
if(numClicks === 1) {
507-
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
508-
else hiddenSlices.splice(thisLabelIndex, 1);
509-
} else if(numClicks === 2) {
510-
hiddenSlices = [];
511-
gd.calcdata[0].forEach(function(d) {
512-
if(thisLabel !== d.label) {
513-
hiddenSlices.push(d.label);
514-
}
515-
});
516-
if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) {
517-
hiddenSlices = [];
518-
}
519-
}
520-
521-
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices);
522-
} else {
523-
var allTraces = [],
524-
traceVisibility = [],
525-
i;
526-
527-
for(i = 0; i < fullData.length; i++) {
528-
allTraces.push(i);
529-
// Allow the legendonly state through for *all* trace types (including
530-
// carpet for which it's overridden with true/false in supplyDefaults)
531-
traceVisibility.push(
532-
Registry.traceIs(fullData[i], 'notLegendIsolatable') ? true : 'legendonly'
533-
);
534-
}
535-
536-
if(legendgroup === '') {
537-
traceIndicesInGroup = [trace.index];
538-
traceVisibility[trace.index] = true;
539-
} else {
540-
for(i = 0; i < fullData.length; i++) {
541-
tracei = fullData[i];
542-
if(tracei.legendgroup === legendgroup) {
543-
traceIndicesInGroup.push(tracei.index);
544-
traceVisibility[allTraces.indexOf(i)] = true;
545-
}
546-
}
547-
}
548-
549-
if(numClicks === 1) {
550-
newVisible = trace.visible === true ? 'legendonly' : true;
551-
Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup);
552-
} else if(numClicks === 2) {
553-
var sameAsLast = true;
554-
for(i = 0; i < fullData.length; i++) {
555-
if(fullData[i].visible !== traceVisibility[i]) {
556-
sameAsLast = false;
557-
break;
558-
}
559-
}
560-
if(sameAsLast) {
561-
traceVisibility = true;
562-
}
563-
var visibilityUpdates = [];
564-
for(i = 0; i < fullData.length; i++) {
565-
visibilityUpdates.push(allTraces[i]);
566-
}
567-
Plotly.restyle(gd, 'visible', traceVisibility, visibilityUpdates);
568-
}
569-
}
570-
}
571-
572481
function computeTextDimensions(g, gd) {
573482
var legendItem = g.data()[0][0];
574483

src/components/legend/handle_click.js

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Plotly = require('../../plotly');
12+
var Lib = require('../../lib');
13+
var Registry = require('../../registry');
14+
15+
var SHOWISOLATETIP = true;
16+
17+
module.exports = function handleClick(g, gd, numClicks) {
18+
if(gd._dragged || gd._editing) return;
19+
20+
var hiddenSlices = gd._fullLayout.hiddenlabels ?
21+
gd._fullLayout.hiddenlabels.slice() :
22+
[];
23+
24+
var legendItem = g.data()[0][0];
25+
var fullData = gd._fullData;
26+
var fullTrace = legendItem.trace;
27+
var legendgroup = fullTrace.legendgroup;
28+
29+
var i, j, kcont, key, keys, val;
30+
var attrUpdate = {};
31+
var attrIndices = [];
32+
var carrs = [];
33+
var carrIdx = [];
34+
35+
function insertUpdate(traceIndex, key, value) {
36+
var attrIndex = attrIndices.indexOf(traceIndex);
37+
var valueArray = attrUpdate[key];
38+
if(!valueArray) {
39+
valueArray = attrUpdate[key] = [];
40+
}
41+
42+
if(attrIndices.indexOf(traceIndex) === -1) {
43+
attrIndices.push(traceIndex);
44+
attrIndex = attrIndices.length - 1;
45+
}
46+
47+
valueArray[attrIndex] = value;
48+
49+
return attrIndex;
50+
}
51+
52+
function setVisibility(fullTrace, visibility) {
53+
var fullInput = fullTrace._fullInput;
54+
if(Registry.hasTransform(fullInput, 'groupby')) {
55+
var kcont = carrs[fullInput.index];
56+
if(!kcont) {
57+
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
58+
var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1];
59+
kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible');
60+
carrs[fullInput.index] = kcont;
61+
}
62+
63+
var curState = kcont.get(fullTrace._group);
64+
65+
// If not specified, assume visible. This happens if there are other style
66+
// properties set for a group but not the visibility. There are many similar
67+
// ways to do this (e.g. why not just `curState = fullTrace.visible`??? The
68+
// answer is: because it breaks other things like groupby trace names in
69+
// subtle ways.)
70+
if(curState === undefined) {
71+
curState = true;
72+
}
73+
74+
if(curState !== false) {
75+
// true -> legendonly. All others toggle to true:
76+
kcont.set(fullTrace._group, visibility);
77+
}
78+
carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true);
79+
} else {
80+
// false -> false (not possible since will not be visible in legend)
81+
// true -> legendonly
82+
// legendonly -> true
83+
var nextVisibility = fullInput.visible === false ? false : visibility;
84+
85+
insertUpdate(fullInput.index, 'visible', nextVisibility);
86+
}
87+
}
88+
89+
if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) {
90+
Lib.notifier('Double click on legend to isolate individual trace', 'long');
91+
SHOWISOLATETIP = false;
92+
} else {
93+
SHOWISOLATETIP = false;
94+
}
95+
96+
if(Registry.traceIs(fullTrace, 'pie')) {
97+
var thisLabel = legendItem.label,
98+
thisLabelIndex = hiddenSlices.indexOf(thisLabel);
99+
100+
if(numClicks === 1) {
101+
if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
102+
else hiddenSlices.splice(thisLabelIndex, 1);
103+
} else if(numClicks === 2) {
104+
hiddenSlices = [];
105+
gd.calcdata[0].forEach(function(d) {
106+
if(thisLabel !== d.label) {
107+
hiddenSlices.push(d.label);
108+
}
109+
});
110+
if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) {
111+
hiddenSlices = [];
112+
}
113+
}
114+
115+
Plotly.relayout(gd, 'hiddenlabels', hiddenSlices);
116+
} else {
117+
var hasLegendgroup = legendgroup && legendgroup.length;
118+
var traceIndicesInGroup = [];
119+
var tracei;
120+
if(hasLegendgroup) {
121+
for(i = 0; i < fullData.length; i++) {
122+
tracei = fullData[i];
123+
if(!tracei.visible) continue;
124+
if(tracei.legendgroup === legendgroup) {
125+
traceIndicesInGroup.push(i);
126+
}
127+
}
128+
}
129+
130+
if(numClicks === 1) {
131+
var nextVisibility;
132+
133+
switch(fullTrace.visible) {
134+
case true:
135+
nextVisibility = 'legendonly';
136+
break;
137+
case false:
138+
nextVisibility = false;
139+
break;
140+
case 'legendonly':
141+
nextVisibility = true;
142+
break;
143+
}
144+
145+
if(hasLegendgroup) {
146+
for(i = 0; i < fullData.length; i++) {
147+
if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) {
148+
setVisibility(fullData[i], nextVisibility);
149+
}
150+
}
151+
} else {
152+
setVisibility(fullTrace, nextVisibility);
153+
}
154+
} else if(numClicks === 2) {
155+
// Compute the clicked index. expandedIndex does what we want for expanded traces
156+
// but also culls hidden traces. That means we have some work to do.
157+
var isClicked, isInGroup, otherState;
158+
var isIsolated = true;
159+
for(i = 0; i < fullData.length; i++) {
160+
isClicked = fullData[i] === fullTrace;
161+
if(isClicked) continue;
162+
163+
isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup);
164+
165+
if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
166+
isIsolated = false;
167+
break;
168+
}
169+
}
170+
171+
for(i = 0; i < fullData.length; i++) {
172+
// False is sticky; we don't change it.
173+
if(fullData[i].visible === false) continue;
174+
175+
if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) {
176+
continue;
177+
}
178+
179+
switch(fullTrace.visible) {
180+
case 'legendonly':
181+
setVisibility(fullData[i], true);
182+
break;
183+
case true:
184+
otherState = isIsolated ? true : 'legendonly';
185+
isClicked = fullData[i] === fullTrace;
186+
isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup);
187+
setVisibility(fullData[i], isInGroup ? true : otherState);
188+
break;
189+
}
190+
}
191+
}
192+
193+
for(i = 0; i < carrs.length; i++) {
194+
kcont = carrs[i];
195+
if(!kcont) continue;
196+
var update = kcont.constructUpdate();
197+
198+
var updateKeys = Object.keys(update);
199+
for(j = 0; j < updateKeys.length; j++) {
200+
key = updateKeys[j];
201+
val = attrUpdate[key] = attrUpdate[key] || [];
202+
val[carrIdx[i]] = update[key];
203+
}
204+
}
205+
206+
// The length of the value arrays should be equal and any unspecified
207+
// values should be explicitly undefined for them to get properly culled
208+
// as updates and not accidentally reset to the default value. This fills
209+
// out sparse arrays with the required number of undefined values:
210+
keys = Object.keys(attrUpdate);
211+
for(i = 0; i < keys.length; i++) {
212+
key = keys[i];
213+
for(j = 0; j < attrIndices.length; j++) {
214+
// Use hasOwnPropety to protect against falsey values:
215+
if(!attrUpdate[key].hasOwnProperty(j)) {
216+
attrUpdate[key][j] = undefined;
217+
}
218+
}
219+
}
220+
221+
Plotly.restyle(gd, attrUpdate, attrIndices);
222+
}
223+
};

0 commit comments

Comments
 (0)