Skip to content

Commit d5f3cd5

Browse files
committed
sankey: grouping with animations
1 parent 25fa0c2 commit d5f3cd5

File tree

9 files changed

+318
-48
lines changed

9 files changed

+318
-48
lines changed

src/snapshot/helpers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports.getDelay = function(fullLayout) {
1515
return (
1616
fullLayout._has('gl3d') ||
1717
fullLayout._has('gl2d') ||
18+
fullLayout._has('sankey') ||
1819
fullLayout._has('mapbox')
1920
) ? 500 : 0;
2021
};

src/traces/sankey/attributes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ var attrs = module.exports = overrideAll({
8989
role: 'info',
9090
description: 'The shown name of the node.'
9191
},
92+
groups: {
93+
valType: 'data_array',
94+
dflt: [],
95+
role: 'calc',
96+
description: [
97+
'Groups of nodes.',
98+
'Each group is defined by an array with the indices of the nodes it contains.',
99+
'Multiple groups can be specified.'
100+
].join(' ')
101+
},
92102
color: {
93103
valType: 'color',
94104
role: 'style',

src/traces/sankey/calc.js

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ var isIndex = Lib.isIndex;
1717
var Colorscale = require('../../components/colorscale');
1818

1919
function convertToD3Sankey(trace) {
20-
var nodeSpec = trace.node;
21-
var linkSpec = trace.link;
20+
// var nodeSpec = trace.node;
21+
// var linkSpec = trace.link;
22+
var nodeSpec = Lib.extendDeep({}, trace.node);
23+
var linkSpec = Lib.extendDeep({}, trace.link);
2224

2325
var links = [];
2426
var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color);
@@ -34,7 +36,32 @@ function convertToD3Sankey(trace) {
3436
components[cscale.label] = scale;
3537
}
3638

37-
var nodeCount = nodeSpec.label.length;
39+
var maxNodeId = 0;
40+
for(i = 0; i < linkSpec.value.length; i++) {
41+
if(linkSpec.source[i] > maxNodeId) maxNodeId = linkSpec.source[i];
42+
if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i];
43+
}
44+
var nodeCount = maxNodeId + 1;
45+
46+
// Group nodes
47+
var j;
48+
var groups = trace.node.groups;
49+
var groupLookup = {};
50+
for(i = 0; i < groups.length; i++) {
51+
var group = groups[i];
52+
// Build a lookup table to quickly find in which group a node is
53+
if(Array.isArray(group)) {
54+
for(j = 0; j < group.length; j++) {
55+
var nodeIndex = group[j];
56+
var groupIndex = nodeCount + i;
57+
groupLookup[nodeIndex] = groupIndex;
58+
}
59+
} else {
60+
Lib.warn('node.groups must be an array, default to empty array []');
61+
}
62+
}
63+
64+
// Process links
3865
for(i = 0; i < linkSpec.value.length; i++) {
3966
var val = linkSpec.value[i];
4067
// remove negative values, but keep zeros with special treatment
@@ -44,6 +71,22 @@ function convertToD3Sankey(trace) {
4471
continue;
4572
}
4673

74+
// Remove links that are within the same group
75+
if(groupLookup.hasOwnProperty(source) && groupLookup.hasOwnProperty(target) && groupLookup[source] === groupLookup[target]) {
76+
continue;
77+
}
78+
79+
// if link targets a node in the group, relink target to that group
80+
if(groupLookup.hasOwnProperty(target)) {
81+
target = groupLookup[target];
82+
}
83+
84+
// if link originates from a node in a group, relink source to that group
85+
// if(group.indexOf(source) !== -1) {
86+
if(groupLookup.hasOwnProperty(source)) {
87+
source = groupLookup[source];
88+
}
89+
4790
source = +source;
4891
target = +target;
4992
linkedNodes[source] = linkedNodes[target] = true;
@@ -65,34 +108,29 @@ function convertToD3Sankey(trace) {
65108
});
66109
}
67110

