Skip to content

Commit 627fd74

Browse files
committed
sankey: implement node grouping via mouse selection
1 parent c657348 commit 627fd74

File tree

8 files changed

+176
-0
lines changed

8 files changed

+176
-0
lines changed

Diff for: src/components/modebar/buttons.js

+11
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,17 @@ function toggleHover(gd) {
513513
Registry.call('_guiRelayout', gd, 'hovermode', newHover);
514514
}
515515

516+
modeBarButtons.resetSankeyGroup = {
517+
name: 'resetSankeyGroup',
518+
title: function(gd) { return _(gd, 'Ungroup all nodes'); },
519+
icon: Icons.home,
520+
click: function(gd) {
521+
Registry.call('restyle', gd, {
522+
'node.groups': [[]],
523+
});
524+
}
525+
};
526+
516527
// buttons when more then one plot types are present
517528

518529
modeBarButtons.toggleHover = {

Diff for: src/components/modebar/manage.js

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
8686
var hasTernary = fullLayout._has('ternary');
8787
var hasMapbox = fullLayout._has('mapbox');
8888
var hasPolar = fullLayout._has('polar');
89+
var hasSankey = fullLayout._has('sankey');
8990
var allAxesFixed = areAllAxesFixed(fullLayout);
9091

9192
var groups = [];
@@ -139,6 +140,9 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
139140
else if(hasPie) {
140141
hoverGroup = ['hoverClosestPie'];
141142
}
143+
else if(hasSankey) {
144+
hoverGroup = ['resetSankeyGroup'];
145+
}
142146
else { // hasPolar, hasTernary
143147
// always show at least one hover icon.
144148
hoverGroup = ['toggleHover'];

Diff for: src/plots/cartesian/select.js

+9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
4848
var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes);
4949
var subtract = e.altKey;
5050

51+
var doneFnCompleted = dragOptions.doneFnCompleted;
52+
5153
var filterPoly, selectionTester, mergedPolygons, currentPolygon;
5254
var i, searchInfo, eventData;
5355

@@ -284,6 +286,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
284286
dragOptions.mergedPolygons.length = 0;
285287
[].push.apply(dragOptions.mergedPolygons, mergedPolygons);
286288
}
289+
290+
doneFnCompleted(selection);
287291
});
288292
};
289293
}
@@ -519,6 +523,11 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) {
519523
var info = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]);
520524
info.scene = gd._fullLayout._splomScenes[trace.uid];
521525
searchTraces.push(info);
526+
} else if(
527+
trace.type === 'sankey'
528+
) {
529+
var sankeyInfo = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]);
530+
searchTraces.push(sankeyInfo);
522531
} else {
523532
if(xAxisIds.indexOf(trace.xaxis) === -1) continue;
524533
if(yAxisIds.indexOf(trace.yaxis) === -1) continue;

Diff for: src/traces/sankey/base_plot.js

+97
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
1313
var plot = require('./plot');
1414
var fxAttrs = require('../../components/fx/layout_attributes');
1515

16+
var setCursor = require('../../lib/setcursor');
17+
var dragElement = require('../../components/dragelement');
18+
var prepSelect = require('../../plots/cartesian/select').prepSelect;
19+
var Lib = require('../../lib');
20+
var Registry = require('../../registry');
21+
1622
var SANKEY = 'sankey';
1723

1824
exports.name = SANKEY;
@@ -34,3 +40,94 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
3440
oldFullLayout._paperdiv.selectAll('.sankey').remove();
3541
}
3642
};
43+
44+
exports.updateFx = function(gd) {
45+
for(var i = 0; i < gd._fullData.length; i++) {
46+
subplotUpdateFx(gd, i);
47+
}
48+
};
49+
50+
var dragOptions = [];
51+
function subplotUpdateFx(gd, index) {
52+
var i = index;
53+
var fullData = gd._fullData[i];
54+
var fullLayout = gd._fullLayout;
55+
56+
var dragMode = fullLayout.dragmode;
57+
var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair';
58+
var bgRect = fullData._bgRect;
59+
60+
setCursor(bgRect, cursor);
61+
62+
var xaxis = {
63+
_id: 'x',
64+
c2p: function(v) { return v; },
65+
_offset: fullData._sankey.translateX,
66+
_length: fullData._sankey.width
67+
};
68+
var yaxis = {
69+
_id: 'y',
70+
c2p: function(v) { return v; },
71+
_offset: fullData._sankey.translateY,
72+
_length: fullData._sankey.height
73+
};
74+
75+
// Note: dragOptions is needed to be declared for all dragmodes because
76+
// it's the object that holds persistent selection state.
77+
dragOptions[i] = {
78+
gd: gd,
79+
element: bgRect.node(),
80+
plotinfo: {
81+
id: i,
82+
xaxis: xaxis,
83+
yaxis: yaxis,
84+
fillRangeItems: Lib.noop
85+
},
86+
subplot: i,
87+
// create mock x/y axes for hover routine
88+
xaxes: [xaxis],
89+
yaxes: [yaxis],
90+
doneFnCompleted: function(selection) {
91+
var newGroups;
92+
var oldGroups = gd._fullData[i].node.groups.slice();
93+
var newGroup = [];
94+
95+
function findNode(pt) {
96+
return gd._fullData[i]._sankey.graph.nodes.find(function(n) {
97+
return n.pointNumber === pt;
98+
});
99+
}
100+
101+
for(var j = 0; j < selection.length; j++) {
102+
var node = findNode(selection[j].pointNumber);
103+
if(!node) continue;
104+
105+
// If the node represents a group
106+
if(node.group) {
107+
// Add all its children to the current selection
108+
for(var k = 0; k < node.childrenNodes.length; k++) {
109+
newGroup.push(node.childrenNodes[k].pointNumber);
110+
}
111+
// Flag group for removal from existing list of groups
112+
oldGroups[node.pointNumber - fullData.node._count] = false;
113+
} else {
114+
newGroup.push(node.pointNumber);
115+
}
116+
}
117+
118+
newGroups = oldGroups
119+
.filter(function(g) { return g; })
120+
.concat([newGroup]);
121+
122+
Registry.call('_guiRestyle', gd, {
123+
'node.groups': [ newGroups ]
124+
}, i).catch(Lib.noop); // TODO will this ever fail?
125+
}
126+
};
127+
128+
dragOptions[i].prepFn = function(e, startX, startY) {
129+
prepSelect(e, startX, startY, dragOptions[i], dragMode);
130+
};
131+
132+
dragElement.init(dragOptions[i]);
133+
}

