From 627fd74d07bdcfb4407c9241e5f00d92e09ab293 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 14 Mar 2019 00:32:22 -0400 Subject: [PATCH 1/7] sankey: implement node grouping via mouse selection --- src/components/modebar/buttons.js | 11 ++++ src/components/modebar/manage.js | 4 ++ src/plots/cartesian/select.js | 9 +++ src/traces/sankey/base_plot.js | 97 +++++++++++++++++++++++++++++++ src/traces/sankey/calc.js | 1 + src/traces/sankey/index.js | 1 + src/traces/sankey/render.js | 17 ++++++ src/traces/sankey/select.js | 36 ++++++++++++ 8 files changed, 176 insertions(+) create mode 100644 src/traces/sankey/select.js diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 82422887afb..3753d9924c6 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -513,6 +513,17 @@ function toggleHover(gd) { Registry.call('_guiRelayout', gd, 'hovermode', newHover); } +modeBarButtons.resetSankeyGroup = { + name: 'resetSankeyGroup', + title: function(gd) { return _(gd, 'Ungroup all nodes'); }, + icon: Icons.home, + click: function(gd) { + Registry.call('restyle', gd, { + 'node.groups': [[]], + }); + } +}; + // buttons when more then one plot types are present modeBarButtons.toggleHover = { diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 37db6546ee8..9a9e9c0b922 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -86,6 +86,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) { var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); + var hasSankey = fullLayout._has('sankey'); var allAxesFixed = areAllAxesFixed(fullLayout); var groups = []; @@ -139,6 +140,9 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) { else if(hasPie) { hoverGroup = ['hoverClosestPie']; } + else if(hasSankey) { + hoverGroup = ['resetSankeyGroup']; + } else { // hasPolar, hasTernary // always show at least one hover icon. hoverGroup = ['toggleHover']; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index a051adbe9ad..20403dbf4bf 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -48,6 +48,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes); var subtract = e.altKey; + var doneFnCompleted = dragOptions.doneFnCompleted; + var filterPoly, selectionTester, mergedPolygons, currentPolygon; var i, searchInfo, eventData; @@ -284,6 +286,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) { dragOptions.mergedPolygons.length = 0; [].push.apply(dragOptions.mergedPolygons, mergedPolygons); } + + doneFnCompleted(selection); }); }; } @@ -519,6 +523,11 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { var info = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); info.scene = gd._fullLayout._splomScenes[trace.uid]; searchTraces.push(info); + } else if( + trace.type === 'sankey' + ) { + var sankeyInfo = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); + searchTraces.push(sankeyInfo); } else { if(xAxisIds.indexOf(trace.xaxis) === -1) continue; if(yAxisIds.indexOf(trace.yaxis) === -1) continue; diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index 0721123e483..bb0bd95dc73 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -13,6 +13,12 @@ var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; var plot = require('./plot'); var fxAttrs = require('../../components/fx/layout_attributes'); +var setCursor = require('../../lib/setcursor'); +var dragElement = require('../../components/dragelement'); +var prepSelect = require('../../plots/cartesian/select').prepSelect; +var Lib = require('../../lib'); +var Registry = require('../../registry'); + var SANKEY = 'sankey'; exports.name = SANKEY; @@ -34,3 +40,94 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) oldFullLayout._paperdiv.selectAll('.sankey').remove(); } }; + +exports.updateFx = function(gd) { + for(var i = 0; i < gd._fullData.length; i++) { + subplotUpdateFx(gd, i); + } +}; + +var dragOptions = []; +function subplotUpdateFx(gd, index) { + var i = index; + var fullData = gd._fullData[i]; + var fullLayout = gd._fullLayout; + + var dragMode = fullLayout.dragmode; + var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; + var bgRect = fullData._bgRect; + + setCursor(bgRect, cursor); + + var xaxis = { + _id: 'x', + c2p: function(v) { return v; }, + _offset: fullData._sankey.translateX, + _length: fullData._sankey.width + }; + var yaxis = { + _id: 'y', + c2p: function(v) { return v; }, + _offset: fullData._sankey.translateY, + _length: fullData._sankey.height + }; + + // Note: dragOptions is needed to be declared for all dragmodes because + // it's the object that holds persistent selection state. + dragOptions[i] = { + gd: gd, + element: bgRect.node(), + plotinfo: { + id: i, + xaxis: xaxis, + yaxis: yaxis, + fillRangeItems: Lib.noop + }, + subplot: i, + // create mock x/y axes for hover routine + xaxes: [xaxis], + yaxes: [yaxis], + doneFnCompleted: function(selection) { + var newGroups; + var oldGroups = gd._fullData[i].node.groups.slice(); + var newGroup = []; + + function findNode(pt) { + return gd._fullData[i]._sankey.graph.nodes.find(function(n) { + return n.pointNumber === pt; + }); + } + + for(var j = 0; j < selection.length; j++) { + var node = findNode(selection[j].pointNumber); + if(!node) continue; + + // If the node represents a group + if(node.group) { + // Add all its children to the current selection + for(var k = 0; k < node.childrenNodes.length; k++) { + newGroup.push(node.childrenNodes[k].pointNumber); + } + // Flag group for removal from existing list of groups + oldGroups[node.pointNumber - fullData.node._count] = false; + } else { + newGroup.push(node.pointNumber); + } + } + + newGroups = oldGroups + .filter(function(g) { return g; }) + .concat([newGroup]); + + Registry.call('_guiRestyle', gd, { + 'node.groups': [ newGroups ] + }, i).catch(Lib.noop); // TODO will this ever fail? + } + }; + + dragOptions[i].prepFn = function(e, startX, startY) { + prepSelect(e, startX, startY, dragOptions[i], dragMode); + }; + + dragElement.init(dragOptions[i]); +} diff --git a/src/traces/sankey/calc.js b/src/traces/sankey/calc.js index b46715a155e..319eb8de82d 100644 --- a/src/traces/sankey/calc.js +++ b/src/traces/sankey/calc.js @@ -40,6 +40,7 @@ function convertToD3Sankey(trace) { if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i]; } var nodeCount = maxNodeId + 1; + trace.node._count = nodeCount; // Group nodes var j; diff --git a/src/traces/sankey/index.js b/src/traces/sankey/index.js index d4cc4a22a81..677d08ca7ea 100644 --- a/src/traces/sankey/index.js +++ b/src/traces/sankey/index.js @@ -18,6 +18,7 @@ Plot.plot = require('./plot'); Plot.moduleType = 'trace'; Plot.name = 'sankey'; Plot.basePlotModule = require('./base_plot'); +Plot.selectPoints = require('./select.js'); Plot.categories = ['noOpacity']; Plot.meta = { description: [ diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index ab4a330c8f5..302b40ad946 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -836,6 +836,23 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { .style('pointer-events', 'auto') .attr('transform', sankeyTransform); + sankey.each(function(d, i) { + gd._fullData[i]._sankey = d; + + // Draw dragbox + Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bg-' + i, function(el) { + el + .style('pointer-events', 'all') + .attr('width', d.width) + .attr('height', d.height) + .attr('x', d.translateX) + .attr('y', d.translateY) + .style({fill: 'transparent', 'stroke-width': 0}); + + gd._fullData[i]._bgRect = el; + }); + }); + sankey.transition() .ease(c.ease).duration(c.duration) .attr('transform', sankeyTransform); diff --git a/src/traces/sankey/select.js b/src/traces/sankey/select.js new file mode 100644 index 00000000000..87fa69a253e --- /dev/null +++ b/src/traces/sankey/select.js @@ -0,0 +1,36 @@ +/** +* Copyright 2012-2019, 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'; + +module.exports = function selectPoints(searchInfo, selectionTester) { + var cd = searchInfo.cd; + var selection = []; + var fullData = cd[0].trace; + + var nodes = fullData._sankey.graph.nodes; + + for(var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if(node.partOfGroup) continue; // Those are invisible + + // TODO: decide on selection criteria, using centroid for now + var pos = [(node.x0 + node.x1) / 2, (node.y0 + node.y1) / 2]; + + // Swap x and y if trace is vertical + if(fullData.orientation === 'v') pos.reverse(); + + if(selectionTester.contains(pos, false, i, searchInfo)) { + selection.push({ + pointNumber: node.pointNumber + // TODO: add eventData + }); + } + } + return selection; +}; From 20d8c92d8454cc804e71b84a90469df807d14898 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Tue, 2 Apr 2019 16:39:14 -0400 Subject: [PATCH 2/7] sankey: fix code style, variable names and cleanup draggers --- src/traces/sankey/base_plot.js | 48 ++++++++++++++++--------------- src/traces/sankey/render.js | 4 +-- src/traces/sankey/select.js | 4 +-- test/jasmine/tests/sankey_test.js | 14 +++++++++ 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index bb0bd95dc73..4b54cf45e41 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -30,6 +30,7 @@ exports.baseLayoutAttrOverrides = overrideAll({ exports.plot = function(gd) { var calcData = getModuleCalcData(gd.calcdata, SANKEY)[0]; plot(gd, calcData); + exports.updateFx(gd); }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { @@ -38,6 +39,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) if(hadPlot && !hasPlot) { oldFullLayout._paperdiv.selectAll('.sankey').remove(); + oldFullLayout._paperdiv.selectAll('.bgsankey').remove(); } }; @@ -47,55 +49,55 @@ exports.updateFx = function(gd) { } }; -var dragOptions = []; function subplotUpdateFx(gd, index) { - var i = index; - var fullData = gd._fullData[i]; + var trace = gd._fullData[index]; var fullLayout = gd._fullLayout; var dragMode = fullLayout.dragmode; var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; - var bgRect = fullData._bgRect; + var bgRect = trace._bgRect; setCursor(bgRect, cursor); var xaxis = { _id: 'x', - c2p: function(v) { return v; }, - _offset: fullData._sankey.translateX, - _length: fullData._sankey.width + c2p: Lib.identity, + _offset: trace._sankey.translateX, + _length: trace._sankey.width }; var yaxis = { _id: 'y', - c2p: function(v) { return v; }, - _offset: fullData._sankey.translateY, - _length: fullData._sankey.height + c2p: Lib.identity, + _offset: trace._sankey.translateY, + _length: trace._sankey.height }; // Note: dragOptions is needed to be declared for all dragmodes because // it's the object that holds persistent selection state. - dragOptions[i] = { + var dragOptions = { gd: gd, element: bgRect.node(), plotinfo: { - id: i, + id: index, xaxis: xaxis, yaxis: yaxis, fillRangeItems: Lib.noop }, - subplot: i, + subplot: index, // create mock x/y axes for hover routine xaxes: [xaxis], yaxes: [yaxis], doneFnCompleted: function(selection) { + var traceNow = gd._fullData[index]; var newGroups; - var oldGroups = gd._fullData[i].node.groups.slice(); + var oldGroups = traceNow.node.groups.slice(); var newGroup = []; function findNode(pt) { - return gd._fullData[i]._sankey.graph.nodes.find(function(n) { - return n.pointNumber === pt; - }); + var nodes = traceNow._sankey.graph.nodes; + for(var i = 0; i < nodes.length; i++) { + if(nodes[i].pointNumber === pt) return nodes[i]; + } } for(var j = 0; j < selection.length; j++) { @@ -109,25 +111,25 @@ function subplotUpdateFx(gd, index) { newGroup.push(node.childrenNodes[k].pointNumber); } // Flag group for removal from existing list of groups - oldGroups[node.pointNumber - fullData.node._count] = false; + oldGroups[node.pointNumber - traceNow.node._count] = false; } else { newGroup.push(node.pointNumber); } } newGroups = oldGroups - .filter(function(g) { return g; }) + .filter(Boolean) .concat([newGroup]); Registry.call('_guiRestyle', gd, { 'node.groups': [ newGroups ] - }, i).catch(Lib.noop); // TODO will this ever fail? + }, index); } }; - dragOptions[i].prepFn = function(e, startX, startY) { - prepSelect(e, startX, startY, dragOptions[i], dragMode); + dragOptions.prepFn = function(e, startX, startY) { + prepSelect(e, startX, startY, dragOptions, dragMode); }; - dragElement.init(dragOptions[i]); + dragElement.init(dragOptions); } diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 302b40ad946..15d770010f2 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -838,15 +838,15 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { sankey.each(function(d, i) { gd._fullData[i]._sankey = d; - // Draw dragbox - Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bg-' + i, function(el) { + Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bgsankey-' + d.guid, function(el) { el .style('pointer-events', 'all') .attr('width', d.width) .attr('height', d.height) .attr('x', d.translateX) .attr('y', d.translateY) + .classed('bgsankey', true) .style({fill: 'transparent', 'stroke-width': 0}); gd._fullData[i]._bgRect = el; diff --git a/src/traces/sankey/select.js b/src/traces/sankey/select.js index 87fa69a253e..f1f6d3beb11 100644 --- a/src/traces/sankey/select.js +++ b/src/traces/sankey/select.js @@ -19,13 +19,13 @@ module.exports = function selectPoints(searchInfo, selectionTester) { var node = nodes[i]; if(node.partOfGroup) continue; // Those are invisible - // TODO: decide on selection criteria, using centroid for now + // Position of node's centroid var pos = [(node.x0 + node.x1) / 2, (node.y0 + node.y1) / 2]; // Swap x and y if trace is vertical if(fullData.orientation === 'v') pos.reverse(); - if(selectionTester.contains(pos, false, i, searchInfo)) { + if(selectionTester && selectionTester.contains(pos, false, i, searchInfo)) { selection.push({ pointNumber: node.pointNumber // TODO: add eventData diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index 5780a72cdec..5cfff820396 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -379,6 +379,20 @@ describe('sankey tests', function() { }); }); + it('Plotly.deleteTraces removes draggers', function(done) { + var mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy) + .then(function() { + expect(document.getElementsByClassName('bgsankey').length).toBe(1); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(document.getElementsByClassName('bgsankey').length).toBe(0); + }) + .catch(failTest) + .then(done); + }); + it('Plotly.plot does not show Sankey if \'visible\' is false', function(done) { var mockCopy = Lib.extendDeep({}, mock); From aa17f1b6b32091fc171e97dfad4e3e18ee331421 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Tue, 2 Apr 2019 17:52:33 -0400 Subject: [PATCH 3/7] sankey: test to :lock: down grouping via mouse selection --- src/traces/sankey/base_plot.js | 2 ++ test/jasmine/tests/select_test.js | 41 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index 4b54cf45e41..28b81327bbf 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -57,6 +57,8 @@ function subplotUpdateFx(gd, index) { var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; var bgRect = trace._bgRect; + if(dragMode === 'pan' || dragMode === 'zoom') return; + setCursor(bgRect, cursor); var xaxis = { diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 3b4977b252b..efc645c490c 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -2666,6 +2666,47 @@ describe('Test select box and lasso per trace:', function() { .catch(failTest) .then(done); }); + + it('@flaky should work on sankey traces', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/sankey_circular.json')); + fig.layout.dragmode = 'select'; + var dblClickPos = [250, 400]; + var opts = {}; + + Plotly.plot(gd, fig) + .then(function() { + // No groups initially + expect(gd._fullData[0].node.groups).toEqual([]); + + opts.element = document.elementFromPoint(400, 400); + }) + .then(function() { + // Grouping the two nodes on the top right + return _run( + [[640, 130], [400, 450]], + function() { + expect(gd._fullData[0].node.groups).toEqual([[2, 3]]); + }, + dblClickPos, BOXEVENTS, 'for top right nodes #2 and #3' + ); + }) + .then(function() { + // Grouping node #4 and the previous group + drag([[715, 400], [300, 110]], opts); + }) + .then(function() { + expect(gd._fullData[0].node.groups).toEqual([[4, 3, 2]]); + }) + .then(function() { + // Grouping node #0 and #1 on the left side + drag([[160, 110], [200, 590]], opts); + }) + .then(function() { + expect(gd._fullData[0].node.groups).toEqual([[4, 3, 2], [0, 1]]); + }) + .catch(failTest) + .then(done); + }); }); describe('Test that selections persist:', function() { From 98541b066a9030b0db99a61d7698ed1fdbb25319 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 4 Apr 2019 12:33:16 -0400 Subject: [PATCH 4/7] sankey: create a single dragger per trace that gets restyle, improve test --- src/traces/sankey/render.js | 26 +++++---- test/jasmine/tests/select_test.js | 93 +++++++++++++++++++------------ 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 15d770010f2..312e9064fdb 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -838,19 +838,20 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { sankey.each(function(d, i) { gd._fullData[i]._sankey = d; - // Draw dragbox - Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bgsankey-' + d.guid, function(el) { - el - .style('pointer-events', 'all') - .attr('width', d.width) - .attr('height', d.height) - .attr('x', d.translateX) - .attr('y', d.translateY) - .classed('bgsankey', true) - .style({fill: 'transparent', 'stroke-width': 0}); - + // Create dragbox if missing + Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bgsankey-' + d.trace.uid, function(el) { gd._fullData[i]._bgRect = el; }); + + // Style dragbox + gd._fullData[i]._bgRect + .style('pointer-events', 'all') + .attr('width', d.width) + .attr('height', d.height) + .attr('x', d.translateX) + .attr('y', d.translateY) + .classed('bgsankey', true) + .style({fill: 'transparent', 'stroke-width': 0}); }); sankey.transition() @@ -942,7 +943,8 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { .call(attachPointerEvents, sankey, callbacks.nodeEvents) .call(attachDragHandler, sankeyLink, callbacks, gd); // has to be here as it binds sankeyLink - sankeyNode.transition() + sankeyNode + .transition() .ease(c.ease).duration(c.duration) .call(updateNodePositions) .style('opacity', function(n) { return n.partOfGroup ? 0 : 1;}); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index efc645c490c..2b015e00a42 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -13,6 +13,8 @@ var mouseEvent = require('../assets/mouse_event'); var touchEvent = require('../assets/touch_event'); var LONG_TIMEOUT_INTERVAL = 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL; +var delay = require('../assets/delay'); +var sankeyConstants = require('@src/traces/sankey/constants'); function drag(path, options) { var len = path.length; @@ -2667,45 +2669,62 @@ describe('Test select box and lasso per trace:', function() { .then(done); }); - it('@flaky should work on sankey traces', function(done) { - var fig = Lib.extendDeep({}, require('@mocks/sankey_circular.json')); - fig.layout.dragmode = 'select'; - var dblClickPos = [250, 400]; - var opts = {}; + describe('should work on sankey traces', function() { + var waitingTime = sankeyConstants.duration * 2; - Plotly.plot(gd, fig) - .then(function() { - // No groups initially - expect(gd._fullData[0].node.groups).toEqual([]); + it('@flaky select', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/sankey_circular.json')); + fig.layout.dragmode = 'select'; + var dblClickPos = [250, 400]; - opts.element = document.elementFromPoint(400, 400); - }) - .then(function() { - // Grouping the two nodes on the top right - return _run( - [[640, 130], [400, 450]], - function() { - expect(gd._fullData[0].node.groups).toEqual([[2, 3]]); - }, - dblClickPos, BOXEVENTS, 'for top right nodes #2 and #3' - ); - }) - .then(function() { - // Grouping node #4 and the previous group - drag([[715, 400], [300, 110]], opts); - }) - .then(function() { - expect(gd._fullData[0].node.groups).toEqual([[4, 3, 2]]); - }) - .then(function() { - // Grouping node #0 and #1 on the left side - drag([[160, 110], [200, 590]], opts); - }) - .then(function() { - expect(gd._fullData[0].node.groups).toEqual([[4, 3, 2], [0, 1]]); - }) - .catch(failTest) - .then(done); + Plotly.plot(gd, fig) + .then(function() { + // No groups initially + expect(gd._fullData[0].node.groups).toEqual([]); + }) + .then(function() { + // Grouping the two nodes on the top right + return _run( + [[640, 130], [400, 450]], + function() { + expect(gd._fullData[0].node.groups).toEqual([[2, 3]], 'failed to group #2 + #3'); + }, + dblClickPos, BOXEVENTS, 'for top right nodes #2 and #3' + ); + }) + .then(delay(waitingTime)) + .then(function() { + // Grouping node #4 and the previous group + drag([[715, 400], [300, 110]]); + }) + .then(delay(waitingTime)) + .then(function() { + expect(gd._fullData[0].node.groups).toEqual([[4, 3, 2]], 'failed to group #4 + existing group of #2 and #3'); + }) + .catch(failTest) + .then(done); + }); + + it('@flaky should not work when dragmode is undefined', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/sankey_circular.json')); + fig.layout.dragmode = undefined; + + Plotly.plot(gd, fig) + .then(function() { + // No groups initially + expect(gd._fullData[0].node.groups).toEqual([]); + }) + .then(function() { + // Grouping the two nodes on the top right + drag([[640, 130], [400, 450]]); + }) + .then(delay(waitingTime)) + .then(function() { + expect(gd._fullData[0].node.groups).toEqual([]); + }) + .catch(failTest) + .then(done); + }); }); }); From a25c1ad4d496a005335b75e6f696bd3a4412659c Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 4 Apr 2019 14:08:50 -0400 Subject: [PATCH 5/7] sankey: in fullData add a reference to existing bgRect on redraw --- src/traces/sankey/render.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 312e9064fdb..b2f48f867e2 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -839,9 +839,10 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { sankey.each(function(d, i) { gd._fullData[i]._sankey = d; // Create dragbox if missing - Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bgsankey-' + d.trace.uid, function(el) { - gd._fullData[i]._bgRect = el; - }); + var dragboxClassName = 'bgsankey-' + d.trace.uid + '-' + i; + Lib.ensureSingle(gd._fullLayout._draggers, 'rect', dragboxClassName); + + gd._fullData[i]._bgRect = d3.select('.' + dragboxClassName); // Style dragbox gd._fullData[i]._bgRect From 506da4c24bd6296d4ed40e3f1f10705371ea7b8f Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Mon, 8 Apr 2019 15:36:35 -0400 Subject: [PATCH 6/7] sankey: reset to initial view (x, y and groups) --- src/components/modebar/buttons.js | 15 ++++++++++++--- src/traces/sankey/plot.js | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3753d9924c6..849fd0b5ffb 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -518,9 +518,18 @@ modeBarButtons.resetSankeyGroup = { title: function(gd) { return _(gd, 'Ungroup all nodes'); }, icon: Icons.home, click: function(gd) { - Registry.call('restyle', gd, { - 'node.groups': [[]], - }); + var aObj = { + 'node.groups': [], + 'node.x': [], + 'node.y': [] + }; + for(var i = 0; i < gd._fullData.length; i++) { + var viewInitial = gd._fullData[i]._viewInitial; + aObj['node.groups'].push(viewInitial.node.groups.slice()); + aObj['node.x'].push(viewInitial.node.x.slice()); + aObj['node.y'].push(viewInitial.node.y.slice()); + } + Registry.call('restyle', gd, aObj); } }; diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index c10e0866851..1ba14ee4acb 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -127,6 +127,20 @@ module.exports = function plot(gd, calcData) { var svg = fullLayout._paper; var size = fullLayout._size; + // stash initial view + for(var i = 0; i < calcData.length; i++) { + if(!gd._fullData[i]._viewInitial) { + var node = gd._fullData[i].node; + gd._fullData[i]._viewInitial = { + node: { + groups: node.groups.slice(), + x: node.x.slice(), + y: node.y.slice() + } + }; + } + } + var linkSelect = function(element, d) { var evt = d.link; evt.originalEvent = d3.event; From f8a40731db4c0366c95e3440bfa0e4ad816c457b Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Mon, 8 Apr 2019 16:58:20 -0400 Subject: [PATCH 7/7] sankey: test to :lock: down modebar button that resets view --- test/jasmine/tests/sankey_test.js | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index 50dd74b1edd..4e3ae1463e1 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -23,6 +23,7 @@ var defaultColors = require('@src/components/color/attributes').defaults; var drag = require('../assets/drag'); var checkOverlap = require('../assets/check_overlap'); var delay = require('../assets/delay'); +var selectButton = require('../assets/modebar_button'); describe('sankey tests', function() { 'use strict'; @@ -560,6 +561,52 @@ describe('sankey tests', function() { .catch(failTest) .then(done); }); + + it('resets each subplot to its initial view (ie. x, y groups) via modebar button', function(done) { + var mockCopy = Lib.extendDeep({}, require('@mocks/sankey_subplots_circular')); + + // Set initial view + mockCopy.data[0].node.x = [0.25]; + mockCopy.data[0].node.y = [0.25]; + + mockCopy.data[0].node.groups = []; + mockCopy.data[1].node.groups = [[2, 3]]; + + Plotly.plot(gd, mockCopy) + .then(function() { + expect(gd._fullData[0].node.groups).toEqual([]); + expect(gd._fullData[1].node.groups).toEqual([[2, 3]]); + + // Change groups + return Plotly.restyle(gd, { + 'node.groups': [[[1, 2]], [[]]], + 'node.x': [[0.1]], + 'node.y': [[0.1]] + }); + }) + .then(function() { + // Check current state + expect(gd._fullData[0].node.x).toEqual([0.1]); + expect(gd._fullData[0].node.y).toEqual([0.1]); + + expect(gd._fullData[0].node.groups).toEqual([[1, 2]]); + expect(gd._fullData[1].node.groups).toEqual([[]]); + + // Click reset + var resetButton = selectButton(gd._fullLayout._modeBar, 'resetViewSankey'); + resetButton.click(); + }) + .then(function() { + // Check we are back to initial view + expect(gd._fullData[0].node.x).toEqual([0.25]); + expect(gd._fullData[0].node.y).toEqual([0.25]); + + expect(gd._fullData[0].node.groups).toEqual([]); + expect(gd._fullData[1].node.groups).toEqual([[2, 3]]); + }) + .catch(failTest) + .then(done); + }); }); describe('Test hover/click interactions:', function() {