111+
// Process nodes
112+
var totalCount = nodeCount + groups.length;
68113
var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color);
69114
var nodes = [];
70-
var removedNodes = false;
71-
var nodeIndices = {};
72-
73-
for(i = 0; i < nodeCount; i++) {
74-
if(linkedNodes[i]) {
75-
var l = nodeSpec.label[i];
76-
nodeIndices[i] = nodes.length;
77-
nodes.push({
78-
pointNumber: i,
79-
label: l,
80-
color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color
81-
});
82-
} else removedNodes = true;
83-
}
115+
for(i = 0; i < totalCount; i++) {
116+
if(!linkedNodes[i]) continue;
117+
var l = nodeSpec.label[i];
84118

85-
// need to re-index links now, since we didn't put all the nodes in
86-
if(removedNodes) {
87-
for(i = 0; i < links.length; i++) {
88-
links[i].source = nodeIndices[links[i].source];
89-
links[i].target = nodeIndices[links[i].target];
90-
}
119+
nodes.push({
120+
group: (i > nodeCount - 1),
121+
pointNumber: i,
122+
label: l,
123+
color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color
124+
});
91125
}
92126

93127
return {
94128
links: links,
95-
nodes: nodes
129+
nodes: nodes,
130+
131+
// Data structure for groups
132+
groups: groups,
133+
groupLookup: groupLookup
96134
};
97135
}
98136

@@ -130,6 +168,10 @@ module.exports = function calc(gd, trace) {
130168
return wrap({
131169
circular: circular,
132170
_nodes: result.nodes,
133-
_links: result.links
171+
_links: result.links,
172+
173+
// Data structure for grouping
174+
_groups: result.groups,
175+
_groupLookup: result.groupLookup,
134176
});
135177
};