Diff for: src/traces/sankey/calc.js

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function convertToD3Sankey(trace) {
4040
if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i];
4141
}
4242
var nodeCount = maxNodeId + 1;
43+
trace.node._count = nodeCount;
4344

4445
// Group nodes
4546
var j;

Diff for: src/traces/sankey/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Plot.plot = require('./plot');
1818
Plot.moduleType = 'trace';
1919
Plot.name = 'sankey';
2020
Plot.basePlotModule = require('./base_plot');
21+
Plot.selectPoints = require('./select.js');
2122
Plot.categories = ['noOpacity'];
2223
Plot.meta = {
2324
description: [

Diff for: src/traces/sankey/render.js

+17
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,23 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
836836
.style('pointer-events', 'auto')
837837
.attr('transform', sankeyTransform);
838838

839+
sankey.each(function(d, i) {
840+
gd._fullData[i]._sankey = d;
841+
842+
// Draw dragbox
843+
Lib.ensureSingle(gd._fullLayout._draggers, 'rect', 'bg-' + i, function(el) {
844+
el
845+
.style('pointer-events', 'all')
846+
.attr('width', d.width)
847+
.attr('height', d.height)
848+
.attr('x', d.translateX)
849+
.attr('y', d.translateY)
850+
.style({fill: 'transparent', 'stroke-width': 0});
851+
852+
gd._fullData[i]._bgRect = el;
853+
});
854+
});
855+
839856
sankey.transition()
840857
.ease(c.ease).duration(c.duration)
841858
.attr('transform', sankeyTransform);

Diff for: src/traces/sankey/select.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = function selectPoints(searchInfo, selectionTester) {
12+
var cd = searchInfo.cd;
13+
var selection = [];
14+
var fullData = cd[0].trace;
15+
16+
var nodes = fullData._sankey.graph.nodes;
17+
18+
for(var i = 0; i < nodes.length; i++) {
19+
var node = nodes[i];
20+
if(node.partOfGroup) continue; // Those are invisible
21+
22+
// TODO: decide on selection criteria, using centroid for now
23+
var pos = [(node.x0 + node.x1) / 2, (node.y0 + node.y1) / 2];
24+
25+
// Swap x and y if trace is vertical
26+
if(fullData.orientation === 'v') pos.reverse();
27+
28+
if(selectionTester.contains(pos, false, i, searchInfo)) {
29+
selection.push({
30+
pointNumber: node.pointNumber
31+
// TODO: add eventData
32+
});
33+
}
34+
}
35+
return selection;
36+
};

0 commit comments

Comments
 (0)