From afeaf5ef5aea3adab4ee91767f0275c0a80a8d77 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Wed, 1 Mar 2017 14:20:57 -0800 Subject: [PATCH 1/7] Allow toggling legend to show just 1 series (or group) by double clicking --- src/components/legend/constants.js | 3 +- src/components/legend/draw.js | 109 ++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 527fa7ba190..a8311e551fd 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -12,5 +12,6 @@ module.exports = { scrollBarWidth: 4, scrollBarHeight: 20, scrollBarColor: '#808BA4', - scrollBarMargin: 4 + scrollBarMargin: 4, + DBLCLICKDELAY: 300 }; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index d6c836c14e1..3be59f4ba9e 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -33,6 +33,8 @@ module.exports = function draw(gd) { if(!fullLayout._infolayer || !gd.calcdata) return; + if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0; + var opts = fullLayout.legend, legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), hiddenSlices = fullLayout.hiddenlabels || []; @@ -395,9 +397,9 @@ function drawTexts(g, gd) { } function setupTraceToggle(g, gd) { - var hiddenSlices = gd._fullLayout.hiddenlabels ? - gd._fullLayout.hiddenlabels.slice() : - []; + var newMouseDownTime, + numClicks = 1, + DBLCLICKDELAY = constants.DBLCLICKDELAY; var traceToggle = g.selectAll('rect') .data([0]); @@ -408,41 +410,98 @@ function setupTraceToggle(g, gd) { .attr('pointer-events', 'all') .call(Color.fill, 'rgba(0,0,0,0)'); - traceToggle.on('click', function() { + + traceToggle.on('mousedown', function() { + newMouseDownTime = (new Date()).getTime(); + if(newMouseDownTime - gd._legendMouseDownTime < DBLCLICKDELAY) { + // in a click train + numClicks += 1; + } + else { + // new click train + numClicks = 1; + gd._legendMouseDownTime = newMouseDownTime; + } + }); + traceToggle.on('mouseup', function() { if(gd._dragged) return; + var legend = gd._fullLayout.legend; - var legendItem = g.data()[0][0], - fullData = gd._fullData, - trace = legendItem.trace, - legendgroup = trace.legendgroup, - traceIndicesInGroup = [], - tracei, - newVisible; + if((new Date()).getTime() - gd._legendMouseDownTime > DBLCLICKDELAY) { + numClicks = Math.max(numClicks - 1, 1); + } - if(Registry.traceIs(trace, 'pie')) { - var thisLabel = legendItem.label, - thisLabelIndex = hiddenSlices.indexOf(thisLabel); + if(numClicks === 1) { + legend._clickTimeout = setTimeout(function() { handleClick(g, gd, numClicks); }, 300); + } else if(numClicks === 2) { + if(legend._clickTimeout) { + clearTimeout(legend._clickTimeout); + } + handleClick(g, gd, numClicks); + } + }); +} + +function handleClick(g, gd, numClicks) { + 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; + + if(Registry.traceIs(trace, '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); + } + }); + } + + Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); + } else { + var otherTraces = [], + traceIndex, + i; + + for(i = 0; i < fullData.length; i++) { + otherTraces.push(i); + } - Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); + if(legendgroup === '') { + traceIndicesInGroup = [trace.index]; + otherTraces.splice(trace.index, 1); } else { - if(legendgroup === '') { - traceIndicesInGroup = [trace.index]; - } else { - for(var i = 0; i < fullData.length; i++) { - tracei = fullData[i]; - if(tracei.legendgroup === legendgroup) { - traceIndicesInGroup.push(tracei.index); - } + for(i = 0; i < fullData.length; i++) { + tracei = fullData[i]; + if(tracei.legendgroup === legendgroup) { + traceIndicesInGroup.push(tracei.index); + traceIndex = otherTraces.indexOf(i); + otherTraces.splice(traceIndex, 1); } } - + } + if(numClicks === 1) { newVisible = trace.visible === true ? 'legendonly' : true; Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); + } else if(numClicks === 2) { + Plotly.restyle(gd, 'visible', true, traceIndicesInGroup); + Plotly.restyle(gd, 'visible', 'legendonly', otherTraces); } - }); + } } function computeTextDimensions(g, gd) { From 895ba890ae71dc01b572bfbe144682dfa54213e5 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Thu, 2 Mar 2017 18:11:29 -0800 Subject: [PATCH 2/7] Trace Isolation PR Feedback - Show Lib.notify on first single-click toggle - Refactor DBLCLICKDELAY into interaction constants - Double click on the only visible trace (or group) re-enables all traces - Single call to restyle TODO: Tests, fix interaction on editable: true --- src/components/dragelement/index.js | 4 +-- src/components/legend/constants.js | 3 +-- src/components/legend/draw.js | 42 ++++++++++++++++++++++------- src/constants/interactions.js | 6 ++++- src/plots/cartesian/constants.js | 4 --- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 8b531a1b5a8..5661e3b148c 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -13,7 +13,7 @@ var Plotly = require('../../plotly'); var Lib = require('../../lib'); var constants = require('../../plots/cartesian/constants'); - +var interactConstants = require('../../constants/interactions'); var dragElement = module.exports = {}; @@ -51,7 +51,7 @@ dragElement.unhoverRaw = unhover.raw; dragElement.init = function init(options) { var gd = Lib.getPlotDiv(options.element) || {}, numClicks = 1, - DBLCLICKDELAY = constants.DBLCLICKDELAY, + DBLCLICKDELAY = interactConstants.DBLCLICKDELAY, startX, startY, newMouseDownTime, diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index a8311e551fd..527fa7ba190 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -12,6 +12,5 @@ module.exports = { scrollBarWidth: 4, scrollBarHeight: 20, scrollBarColor: '#808BA4', - scrollBarMargin: 4, - DBLCLICKDELAY: 300 + scrollBarMargin: 4 }; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 3be59f4ba9e..542e43b577d 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -21,11 +21,13 @@ var Color = require('../color'); var svgTextUtils = require('../../lib/svg_text_utils'); var constants = require('./constants'); +var interactConstants = require('../../constants/interactions'); var getLegendData = require('./get_legend_data'); var style = require('./style'); var helpers = require('./helpers'); var anchorUtils = require('./anchor_utils'); +var SHOWISOLATETIP = true; module.exports = function draw(gd) { var fullLayout = gd._fullLayout; @@ -399,7 +401,7 @@ function drawTexts(g, gd) { function setupTraceToggle(g, gd) { var newMouseDownTime, numClicks = 1, - DBLCLICKDELAY = constants.DBLCLICKDELAY; + DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; var traceToggle = g.selectAll('rect') .data([0]); @@ -432,7 +434,7 @@ function setupTraceToggle(g, gd) { } if(numClicks === 1) { - legend._clickTimeout = setTimeout(function() { handleClick(g, gd, numClicks); }, 300); + legend._clickTimeout = setTimeout(function() { handleClick(g, gd, numClicks); }, DBLCLICKDELAY); } else if(numClicks === 2) { if(legend._clickTimeout) { clearTimeout(legend._clickTimeout); @@ -455,6 +457,13 @@ function handleClick(g, gd, numClicks) { tracei, newVisible; + + 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(trace, 'pie')) { var thisLabel = legendItem.label, thisLabelIndex = hiddenSlices.indexOf(thisLabel); @@ -469,37 +478,50 @@ function handleClick(g, gd, numClicks) { hiddenSlices.push(d.label); } }); + if(gd._fullLayout.hiddenlabels && gd._fullLayout.hiddenlabels.length === hiddenSlices.length && thisLabelIndex === -1) { + hiddenSlices = []; + } } Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); } else { - var otherTraces = [], - traceIndex, + var allTraces = [], + traceVisibility = [], i; for(i = 0; i < fullData.length; i++) { - otherTraces.push(i); + allTraces.push(i); + traceVisibility.push('legendonly'); } if(legendgroup === '') { traceIndicesInGroup = [trace.index]; - otherTraces.splice(trace.index, 1); + traceVisibility[trace.index] = true; } else { for(i = 0; i < fullData.length; i++) { tracei = fullData[i]; if(tracei.legendgroup === legendgroup) { traceIndicesInGroup.push(tracei.index); - traceIndex = otherTraces.indexOf(i); - otherTraces.splice(traceIndex, 1); + traceVisibility[allTraces.indexOf(i)] = true; } } } + if(numClicks === 1) { newVisible = trace.visible === true ? 'legendonly' : true; Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); } else if(numClicks === 2) { - Plotly.restyle(gd, 'visible', true, traceIndicesInGroup); - Plotly.restyle(gd, 'visible', 'legendonly', otherTraces); + var sameAsLast = true; + for(i = 0; i < fullData.length; i++) { + if(fullData[i].visible !== traceVisibility[i]) { + sameAsLast = false; + break; + } + } + if(sameAsLast) { + traceVisibility = true; + } + Plotly.restyle(gd, 'visible', traceVisibility, allTraces); } } } diff --git a/src/constants/interactions.js b/src/constants/interactions.js index 132e9ca4c37..3e56a09f434 100644 --- a/src/constants/interactions.js +++ b/src/constants/interactions.js @@ -14,5 +14,9 @@ module.exports = { * Timing information for interactive elements */ SHOW_PLACEHOLDER: 100, - HIDE_PLACEHOLDER: 1000 + HIDE_PLACEHOLDER: 1000, + + // ms between first mousedown and 2nd mouseup to constitute dblclick... + // we don't seem to have access to the system setting + DBLCLICKDELAY: 300 }; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 8d7ed20df45..2934d4ecd0f 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -29,10 +29,6 @@ module.exports = { AX_ID_PATTERN: /^[xyz][0-9]*$/, AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, - // ms between first mousedown and 2nd mouseup to constitute dblclick... - // we don't seem to have access to the system setting - DBLCLICKDELAY: 300, - // pixels to move mouse before you stop clamping to starting point MINDRAG: 8, From afcf4f49343cb02fd6553c2c2fbc3df634a2493c Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Thu, 2 Mar 2017 19:39:53 -0800 Subject: [PATCH 3/7] Sort out interaction between drag/edit and legend toggles. Introduces a global _editing flag to be able to block other interactions while edit is in progress. Could be one solution for #1437. This makes it more clear that there should be some refactor to consolidate the double click behavior, since when the drag layer is on, that code path's click detection is used. The other path needs to remain since the drag layer doesn't exist when editable: false, but it's now easier to picture a world where these two paths converge. --- src/components/dragelement/index.js | 5 +++-- src/components/legend/draw.js | 27 +++++++++++++++++++++++++-- src/lib/svg_text_utils.js | 6 +++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 5661e3b148c..156382b6c5d 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -37,12 +37,13 @@ dragElement.unhoverRaw = unhover.raw; * dx and dy are the net pixel offset of the drag, * dragged is true/false, has the mouse moved enough to * constitute a drag - * doneFn (optional) function(dragged, numClicks) + * doneFn (optional) function(dragged, numClicks, e) * executed on mouseup, or mouseout of window since * we don't get events after that * dragged is as in moveFn * numClicks is how many clicks we've registered within * a doubleclick time + * e is the original event * setCursor (optional) function(event) * executed on mousemove before mousedown * the purpose of this callback is to update the mouse cursor before @@ -136,7 +137,7 @@ dragElement.init = function init(options) { numClicks = Math.max(numClicks - 1, 1); } - if(options.doneFn) options.doneFn(gd._dragged, numClicks); + if(options.doneFn) options.doneFn(gd._dragged, numClicks, e); if(!gd._dragged) { var e2 = document.createEvent('MouseEvents'); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 542e43b577d..4eabae125cd 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -329,9 +329,31 @@ module.exports = function draw(gd) { xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor); yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor); }, - doneFn: function(dragged) { + doneFn: function(dragged, numClicks, e) { if(dragged && xf !== undefined && yf !== undefined) { Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); + } else { + var traces = [], + clickedTrace; + fullLayout._infolayer.selectAll('g.traces').each(function() { d3.select(this).call(function() { traces.push(this); }); }); + for(var i = 0; i < traces.length; i++) { + var tracei = traces[i]; + var p = tracei[0][0].getBoundingClientRect(); + if(e.clientX >= p.left && e.clientX <= p.right && e.clientY >= p.top && e.clientY <= p.bottom) { + clickedTrace = tracei; + break; + } + } + if(clickedTrace) { + if(numClicks === 1) { + legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, 300); + } else if(numClicks === 2) { + if(legend._clickTimeout) { + clearTimeout(legend._clickTimeout); + } + handleClick(clickedTrace, gd, numClicks); + } + } } } }); @@ -426,7 +448,7 @@ function setupTraceToggle(g, gd) { } }); traceToggle.on('mouseup', function() { - if(gd._dragged) return; + if(gd._dragged || gd._editing) return; var legend = gd._fullLayout.legend; if((new Date()).getTime() - gd._legendMouseDownTime > DBLCLICKDELAY) { @@ -445,6 +467,7 @@ function setupTraceToggle(g, gd) { } function handleClick(g, gd, numClicks) { + if(gd._dragged || gd._editing) return; var hiddenSlices = gd._fullLayout.hiddenlabels ? gd._fullLayout.hiddenlabels.slice() : []; diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index a55334d5157..f6d4419dc0c 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -467,7 +467,8 @@ exports.makeEditable = function(context, _delegate, options) { } function appendEditable() { - var plotDiv = d3.select(Lib.getPlotDiv(that.node())), + var gd = Lib.getPlotDiv(that.node()), + plotDiv = d3.select(gd), container = plotDiv.select('.svg-container'), div = container.append('div'); div.classed('plugin-editable editable', true) @@ -487,6 +488,7 @@ exports.makeEditable = function(context, _delegate, options) { .text(options.text || that.attr('data-unformatted')) .call(alignHTMLWith(that, container, options)) .on('blur', function() { + gd._editing = false; that.text(this.textContent) .style({opacity: 1}); var svgClass = d3.select(this).attr('class'), @@ -503,6 +505,7 @@ exports.makeEditable = function(context, _delegate, options) { }) .on('focus', function() { var context = this; + gd._editing = true; d3.select(document).on('mouseup', function() { if(d3.event.target === context) return false; if(document.activeElement === div.node()) div.node().blur(); @@ -510,6 +513,7 @@ exports.makeEditable = function(context, _delegate, options) { }) .on('keyup', function() { if(d3.event.which === 27) { + gd._editing = false; that.style({opacity: 1}); d3.select(this) .style({opacity: 0}) From c5a2ff830fbb0e7b09ffc3b39f09ab37e4c3abe3 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 3 Mar 2017 19:54:51 -0800 Subject: [PATCH 4/7] Move DBLCLICKDELAY and make clicked series detection cleaner --- src/components/legend/draw.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 4eabae125cd..30163eb1e98 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -28,6 +28,7 @@ var helpers = require('./helpers'); var anchorUtils = require('./anchor_utils'); var SHOWISOLATETIP = true; +var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; module.exports = function draw(gd) { var fullLayout = gd._fullLayout; @@ -335,18 +336,15 @@ module.exports = function draw(gd) { } else { var traces = [], clickedTrace; - fullLayout._infolayer.selectAll('g.traces').each(function() { d3.select(this).call(function() { traces.push(this); }); }); - for(var i = 0; i < traces.length; i++) { - var tracei = traces[i]; - var p = tracei[0][0].getBoundingClientRect(); - if(e.clientX >= p.left && e.clientX <= p.right && e.clientY >= p.top && e.clientY <= p.bottom) { - clickedTrace = tracei; - break; - } - } - if(clickedTrace) { + traces = fullLayout._infolayer.selectAll('g.traces').filter(function() { + var bbox = this.getBoundingClientRect(); + return (e.clientX >= bbox.left && e.clientX <= bbox.right && + e.clientY >= bbox.top && e.clientY <= bbox.bottom); + })[0]; + if(traces.length > 0) { + clickedTrace = d3.select(traces[0]); if(numClicks === 1) { - legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, 300); + legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, DBLCLICKDELAY); } else if(numClicks === 2) { if(legend._clickTimeout) { clearTimeout(legend._clickTimeout); @@ -422,8 +420,7 @@ function drawTexts(g, gd) { function setupTraceToggle(g, gd) { var newMouseDownTime, - numClicks = 1, - DBLCLICKDELAY = interactConstants.DBLCLICKDELAY; + numClicks = 1; var traceToggle = g.selectAll('rect') .data([0]); From fb3708844e5a5e09608d7d8614f710db755c8e3f Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 3 Mar 2017 20:42:28 -0800 Subject: [PATCH 5/7] Test related fixes - Update Purge() to account for new field. - Update import for DBLCLICKDELAY - Update tests for mousedown+mouseup and DBLCLICKDELAY before toggle fades. --- src/plot_api/plot_api.js | 1 + test/jasmine/tests/click_test.js | 2 +- test/jasmine/tests/legend_scroll_test.js | 38 ++++++++++++++---------- test/jasmine/tests/plots_test.js | 2 +- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e805637d037..431fec5bccc 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2794,6 +2794,7 @@ Plotly.purge = function purge(gd) { delete gd._context; delete gd._replotPending; delete gd._mouseDownTime; + delete gd._legendMouseDownTime; delete gd._hmpixcount; delete gd._hmlumcount; diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index b38c7e1d476..9e13b9ffd12 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -1,7 +1,7 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); var Drawing = require('@src/components/drawing'); -var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; +var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js index b93610ac5f7..64087fd14a1 100644 --- a/test/jasmine/tests/legend_scroll_test.js +++ b/test/jasmine/tests/legend_scroll_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); var constants = require('@src/components/legend/constants'); +var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; var d3 = require('d3'); var createGraph = require('../assets/create_graph_div'); @@ -8,7 +9,6 @@ var destroyGraph = require('../assets/destroy_graph_div'); var getBBox = require('../assets/get_bbox'); var mock = require('../../image/mocks/legend_scroll.json'); - describe('The legend', function() { 'use strict'; @@ -96,7 +96,7 @@ describe('The legend', function() { 'translate(0, ' + finalDataScroll + ')'); }); - it('should keep the scrollbar position after a toggle event', function() { + it('should keep the scrollbar position after a toggle event', function(done) { var legend = getLegend(), scrollBox = getScrollBox(), toggle = getToggle(), @@ -105,14 +105,18 @@ describe('The legend', function() { legend.dispatchEvent(scrollTo(wheelDeltaY)); var dataScroll = scrollBox.getAttribute('data-scroll'); - toggle.dispatchEvent(new MouseEvent('click')); - expect(+toggle.parentNode.style.opacity).toBeLessThan(1); - expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); - expect(scrollBox.getAttribute('transform')).toBe( - 'translate(0, ' + dataScroll + ')'); + toggle.dispatchEvent(new MouseEvent('mousedown')); + toggle.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(+toggle.parentNode.style.opacity).toBeLessThan(1); + expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); + expect(scrollBox.getAttribute('transform')).toBe( + 'translate(0, ' + dataScroll + ')'); + done(); + }, DBLCLICKDELAY * 2); }); - it('should be restored and functional after relayout', function() { + it('should be restored and functional after relayout', function(done) { var wheelDeltaY = 100, legend = getLegend(), scrollBox, @@ -139,13 +143,17 @@ describe('The legend', function() { expect(scrollBar.getAttribute('y')).toBe(scrollBarY); var dataScroll = scrollBox.getAttribute('data-scroll'); - toggle.dispatchEvent(new MouseEvent('click')); - expect(+toggle.parentNode.style.opacity).toBeLessThan(1); - expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); - expect(scrollBox.getAttribute('transform')).toBe( - 'translate(0, ' + dataScroll + ')'); - expect(scrollBar.getAttribute('width')).toBeGreaterThan(0); - expect(scrollBar.getAttribute('height')).toBeGreaterThan(0); + toggle.dispatchEvent(new MouseEvent('mousedown')); + toggle.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(+toggle.parentNode.style.opacity).toBeLessThan(1); + expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); + expect(scrollBox.getAttribute('transform')).toBe( + 'translate(0, ' + dataScroll + ')'); + expect(scrollBar.getAttribute('width')).toBeGreaterThan(0); + expect(scrollBar.getAttribute('height')).toBeGreaterThan(0); + done(); + }, DBLCLICKDELAY * 2); }); it('should constrain scrolling to the contents', function() { diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 0e702ec5b89..4e2275030ac 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -397,7 +397,7 @@ describe('Test Plots', function() { '_ev', '_internalEv', 'on', 'once', 'removeListener', 'removeAllListeners', '_internalOn', '_internalOnce', '_removeInternalListener', '_removeAllInternalListeners', 'emit', '_context', '_replotPending', - '_hmpixcount', '_hmlumcount', '_mouseDownTime' + '_hmpixcount', '_hmlumcount', '_mouseDownTime', '_legendMouseDownTime', ]; Plots.purge(gd); From 1fe3b085ac3be1533d7861475f2c209adb54a43a Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Sun, 5 Mar 2017 22:12:18 -0800 Subject: [PATCH 6/7] New legend interaction tests --- test/jasmine/tests/legend_test.js | 186 ++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index c832da02dcd..24bd3f3a931 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); +var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; var Legend = require('@src/components/legend'); var getLegendData = require('@src/components/legend/get_legend_data'); @@ -628,3 +629,188 @@ describe('legend restyle update', function() { }); }); }); + +describe('legend interaction', function() { + 'use strict'; + + describe('pie chart', function() { + var mockCopy, gd, legendItems, legendItem, legendLabels, legendLabel; + var testEntry = 2; + + beforeAll(function(done) { + var mock = require('@mocks/pie_simple.json'); + mockCopy = Lib.extendDeep({}, mock); + gd = createGraphDiv(); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + legendItems = d3.selectAll('rect.legendtoggle')[0]; + legendLabels = d3.selectAll('text.legendtext')[0]; + legendItem = legendItems[testEntry]; + legendLabel = legendLabels[testEntry].innerHTML; + done(); + }); + }); + afterAll(function() { + destroyGraphDiv(); + }); + describe('single click', function() { + it('should hide slice', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd._fullLayout.hiddenlabels.length).toBe(1); + expect(gd._fullLayout.hiddenlabels[0]).toBe(legendLabel); + done(); + }, DBLCLICKDELAY * 2); + }); + it('should fade legend item', function() { + expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); + }); + it('should unhide slice', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd._fullLayout.hiddenlabels.length).toBe(0); + done(); + }, DBLCLICKDELAY * 2); + }); + it('should unfade legend item', function() { + expect(+legendItem.parentNode.style.opacity).toBe(1); + }); + }); + + describe('double click', function() { + it('should hide other slices', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd._fullLayout.hiddenlabels.length).toBe((legendItems.length - 1)); + expect(gd._fullLayout.hiddenlabels.indexOf(legendLabel)).toBe(-1); + done(); + }, DBLCLICKDELAY); + }); + it('should fade other legend items', function() { + var legendItemi; + for(var i = 0; i < legendItems.length; i++) { + legendItemi = legendItems[i]; + if(i === testEntry) { + expect(+legendItemi.parentNode.style.opacity).toBe(1); + } else { + expect(+legendItemi.parentNode.style.opacity).toBeLessThan(1); + } + } + }); + it('should unhide all slices', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd._fullLayout.hiddenlabels.length).toBe(0); + done(); + }, DBLCLICKDELAY); + }); + it('should unfade legend items', function() { + var legendItemi; + for(var i = 0; i < legendItems.length; i++) { + legendItemi = legendItems[i]; + expect(+legendItemi.parentNode.style.opacity).toBe(1); + } + }); + }); + }); + describe('non-pie chart', function() { + var mockCopy, gd, legendItems, legendItem; + var testEntry = 2; + + beforeAll(function(done) { + var mock = require('@mocks/29.json'); + mockCopy = Lib.extendDeep({}, mock); + gd = createGraphDiv(); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + legendItems = d3.selectAll('rect.legendtoggle')[0]; + legendItem = legendItems[testEntry]; + done(); + }); + }); + afterAll(function() { + destroyGraphDiv(); + }); + + describe('single click', function() { + it('should hide series', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd.data[2].visible).toBe('legendonly'); + done(); + }, DBLCLICKDELAY * 2); + }); + it('should fade legend item', function() { + expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); + }); + it('should unhide series', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + expect(gd.data[2].visible).toBe(true); + done(); + }, DBLCLICKDELAY * 2); + }); + it('should unfade legend item', function() { + expect(+legendItem.parentNode.style.opacity).toBe(1); + }); + }); + describe('double click', function() { + it('should hide series', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + for(var i = 0; i < legendItems.length; i++) { + if(i === testEntry) { + expect(gd.data[i].visible).toBe(true); + } else { + expect(gd.data[i].visible).toBe('legendonly'); + } + } + done(); + }, DBLCLICKDELAY); + }); + it('should fade legend item', function() { + var legendItemi; + for(var i = 0; i < legendItems.length; i++) { + legendItemi = legendItems[i]; + if(i === testEntry) { + expect(+legendItemi.parentNode.style.opacity).toBe(1); + } else { + expect(+legendItemi.parentNode.style.opacity).toBeLessThan(1); + } + } + }); + it('should unhide series', function(done) { + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + legendItem.dispatchEvent(new MouseEvent('mousedown')); + legendItem.dispatchEvent(new MouseEvent('mouseup')); + setTimeout(function() { + for(var i = 0; i < legendItems.length; i++) { + expect(gd.data[i].visible).toBe(true); + } + done(); + }, DBLCLICKDELAY); + }); + it('should unfade legend items', function() { + var legendItemi; + for(var i = 0; i < legendItems.length; i++) { + legendItemi = legendItems[i]; + expect(+legendItemi.parentNode.style.opacity).toBe(1); + } + }); + }); + }); +}); From edc30acb12d96aa51ca80ec96f63f749934195e1 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 10 Mar 2017 23:27:34 -0800 Subject: [PATCH 7/7] Reduce test delays and fix uncovered bug Fixup double_click asset Cleanup legend item selection code in editable mode --- src/components/legend/draw.js | 17 ++++++++--------- test/jasmine/assets/double_click.js | 2 +- test/jasmine/tests/legend_test.js | 16 ++++++++-------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 30163eb1e98..beb5f58a1c8 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -334,15 +334,13 @@ module.exports = function draw(gd) { if(dragged && xf !== undefined && yf !== undefined) { Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); } else { - var traces = [], - clickedTrace; - traces = fullLayout._infolayer.selectAll('g.traces').filter(function() { - var bbox = this.getBoundingClientRect(); - return (e.clientX >= bbox.left && e.clientX <= bbox.right && - e.clientY >= bbox.top && e.clientY <= bbox.bottom); - })[0]; - if(traces.length > 0) { - clickedTrace = d3.select(traces[0]); + var clickedTrace = + fullLayout._infolayer.selectAll('g.traces').filter(function() { + var bbox = this.getBoundingClientRect(); + return (e.clientX >= bbox.left && e.clientX <= bbox.right && + e.clientY >= bbox.top && e.clientY <= bbox.bottom); + }); + if(clickedTrace.size() > 0) { if(numClicks === 1) { legend._clickTimeout = setTimeout(function() { handleClick(clickedTrace, gd, numClicks); }, DBLCLICKDELAY); } else if(numClicks === 2) { @@ -458,6 +456,7 @@ function setupTraceToggle(g, gd) { if(legend._clickTimeout) { clearTimeout(legend._clickTimeout); } + gd._legendMouseDownTime = 0; handleClick(g, gd, numClicks); } }); diff --git a/test/jasmine/assets/double_click.js b/test/jasmine/assets/double_click.js index e92375588a8..2e66c90f952 100644 --- a/test/jasmine/assets/double_click.js +++ b/test/jasmine/assets/double_click.js @@ -1,5 +1,5 @@ var click = require('./click'); -var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; +var DBLCLICKDELAY = require('@src/constants/interactions').DBLCLICKDELAY; module.exports = function doubleClick(x, y) { return new Promise(function(resolve) { diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 24bd3f3a931..7cb4f06974b 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -661,7 +661,7 @@ describe('legend interaction', function() { expect(gd._fullLayout.hiddenlabels.length).toBe(1); expect(gd._fullLayout.hiddenlabels[0]).toBe(legendLabel); done(); - }, DBLCLICKDELAY * 2); + }, DBLCLICKDELAY + 20); }); it('should fade legend item', function() { expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); @@ -672,7 +672,7 @@ describe('legend interaction', function() { setTimeout(function() { expect(gd._fullLayout.hiddenlabels.length).toBe(0); done(); - }, DBLCLICKDELAY * 2); + }, DBLCLICKDELAY + 20); }); it('should unfade legend item', function() { expect(+legendItem.parentNode.style.opacity).toBe(1); @@ -689,7 +689,7 @@ describe('legend interaction', function() { expect(gd._fullLayout.hiddenlabels.length).toBe((legendItems.length - 1)); expect(gd._fullLayout.hiddenlabels.indexOf(legendLabel)).toBe(-1); done(); - }, DBLCLICKDELAY); + }, 20); }); it('should fade other legend items', function() { var legendItemi; @@ -710,7 +710,7 @@ describe('legend interaction', function() { setTimeout(function() { expect(gd._fullLayout.hiddenlabels.length).toBe(0); done(); - }, DBLCLICKDELAY); + }, 20); }); it('should unfade legend items', function() { var legendItemi; @@ -747,7 +747,7 @@ describe('legend interaction', function() { setTimeout(function() { expect(gd.data[2].visible).toBe('legendonly'); done(); - }, DBLCLICKDELAY * 2); + }, DBLCLICKDELAY + 20); }); it('should fade legend item', function() { expect(+legendItem.parentNode.style.opacity).toBeLessThan(1); @@ -758,7 +758,7 @@ describe('legend interaction', function() { setTimeout(function() { expect(gd.data[2].visible).toBe(true); done(); - }, DBLCLICKDELAY * 2); + }, DBLCLICKDELAY + 20); }); it('should unfade legend item', function() { expect(+legendItem.parentNode.style.opacity).toBe(1); @@ -779,7 +779,7 @@ describe('legend interaction', function() { } } done(); - }, DBLCLICKDELAY); + }, 20); }); it('should fade legend item', function() { var legendItemi; @@ -802,7 +802,7 @@ describe('legend interaction', function() { expect(gd.data[i].visible).toBe(true); } done(); - }, DBLCLICKDELAY); + }, 20); }); it('should unfade legend items', function() { var legendItemi;