src/traces/sankey/constants.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ module.exports = {
1515
sankeyIterations: 50,
1616
forceIterations: 5,
1717
forceTicksPerFrame: 10,
18-
duration: 500,
19-
ease: 'cubic-in-out',
18+
duration: 350,
19+
ease: 'quart-in-out',
2020
cn: {
2121
sankey: 'sankey',
2222
sankeyLinks: 'sankey-links',

src/traces/sankey/defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3232
return Lib.coerce(nodeIn, nodeOut, attributes.node, attr, dflt);
3333
}
3434
coerceNode('label');
35+
coerceNode('groups');
3536
coerceNode('pad');
3637
coerceNode('thickness');
3738
coerceNode('line.color');

src/traces/sankey/render.js

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ function sankeyModel(layout, d, traceIndex) {
4545
if(circular) {
4646
sankey = d3SankeyCircular
4747
.sankeyCircular()
48-
.circularLinkGap(0)
49-
.nodeId(function(d) {
50-
return d.pointNumber;
51-
});
48+
.circularLinkGap(0);
5249
} else {
5350
sankey = d3Sankey.sankey();
5451
}
@@ -58,6 +55,9 @@ function sankeyModel(layout, d, traceIndex) {
5855
.size(horizontal ? [width, height] : [height, width])
5956
.nodeWidth(nodeThickness)
6057
.nodePadding(nodePad)
58+
.nodeId(function(d) {
59+
return d.pointNumber;
60+
})
6161
.nodes(nodes)
6262
.links(links);
6363

@@ -67,6 +67,30 @@ function sankeyModel(layout, d, traceIndex) {
6767
Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.');
6868
}
6969

70+
// Create transient nodes for animations
71+
Object.keys(calcData._groupLookup).forEach(function(nodePointNumber) {
72+
var groupIndex = parseInt(calcData._groupLookup[nodePointNumber]);
73+
74+
var groupingNode;
75+
for(var i = 0; i < graph.nodes.length; i++) {
76+
if(graph.nodes[i].pointNumber === groupIndex) {
77+
groupingNode = graph.nodes[i];
78+
break;
79+
}
80+
}
81+
82+
graph.nodes.push({
83+
pointNumber: parseInt(nodePointNumber),
84+
x0: groupingNode.x0,
85+
x1: groupingNode.x1,
86+
y0: groupingNode.y0,
87+
y1: groupingNode.y1,
88+
partOfGroup: true,
89+
sourceLinks: [],
90+
targetLinks: []
91+
});
92+
});
93+
7094
function computeLinkConcentrations() {
7195
var i, j, k;
7296
for(i = 0; i < graph.nodes.length; i++) {
@@ -343,7 +367,7 @@ function linkPath() {
343367
return path;
344368
}
345369

346-
function nodeModel(d, n, i) {
370+
function nodeModel(d, n) {
347371
var tc = tinycolor(n.color);
348372
var zoneThicknessPad = c.nodePadAcross;
349373
var zoneLengthPad = d.nodePad / 2;
@@ -352,8 +376,11 @@ function nodeModel(d, n, i) {
352376
var visibleThickness = n.dx;
353377
var visibleLength = Math.max(0.5, n.dy);
354378

355-
var basicKey = n.label;
356-
var key = basicKey + '__' + i;
379+
var key = 'node_' + n.pointNumber;
380+
// If it's a group, it's mutable and should be unique
381+
if(n.group) {
382+
key = 'group_' + Math.floor(1e12 * (1 + Math.random()));
383+
}
357384

358385
// for event data
359386
n.trace = d.trace;
@@ -362,6 +389,8 @@ function nodeModel(d, n, i) {
362389
return {
363390
index: n.pointNumber,
364391
key: key,
392+
partOfGroup: n.partOfGroup || false,
393+
group: n.group,
365394
traceId: d.key,
366395
node: n,
367396
nodePad: d.nodePad,
@@ -540,7 +569,10 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
540569
function attachForce(sankeyNode, forceKey, d) {
541570
// Attach force to nodes in the same column (same x coordinate)
542571
switchToForceFormat(d.graph.nodes);
543-
var nodes = d.graph.nodes.filter(function(n) {return n.originalX === d.node.originalX;});
572+
var nodes = d.graph.nodes
573+
.filter(function(n) {return n.originalX === d.node.originalX;})
574+
// Filter out children
575+
.filter(function(n) {return !n.partOfGroup;});
544576
d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes)
545577
.alphaDecay(0)
546578
.force('collide', d3Force.forceCollide()
@@ -683,7 +715,6 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
683715
sankeyLink
684716
.enter().append('path')
685717
.classed(c.cn.sankeyLink, true)
686-
.attr('d', linkPath())
687718
.call(attachPointerEvents, sankey, callbacks.linkEvents);
688719

689720
sankeyLink
@@ -701,13 +732,17 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
701732
})
702733
.style('stroke-width', function(d) {
703734
return salientEnough(d) ? d.linkLineWidth : 1;
704-
});
735+
})
736+
.attr('d', linkPath());
705737

706-
sankeyLink.transition()
707-
.ease(c.ease).duration(c.duration)
708-
.attr('d', linkPath());
738+
sankeyLink
739+
.style('opacity', 0)
740+
.transition()
741+
.ease(c.ease).duration(c.duration)
742+
.style('opacity', 1);
709743

710-
sankeyLink.exit().transition()
744+
sankeyLink.exit()
745+
.transition()
711746
.ease(c.ease).duration(c.duration)
712747
.style('opacity', 0)
713748
.remove();
@@ -733,24 +768,26 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
733768
var nodes = d.graph.nodes;
734769
persistOriginalPlace(nodes);
735770
return nodes
736-
.filter(function(n) {return n.value;})
737-
.map(nodeModel.bind(null, d));
771+
.map(nodeModel.bind(null, d));
738772
}, keyFun);
739773

740774
sankeyNode.enter()
741775
.append('g')
742776
.classed(c.cn.sankeyNode, true)
743777
.call(updateNodePositions)
744-
.call(attachPointerEvents, sankey, callbacks.nodeEvents);
778+
.style('opacity', 0);
745779

746780
sankeyNode
781+
.call(attachPointerEvents, sankey, callbacks.nodeEvents)
747782
.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink
748783

749784
sankeyNode.transition()
750785
.ease(c.ease).duration(c.duration)
751-
.call(updateNodePositions);
786+
.call(updateNodePositions)
787+
.style('opacity', function(n) { return n.partOfGroup ? 0 : 1;});
752788

753-
sankeyNode.exit().transition()
789+
sankeyNode.exit()
790+
.transition()
754791
.ease(c.ease).duration(c.duration)
755792
.style('opacity', 0)
756793
.remove();
44.9 KB
Loading

0 commit comments

Comments
 (0)