From 5cc7dc69ca15084ab411ba1e0040d1675ec04030 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 19 Sep 2017 15:44:07 -0700 Subject: [PATCH 1/6] Rewrite legend visibility toggling --- src/components/legend/draw.js | 194 ++++++++++++++++++++------ test/jasmine/tests/legend_test.js | 220 +++++++++++++++++++++++++++--- 2 files changed, 356 insertions(+), 58 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 111d5191747..d2196d74874 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -480,18 +480,67 @@ function setupTraceToggle(g, gd) { function handleClick(g, gd, numClicks) { if(gd._dragged || gd._editing) return; + var hiddenSlices = gd._fullLayout.hiddenlabels ? gd._fullLayout.hiddenlabels.slice() : []; - var legendItem = g.data()[0][0], - fullData = gd._fullData, - trace = legendItem.trace, - legendgroup = trace.legendgroup, - traceIndicesInGroup = [], - tracei, - newVisible; + var legendItem = g.data()[0][0]; + var fullData = gd._fullData; + var fullTrace = legendItem.trace; + var legendgroup = fullTrace.legendgroup; + + var i, j, carr, key, keys, val; + var attrUpdate = {}; + var attrIndices = []; + var carrs = []; + var carrIdx = []; + + function insertUpdate(traceIndex, key, value) { + var attrIndex = attrIndices.indexOf(traceIndex); + var valueArray = attrUpdate[key]; + if(!valueArray) { + valueArray = attrUpdate[key] = []; + } + + if(attrIndices.indexOf(traceIndex) === -1) { + attrIndices.push(traceIndex); + attrIndex = attrIndices.length - 1; + } + + valueArray[attrIndex] = value; + + return attrIndex; + } + + function setVisibility(fullTrace, visibility) { + var fullInput = fullTrace._fullInput; + if(Registry.hasTransform(fullInput, 'groupby')) { + var carr = carrs[fullInput.index]; + if(!carr) { + var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); + var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1]; + carr = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible'); + carrs[fullInput.index] = carr; + } + + // If not specified, assume visible: + var curState = carr.get(fullTrace._group) || true; + if(curState !== false) { + // true -> legendonly. All others toggle to true: + carr.set(fullTrace._group, visibility); + } + carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true); + } else { + // false -> false (not possible since will not be visible in legend) + // true -> legendonly + // legendonly -> true + var nextVisibility = fullInput.visible === false ? false : visibility; + + insertUpdate(fullInput.index, 'visible', nextVisibility); + } + } if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) { Lib.notifier('Double click on legend to isolate individual trace', 'long'); @@ -499,7 +548,8 @@ function handleClick(g, gd, numClicks) { } else { SHOWISOLATETIP = false; } - if(Registry.traceIs(trace, 'pie')) { + + if(Registry.traceIs(fullTrace, 'pie')) { var thisLabel = legendItem.label, thisLabelIndex = hiddenSlices.indexOf(thisLabel); @@ -520,52 +570,116 @@ function handleClick(g, gd, numClicks) { Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); } else { - var allTraces = [], - traceVisibility = [], - i; - - for(i = 0; i < fullData.length; i++) { - allTraces.push(i); - // Allow the legendonly state through for *all* trace types (including - // carpet for which it's overridden with true/false in supplyDefaults) - traceVisibility.push( - Registry.traceIs(fullData[i], 'notLegendIsolatable') ? true : 'legendonly' - ); - } - - if(legendgroup === '') { - traceIndicesInGroup = [trace.index]; - traceVisibility[trace.index] = true; - } else { - for(i = 0; i < fullData.length; i++) { + var hasLegendgroup = legendgroup && legendgroup.length; + var traceIndicesInGroup = []; + var tracei; + if (hasLegendgroup) { + for (i = 0; i < fullData.length; i++) { tracei = fullData[i]; - if(tracei.legendgroup === legendgroup) { - traceIndicesInGroup.push(tracei.index); - traceVisibility[allTraces.indexOf(i)] = true; + if (!tracei.visible) continue; + if (tracei.legendgroup === legendgroup) { + traceIndicesInGroup.push(i); } } } if(numClicks === 1) { - newVisible = trace.visible === true ? 'legendonly' : true; - Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); + var nextVisibility; + + switch(fullTrace.visible) { + case true: + nextVisibility = 'legendonly'; + break; + case false: + nextVisibility = false; + break; + default: + case 'legendonly': + nextVisibility = true; + break; + } + + if (hasLegendgroup) { + for (i = 0; i < fullData.length; i++) { + if (fullData[i].visible && fullData[i].legendgroup === legendgroup) { + setVisibility(fullData[i], nextVisibility); + } + } + } else { + setVisibility(fullTrace, nextVisibility); + } } else if(numClicks === 2) { - var sameAsLast = true; + // Compute the clicked index. expandedIndex does what we want for expanded traces + // but also culls hidden traces. That means we have some work to do. + var clickedIndex; + for(clickedIndex = 0; clickedIndex < fullData.length; clickedIndex++) { + if(fullData[clickedIndex] === fullTrace) break; + } + + var isIsolated = true; for(i = 0; i < fullData.length; i++) { - if(fullData[i].visible !== traceVisibility[i]) { - sameAsLast = false; + var isClicked = fullData[i] === fullTrace; + if (isClicked) continue; + + var isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup); + + if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) { + isIsolated = false; break; } } - if(sameAsLast) { - traceVisibility = true; - } - var visibilityUpdates = []; + for(i = 0; i < fullData.length; i++) { - visibilityUpdates.push(allTraces[i]); + // False is sticky; we don't change it. + if(fullData[i].visible === false) continue; + + if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) { + continue; + } + + switch(fullTrace.visible) { + case 'legendonly': + setVisibility(fullData[i], true); + break; + case true: + var otherState = isIsolated ? true : 'legendonly'; + var isClicked = fullData[i] === fullTrace; + var isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup); + setVisibility(fullData[i], isInGroup ? true : otherState); + break; + } } - Plotly.restyle(gd, 'visible', traceVisibility, visibilityUpdates); } + + for(i = 0; i < carrs.length; i++) { + carr = carrs[i]; + if(!carr) continue; + var update = carr.constructUpdate(); + + var updateKeys = Object.keys(update); + for(j = 0; j < updateKeys.length; j++) { + key = updateKeys[j]; + val = attrUpdate[key] = attrUpdate[key] || []; + val[carrIdx[i]] = update[key]; + } + } + + // The length of the value arrays should be equal and any unspecified + // values should be explicitly undefined for them to get properly culled + // as updates and not accidentally reset to the default value. This fills + // out sparse arrays with the required number of undefined values: + keys = Object.keys(attrUpdate); + for(i = 0; i < keys.length; i++) { + key = keys[i]; + for(j = 0; j < attrIndices.length; j++) { + // Use hasOwnPropety to protect against falsey values: + if(!attrUpdate[key].hasOwnProperty(j)) { + attrUpdate[key][j] = undefined; + } + } + } + + Plotly.restyle(gd, attrUpdate, attrIndices); } } diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index e994811d6ae..0073e83fb35 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -833,23 +833,27 @@ describe('legend interaction', function() { afterAll(destroyGraphDiv); function _click(index) { - var item = d3.selectAll('rect.legendtoggle')[0][index || 0]; - return new Promise(function(resolve) { - item.dispatchEvent(new MouseEvent('mousedown')); - item.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(resolve, DBLCLICKDELAY + 20); - }); + return function() { + var item = d3.selectAll('rect.legendtoggle')[0][index || 0]; + return new Promise(function(resolve) { + item.dispatchEvent(new MouseEvent('mousedown')); + item.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(resolve, DBLCLICKDELAY + 20); + }); + }; } function _dblclick(index) { - var item = d3.selectAll('rect.legendtoggle')[0][index || 0]; - return new Promise(function(resolve) { - item.dispatchEvent(new MouseEvent('mousedown')); - item.dispatchEvent(new MouseEvent('mouseup')); - item.dispatchEvent(new MouseEvent('mousedown')); - item.dispatchEvent(new MouseEvent('mouseup')); - setTimeout(resolve, 20); - }); + return function() { + var item = d3.selectAll('rect.legendtoggle')[0][index || 0]; + return new Promise(function(resolve) { + item.dispatchEvent(new MouseEvent('mousedown')); + item.dispatchEvent(new MouseEvent('mouseup')); + item.dispatchEvent(new MouseEvent('mousedown')); + item.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(resolve, 20); + }); + }; } function assertVisible(gd, expectation) { @@ -864,19 +868,19 @@ describe('legend interaction', function() { Plotly.plot(gd, _mock).then(function() { assertVisible(gd, [true, true, true, true]); }) - .then(_click) + .then(_click(0)) .then(function() { assertVisible(gd, [true, 'legendonly', true, true]); }) - .then(_click) + .then(_click(0)) .then(function() { assertVisible(gd, [true, true, true, true]); }) - .then(_dblclick) + .then(_dblclick(0)) .then(function() { assertVisible(gd, [true, true, 'legendonly', 'legendonly']); }) - .then(_dblclick) + .then(_dblclick(0)) .then(function() { assertVisible(gd, [true, true, true, true]); }) @@ -952,4 +956,184 @@ describe('legend interaction', function() { }).catch(fail).then(done); }); }); + + describe('legend visibility interactions', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function click(index, clicks) { + return function() { + return new Promise(function(resolve) { + var item = d3.selectAll('rect.legendtoggle')[0][index || 0]; + for(var i = 0; i < (clicks || 1); i++) { + item.dispatchEvent(new MouseEvent('mousedown')); + item.dispatchEvent(new MouseEvent('mouseup')); + } + setTimeout(resolve, DBLCLICKDELAY + 20); + }); + }; + } + + function assertVisible(expectation) { + return function() { + var actual = gd._fullData.map(function(trace) { return trace.visible; }); + expect(actual).toEqual(expectation); + }; + } + + describe('for regular traces', function() { + beforeEach(function(done) { + Plotly.plot(gd, [ + {x: [1, 2], y: [0, 1], visible: false}, + {x: [1, 2], y: [1, 2], visible: 'legendonly'}, + {x: [1, 2], y: [2, 3]} + ]).then(done); + }); + + it('clicking once toggles legendonly -> true', function(done) { + Promise.resolve() + .then(assertVisible([false, 'legendonly', true])) + .then(click(0)) + .then(assertVisible([false, true, true])) + .catch(fail).then(done); + }); + + it('clicking once toggles true -> legendonly', function(done) { + Promise.resolve() + .then(assertVisible([false, 'legendonly', true])) + .then(click(1)) + .then(assertVisible([false, 'legendonly', 'legendonly'])) + .catch(fail).then(done); + }); + + it('double-clicking isolates a visible trace ', function(done) { + Promise.resolve() + .then(click(0)) + .then(assertVisible([false, true, true])) + .then(click(0, 2)) + .then(assertVisible([false, true, 'legendonly'])) + .catch(fail).then(done); + }); + + it('double-clicking an isolated trace shows all non-hidden traces', function(done) { + Promise.resolve() + .then(click(0, 2)) + .then(assertVisible([false, true, true])) + .catch(fail).then(done); + }); + }); + + describe('legendgroup visibility', function() { + beforeEach(function(done) { + Plotly.plot(gd, [{ + x: [1, 2], + y: [3, 4], + visible: false + }, { + x: [1, 2, 3, 4], + y: [0, 1, 2, 3], + legendgroup: 'foo' + }, { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + }, { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + legendgroup: 'foo' + }]).then(done); + }); + + it('toggles the visibility of legendgroups as a whole', function(done) { + Promise.resolve() + .then(click(1)) + .then(assertVisible([false, 'legendonly', true, 'legendonly'])) + .then(click(1)) + .then(assertVisible([false, true, true, true])) + .catch(fail).then(done); + }); + + it('isolates legendgroups as a whole', function(done) { + Promise.resolve() + .then(click(1, 2)) + .then(assertVisible([false, true, 'legendonly', true])) + .then(click(1, 2)) + .then(assertVisible([false, true, true, true])) + .catch(fail).then(done); + }); + }); + + describe('legend visibility toggles with groupby', function() { + beforeEach(function(done) { + Plotly.plot(gd, [{ + x: [1, 2], + y: [3, 4], + visible: false + }, { + x: [1, 2, 3, 4], + y: [0, 1, 2, 3] + }, { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + transforms: [{ + type: 'groupby', + groups: ['a', 'b', 'c', 'c'] + }] + }, { + x: [1, 2, 3, 4], + y: [1, 3, 2, 4], + transforms: [{ + type: 'groupby', + groups: ['a', 'b', 'c', 'c'] + }] + }]).then(done); + }); + + it('computes the initial visibility correctly', function(done) { + Promise.resolve() + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); + + it('toggles the visibility of a non-groupby trace in the presence of groupby traces', function(done) { + Promise.resolve() + .then(click(1)) + .then(assertVisible([false, true, 'legendonly', true, true, true, true, true])) + .then(click(1)) + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); + + it('toggles the visibility of the first group in a groupby trace', function(done) { + Promise.resolve() + .then(click(0)) + .then(assertVisible([false, 'legendonly', true, true, true, true, true, true])) + .then(click(0)) + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); + + it('toggles the visibility of the third group in a groupby trace', function(done) { + Promise.resolve() + .then(click(3)) + .then(assertVisible([false, true, true, true, 'legendonly', true, true, true])) + .then(click(3)) + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); + + it('double-clicking isolates a non-groupby trace', function(done) { + Promise.resolve() + .then(click(0, 2)) + .then(assertVisible([false, true, 'legendonly', 'legendonly', 'legendonly', 'legendonly', 'legendonly', 'legendonly'])) + .then(click(0, 2)) + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); + }); + }); }); From 6b8dd49a18c85e64e6324f9827eb0d4d0e440bbe Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 19 Sep 2017 16:19:19 -0700 Subject: [PATCH 2/6] Lint-fix --- src/components/legend/draw.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index d2196d74874..f540d97db60 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -573,11 +573,11 @@ function handleClick(g, gd, numClicks) { var hasLegendgroup = legendgroup && legendgroup.length; var traceIndicesInGroup = []; var tracei; - if (hasLegendgroup) { - for (i = 0; i < fullData.length; i++) { + if(hasLegendgroup) { + for(i = 0; i < fullData.length; i++) { tracei = fullData[i]; - if (!tracei.visible) continue; - if (tracei.legendgroup === legendgroup) { + if(!tracei.visible) continue; + if(tracei.legendgroup === legendgroup) { traceIndicesInGroup.push(i); } } @@ -599,9 +599,9 @@ function handleClick(g, gd, numClicks) { break; } - if (hasLegendgroup) { - for (i = 0; i < fullData.length; i++) { - if (fullData[i].visible && fullData[i].legendgroup === legendgroup) { + if(hasLegendgroup) { + for(i = 0; i < fullData.length; i++) { + if(fullData[i].visible && fullData[i].legendgroup === legendgroup) { setVisibility(fullData[i], nextVisibility); } } @@ -611,17 +611,17 @@ function handleClick(g, gd, numClicks) { } else if(numClicks === 2) { // Compute the clicked index. expandedIndex does what we want for expanded traces // but also culls hidden traces. That means we have some work to do. - var clickedIndex; + var clickedIndex, isIsolated, isClicked, isInGroup, otherState; for(clickedIndex = 0; clickedIndex < fullData.length; clickedIndex++) { if(fullData[clickedIndex] === fullTrace) break; } - var isIsolated = true; + isIsolated = true; for(i = 0; i < fullData.length; i++) { - var isClicked = fullData[i] === fullTrace; - if (isClicked) continue; + isClicked = fullData[i] === fullTrace; + if(isClicked) continue; - var isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup); + isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup); if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) { isIsolated = false; @@ -642,9 +642,9 @@ function handleClick(g, gd, numClicks) { setVisibility(fullData[i], true); break; case true: - var otherState = isIsolated ? true : 'legendonly'; - var isClicked = fullData[i] === fullTrace; - var isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup); + otherState = isIsolated ? true : 'legendonly'; + isClicked = fullData[i] === fullTrace; + isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup); setVisibility(fullData[i], isInGroup ? true : otherState); break; } From f17d763edf4d4de82713645e01e0749b63e76a3d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 19 Sep 2017 16:37:36 -0700 Subject: [PATCH 3/6] Add overlooked test --- test/jasmine/tests/legend_test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 0073e83fb35..fd566f9cc91 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1134,6 +1134,15 @@ describe('legend interaction', function() { .then(assertVisible([false, true, true, true, true, true, true, true])) .catch(fail).then(done); }); + + it('double-clicking isolates a groupby trace', function(done) { + Promise.resolve() + .then(click(1, 2)) + .then(assertVisible([false, 'legendonly', true, 'legendonly', 'legendonly', 'legendonly', 'legendonly', 'legendonly'])) + .then(click(1, 2)) + .then(assertVisible([false, true, true, true, true, true, true, true])) + .catch(fail).then(done); + }); }); }); }); From c729353a406d8d9088d5a247a12d504a8f2c70ac Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 20 Sep 2017 10:01:36 -0700 Subject: [PATCH 4/6] Fix small PR review issues for legend interactions --- src/components/legend/draw.js | 41 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index f540d97db60..bc3eb3e2780 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -413,15 +413,15 @@ function drawTexts(g, gd) { var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); var index = groupbyIndices[groupbyIndices.length - 1]; - var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); + var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); if(origText === '') { - carr.remove(legendItem.trace._group); + kcont.remove(legendItem.trace._group); } else { - carr.set(legendItem.trace._group, text); + kcont.set(legendItem.trace._group, text); } - update = carr.constructUpdate(); + update = kcont.constructUpdate(); } else { update.name = text; } @@ -490,7 +490,7 @@ function handleClick(g, gd, numClicks) { var fullTrace = legendItem.trace; var legendgroup = fullTrace.legendgroup; - var i, j, carr, key, keys, val; + var i, j, kcont, key, keys, val; var attrUpdate = {}; var attrIndices = []; var carrs = []; @@ -516,20 +516,28 @@ function handleClick(g, gd, numClicks) { function setVisibility(fullTrace, visibility) { var fullInput = fullTrace._fullInput; if(Registry.hasTransform(fullInput, 'groupby')) { - var carr = carrs[fullInput.index]; - if(!carr) { + var kcont = carrs[fullInput.index]; + if(!kcont) { var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1]; - carr = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible'); - carrs[fullInput.index] = carr; + kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible'); + carrs[fullInput.index] = kcont; } - // If not specified, assume visible: - var curState = carr.get(fullTrace._group) || true; + var curState = kcont.get(fullTrace._group); + + // If not specified, assume visible. This happens if there are other style + // properties set for a group but not the visibility. There are many similar + // ways to do this (e.g. why not just `curState = fullTrace.visible`??? The + // answer is: because it breaks other things like groupby trace names in + // subtle ways.) + if(curState === undefined) { + curState = true; + } if(curState !== false) { // true -> legendonly. All others toggle to true: - carr.set(fullTrace._group, visibility); + kcont.set(fullTrace._group, visibility); } carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true); } else { @@ -593,7 +601,6 @@ function handleClick(g, gd, numClicks) { case false: nextVisibility = false; break; - default: case 'legendonly': nextVisibility = true; break; @@ -601,7 +608,7 @@ function handleClick(g, gd, numClicks) { if(hasLegendgroup) { for(i = 0; i < fullData.length; i++) { - if(fullData[i].visible && fullData[i].legendgroup === legendgroup) { + if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) { setVisibility(fullData[i], nextVisibility); } } @@ -652,9 +659,9 @@ function handleClick(g, gd, numClicks) { } for(i = 0; i < carrs.length; i++) { - carr = carrs[i]; - if(!carr) continue; - var update = carr.constructUpdate(); + kcont = carrs[i]; + if(!kcont) continue; + var update = kcont.constructUpdate(); var updateKeys = Object.keys(update); for(j = 0; j < updateKeys.length; j++) { From f8a78a2f6613cd017400ed0b64a22114bdcda90d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 20 Sep 2017 10:03:40 -0700 Subject: [PATCH 5/6] Remove unused code in legend interactions --- src/components/legend/draw.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index bc3eb3e2780..77919e7d508 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -618,12 +618,8 @@ function handleClick(g, gd, numClicks) { } else if(numClicks === 2) { // Compute the clicked index. expandedIndex does what we want for expanded traces // but also culls hidden traces. That means we have some work to do. - var clickedIndex, isIsolated, isClicked, isInGroup, otherState; - for(clickedIndex = 0; clickedIndex < fullData.length; clickedIndex++) { - if(fullData[clickedIndex] === fullTrace) break; - } - - isIsolated = true; + var isClicked, isInGroup, otherState; + var isIsolated = true; for(i = 0; i < fullData.length; i++) { isClicked = fullData[i] === fullTrace; if(isClicked) continue; From d23c24dc6bb67b4d51c16dc1cf3944ebefc68eb3 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 20 Sep 2017 10:13:40 -0700 Subject: [PATCH 6/6] Move legend click handler to separate file --- src/components/legend/draw.js | 214 +----------------------- src/components/legend/handle_click.js | 223 ++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 211 deletions(-) create mode 100644 src/components/legend/handle_click.js diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 77919e7d508..6e6def4260d 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); @@ -19,6 +18,7 @@ var dragElement = require('../dragelement'); var Drawing = require('../drawing'); var Color = require('../color'); var svgTextUtils = require('../../lib/svg_text_utils'); +var handleClick = require('./handle_click'); var constants = require('./constants'); var interactConstants = require('../../constants/interactions'); @@ -29,7 +29,6 @@ var style = require('./style'); var helpers = require('./helpers'); var anchorUtils = require('./anchor_utils'); -var SHOWISOLATETIP = true; var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; module.exports = function draw(gd) { @@ -428,8 +427,9 @@ function drawTexts(g, gd) { return Plotly.restyle(gd, update, traceIndex); }); + } else { + text.call(textLayout); } - else text.call(textLayout); } function setupTraceToggle(g, gd) { @@ -478,214 +478,6 @@ function setupTraceToggle(g, gd) { }); } -function handleClick(g, gd, numClicks) { - if(gd._dragged || gd._editing) return; - - var hiddenSlices = gd._fullLayout.hiddenlabels ? - gd._fullLayout.hiddenlabels.slice() : - []; - - var legendItem = g.data()[0][0]; - var fullData = gd._fullData; - var fullTrace = legendItem.trace; - var legendgroup = fullTrace.legendgroup; - - var i, j, kcont, key, keys, val; - var attrUpdate = {}; - var attrIndices = []; - var carrs = []; - var carrIdx = []; - - function insertUpdate(traceIndex, key, value) { - var attrIndex = attrIndices.indexOf(traceIndex); - var valueArray = attrUpdate[key]; - if(!valueArray) { - valueArray = attrUpdate[key] = []; - } - - if(attrIndices.indexOf(traceIndex) === -1) { - attrIndices.push(traceIndex); - attrIndex = attrIndices.length - 1; - } - - valueArray[attrIndex] = value; - - return attrIndex; - } - - function setVisibility(fullTrace, visibility) { - var fullInput = fullTrace._fullInput; - if(Registry.hasTransform(fullInput, 'groupby')) { - var kcont = carrs[fullInput.index]; - if(!kcont) { - var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); - var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1]; - kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible'); - carrs[fullInput.index] = kcont; - } - - var curState = kcont.get(fullTrace._group); - - // If not specified, assume visible. This happens if there are other style - // properties set for a group but not the visibility. There are many similar - // ways to do this (e.g. why not just `curState = fullTrace.visible`??? The - // answer is: because it breaks other things like groupby trace names in - // subtle ways.) - if(curState === undefined) { - curState = true; - } - - if(curState !== false) { - // true -> legendonly. All others toggle to true: - kcont.set(fullTrace._group, visibility); - } - carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true); - } else { - // false -> false (not possible since will not be visible in legend) - // true -> legendonly - // legendonly -> true - var nextVisibility = fullInput.visible === false ? false : visibility; - - insertUpdate(fullInput.index, 'visible', nextVisibility); - } - } - - if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) { - Lib.notifier('Double click on legend to isolate individual trace', 'long'); - SHOWISOLATETIP = false; - } else { - SHOWISOLATETIP = false; - } - - if(Registry.traceIs(fullTrace, 'pie')) { - var thisLabel = legendItem.label, - thisLabelIndex = hiddenSlices.indexOf(thisLabel); - - if(numClicks === 1) { - if(thisLabelIndex === -1) hiddenSlices.push(thisLabel); - else hiddenSlices.splice(thisLabelIndex, 1); - } else if(numClicks === 2) { - hiddenSlices = []; - gd.calcdata[0].forEach(function(d) { - if(thisLabel !== d.label) { - hiddenSlices.push(d.label); - } - }); - if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) { - hiddenSlices = []; - } - } - - Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); - } else { - var hasLegendgroup = legendgroup && legendgroup.length; - var traceIndicesInGroup = []; - var tracei; - if(hasLegendgroup) { - for(i = 0; i < fullData.length; i++) { - tracei = fullData[i]; - if(!tracei.visible) continue; - if(tracei.legendgroup === legendgroup) { - traceIndicesInGroup.push(i); - } - } - } - - if(numClicks === 1) { - var nextVisibility; - - switch(fullTrace.visible) { - case true: - nextVisibility = 'legendonly'; - break; - case false: - nextVisibility = false; - break; - case 'legendonly': - nextVisibility = true; - break; - } - - if(hasLegendgroup) { - for(i = 0; i < fullData.length; i++) { - if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) { - setVisibility(fullData[i], nextVisibility); - } - } - } else { - setVisibility(fullTrace, nextVisibility); - } - } else if(numClicks === 2) { - // Compute the clicked index. expandedIndex does what we want for expanded traces - // but also culls hidden traces. That means we have some work to do. - var isClicked, isInGroup, otherState; - var isIsolated = true; - for(i = 0; i < fullData.length; i++) { - isClicked = fullData[i] === fullTrace; - if(isClicked) continue; - - isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup); - - if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) { - isIsolated = false; - break; - } - } - - for(i = 0; i < fullData.length; i++) { - // False is sticky; we don't change it. - if(fullData[i].visible === false) continue; - - if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) { - continue; - } - - switch(fullTrace.visible) { - case 'legendonly': - setVisibility(fullData[i], true); - break; - case true: - otherState = isIsolated ? true : 'legendonly'; - isClicked = fullData[i] === fullTrace; - isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup); - setVisibility(fullData[i], isInGroup ? true : otherState); - break; - } - } - } - - for(i = 0; i < carrs.length; i++) { - kcont = carrs[i]; - if(!kcont) continue; - var update = kcont.constructUpdate(); - - var updateKeys = Object.keys(update); - for(j = 0; j < updateKeys.length; j++) { - key = updateKeys[j]; - val = attrUpdate[key] = attrUpdate[key] || []; - val[carrIdx[i]] = update[key]; - } - } - - // The length of the value arrays should be equal and any unspecified - // values should be explicitly undefined for them to get properly culled - // as updates and not accidentally reset to the default value. This fills - // out sparse arrays with the required number of undefined values: - keys = Object.keys(attrUpdate); - for(i = 0; i < keys.length; i++) { - key = keys[i]; - for(j = 0; j < attrIndices.length; j++) { - // Use hasOwnPropety to protect against falsey values: - if(!attrUpdate[key].hasOwnProperty(j)) { - attrUpdate[key][j] = undefined; - } - } - } - - Plotly.restyle(gd, attrUpdate, attrIndices); - } -} - function computeTextDimensions(g, gd) { var legendItem = g.data()[0][0]; diff --git a/src/components/legend/handle_click.js b/src/components/legend/handle_click.js new file mode 100644 index 00000000000..46ce749f16f --- /dev/null +++ b/src/components/legend/handle_click.js @@ -0,0 +1,223 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); +var Registry = require('../../registry'); + +var SHOWISOLATETIP = true; + +module.exports = function handleClick(g, gd, numClicks) { + if(gd._dragged || gd._editing) return; + + var hiddenSlices = gd._fullLayout.hiddenlabels ? + gd._fullLayout.hiddenlabels.slice() : + []; + + var legendItem = g.data()[0][0]; + var fullData = gd._fullData; + var fullTrace = legendItem.trace; + var legendgroup = fullTrace.legendgroup; + + var i, j, kcont, key, keys, val; + var attrUpdate = {}; + var attrIndices = []; + var carrs = []; + var carrIdx = []; + + function insertUpdate(traceIndex, key, value) { + var attrIndex = attrIndices.indexOf(traceIndex); + var valueArray = attrUpdate[key]; + if(!valueArray) { + valueArray = attrUpdate[key] = []; + } + + if(attrIndices.indexOf(traceIndex) === -1) { + attrIndices.push(traceIndex); + attrIndex = attrIndices.length - 1; + } + + valueArray[attrIndex] = value; + + return attrIndex; + } + + function setVisibility(fullTrace, visibility) { + var fullInput = fullTrace._fullInput; + if(Registry.hasTransform(fullInput, 'groupby')) { + var kcont = carrs[fullInput.index]; + if(!kcont) { + var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); + var lastGroupbyIndex = groupbyIndices[groupbyIndices.length - 1]; + kcont = Lib.keyedContainer(fullInput, 'transforms[' + lastGroupbyIndex + '].styles', 'target', 'value.visible'); + carrs[fullInput.index] = kcont; + } + + var curState = kcont.get(fullTrace._group); + + // If not specified, assume visible. This happens if there are other style + // properties set for a group but not the visibility. There are many similar + // ways to do this (e.g. why not just `curState = fullTrace.visible`??? The + // answer is: because it breaks other things like groupby trace names in + // subtle ways.) + if(curState === undefined) { + curState = true; + } + + if(curState !== false) { + // true -> legendonly. All others toggle to true: + kcont.set(fullTrace._group, visibility); + } + carrIdx[fullInput.index] = insertUpdate(fullInput.index, 'visible', fullInput.visible === false ? false : true); + } else { + // false -> false (not possible since will not be visible in legend) + // true -> legendonly + // legendonly -> true + var nextVisibility = fullInput.visible === false ? false : visibility; + + insertUpdate(fullInput.index, 'visible', nextVisibility); + } + } + + if(numClicks === 1 && SHOWISOLATETIP && gd.data && gd._context.showTips) { + Lib.notifier('Double click on legend to isolate individual trace', 'long'); + SHOWISOLATETIP = false; + } else { + SHOWISOLATETIP = false; + } + + if(Registry.traceIs(fullTrace, 'pie')) { + var thisLabel = legendItem.label, + thisLabelIndex = hiddenSlices.indexOf(thisLabel); + + if(numClicks === 1) { + if(thisLabelIndex === -1) hiddenSlices.push(thisLabel); + else hiddenSlices.splice(thisLabelIndex, 1); + } else if(numClicks === 2) { + hiddenSlices = []; + gd.calcdata[0].forEach(function(d) { + if(thisLabel !== d.label) { + hiddenSlices.push(d.label); + } + }); + if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) { + hiddenSlices = []; + } + } + + Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); + } else { + var hasLegendgroup = legendgroup && legendgroup.length; + var traceIndicesInGroup = []; + var tracei; + if(hasLegendgroup) { + for(i = 0; i < fullData.length; i++) { + tracei = fullData[i]; + if(!tracei.visible) continue; + if(tracei.legendgroup === legendgroup) { + traceIndicesInGroup.push(i); + } + } + } + + if(numClicks === 1) { + var nextVisibility; + + switch(fullTrace.visible) { + case true: + nextVisibility = 'legendonly'; + break; + case false: + nextVisibility = false; + break; + case 'legendonly': + nextVisibility = true; + break; + } + + if(hasLegendgroup) { + for(i = 0; i < fullData.length; i++) { + if(fullData[i].visible !== false && fullData[i].legendgroup === legendgroup) { + setVisibility(fullData[i], nextVisibility); + } + } + } else { + setVisibility(fullTrace, nextVisibility); + } + } else if(numClicks === 2) { + // Compute the clicked index. expandedIndex does what we want for expanded traces + // but also culls hidden traces. That means we have some work to do. + var isClicked, isInGroup, otherState; + var isIsolated = true; + for(i = 0; i < fullData.length; i++) { + isClicked = fullData[i] === fullTrace; + if(isClicked) continue; + + isInGroup = (hasLegendgroup && fullData[i].legendgroup === legendgroup); + + if(!isInGroup && fullData[i].visible === true && !Registry.traceIs(fullData[i], 'notLegendIsolatable')) { + isIsolated = false; + break; + } + } + + for(i = 0; i < fullData.length; i++) { + // False is sticky; we don't change it. + if(fullData[i].visible === false) continue; + + if(Registry.traceIs(fullData[i], 'notLegendIsolatable')) { + continue; + } + + switch(fullTrace.visible) { + case 'legendonly': + setVisibility(fullData[i], true); + break; + case true: + otherState = isIsolated ? true : 'legendonly'; + isClicked = fullData[i] === fullTrace; + isInGroup = isClicked || (hasLegendgroup && fullData[i].legendgroup === legendgroup); + setVisibility(fullData[i], isInGroup ? true : otherState); + break; + } + } + } + + for(i = 0; i < carrs.length; i++) { + kcont = carrs[i]; + if(!kcont) continue; + var update = kcont.constructUpdate(); + + var updateKeys = Object.keys(update); + for(j = 0; j < updateKeys.length; j++) { + key = updateKeys[j]; + val = attrUpdate[key] = attrUpdate[key] || []; + val[carrIdx[i]] = update[key]; + } + } + + // The length of the value arrays should be equal and any unspecified + // values should be explicitly undefined for them to get properly culled + // as updates and not accidentally reset to the default value. This fills + // out sparse arrays with the required number of undefined values: + keys = Object.keys(attrUpdate); + for(i = 0; i < keys.length; i++) { + key = keys[i]; + for(j = 0; j < attrIndices.length; j++) { + // Use hasOwnPropety to protect against falsey values: + if(!attrUpdate[key].hasOwnProperty(j)) { + attrUpdate[key][j] = undefined; + } + } + } + + Plotly.restyle(gd, attrUpdate, attrIndices); + } +};