Skip to content

Commit db54ced

Browse files
authored
Merge pull request #3556 from plotly/sankey2-grouping
Sankey: group nodes
2 parents 3e15f17 + fb9479a commit db54ced

File tree

8 files changed

+424
-63
lines changed

8 files changed

+424
-63
lines changed

src/traces/sankey/attributes.js

+13
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ var attrs = module.exports = overrideAll({
8989
role: 'info',
9090
description: 'The shown name of the node.'
9191
},
92+
groups: {
93+
valType: 'info_array',
94+
dimensions: 2,
95+
freeLength: true,
96+
dflt: [],
97+
items: {valType: 'number', editType: 'calc'},
98+
role: 'info',
99+
description: [
100+
'Groups of nodes.',
101+
'Each group is defined by an array with the indices of the nodes it contains.',
102+
'Multiple groups can be specified.'
103+
].join(' ')
104+
},
92105
color: {
93106
valType: 'color',
94107
role: 'style',

src/traces/sankey/calc.js

+78-31
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,36 @@ function convertToD3Sankey(trace) {
3434
components[cscale.label] = scale;
3535
}
3636

37-
var nodeCount = nodeSpec.label.length;
37+
var maxNodeId = 0;
38+
for(i = 0; i < linkSpec.value.length; i++) {
39+
if(linkSpec.source[i] > maxNodeId) maxNodeId = linkSpec.source[i];
40+
if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i];
41+
}
42+
var nodeCount = maxNodeId + 1;
43+
44+
// Group nodes
45+
var j;
46+
var groups = trace.node.groups;
47+
var groupLookup = {};
48+
for(i = 0; i < groups.length; i++) {
49+
var group = groups[i];
50+
// Build a lookup table to quickly find in which group a node is
51+
for(j = 0; j < group.length; j++) {
52+
var nodeIndex = group[j];
53+
var groupIndex = nodeCount + i;
54+
if(groupLookup.hasOwnProperty(nodeIndex)) {
55+
Lib.warn('Node ' + nodeIndex + ' is already part of a group.');
56+
} else {
57+
groupLookup[nodeIndex] = groupIndex;
58+
}
59+
}
60+
}
61+
62+
// Process links
63+
var groupedLinks = {
64+
source: [],
65+
target: []
66+
};
3867
for(i = 0; i < linkSpec.value.length; i++) {
3968
var val = linkSpec.value[i];
4069
// remove negative values, but keep zeros with special treatment
@@ -44,6 +73,21 @@ function convertToD3Sankey(trace) {
4473
continue;
4574
}
4675

76+
// Remove links that are within the same group
77+
if(groupLookup.hasOwnProperty(source) && groupLookup.hasOwnProperty(target) && groupLookup[source] === groupLookup[target]) {
78+
continue;
79+
}
80+
81+
// if link targets a node in the group, relink target to that group
82+
if(groupLookup.hasOwnProperty(target)) {
83+
target = groupLookup[target];
84+
}
85+
86+
// if link originates from a node in a group, relink source to that group
87+
if(groupLookup.hasOwnProperty(source)) {
88+
source = groupLookup[source];
89+
}
90+
4791
source = +source;
4892
target = +target;
4993
linkedNodes[source] = linkedNodes[target] = true;
@@ -63,42 +107,46 @@ function convertToD3Sankey(trace) {
63107
target: target,
64108
value: +val
65109
});
110+
111+
groupedLinks.source.push(source);
112+
groupedLinks.target.push(target);
66113
}
67114

115+
// Process nodes
116+
var totalCount = nodeCount + groups.length;
68117
var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color);
69118
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;
119+
for(i = 0; i < totalCount; i++) {
120+
if(!linkedNodes[i]) continue;
121+
var l = nodeSpec.label[i];
122+
123+
nodes.push({
124+
group: (i > nodeCount - 1),
125+
childrenNodes: [],
126+
pointNumber: i,
127+
label: l,
128+
color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color
129+
});
83130
}
84131

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-
}
132+
// Check if we have circularity on the resulting graph
133+
var circular = false;
134+
if(circularityPresent(totalCount, groupedLinks.source, groupedLinks.target)) {
135+
circular = true;
91136
}
92137

93138
return {
139+
circular: circular,
94140
links: links,
95-
nodes: nodes
141+
nodes: nodes,
142+
143+
// Data structure for groups
144+
groups: groups,
145+
groupLookup: groupLookup
96146
};
97147
}
98148

99-
function circularityPresent(nodeList, sources, targets) {
100-
101-
var nodeLen = nodeList.length;
149+
function circularityPresent(nodeLen, sources, targets) {
102150
var nodes = Lib.init2dArray(nodeLen, 0);
103151

104152
for(var i = 0; i < Math.min(sources.length, targets.length); i++) {
@@ -120,16 +168,15 @@ function circularityPresent(nodeList, sources, targets) {
120168
}
121169

122170
module.exports = function calc(gd, trace) {
123-
var circular = false;
124-
if(circularityPresent(trace.node.label, trace.link.source, trace.link.target)) {
125-
circular = true;
126-
}
127-
128171
var result = convertToD3Sankey(trace);
129172

130173
return wrap({
131-
circular: circular,
174+
circular: result.circular,
132175
_nodes: result.nodes,
133-
_links: result.links
176+
_links: result.links,
177+
178+
// Data structure for grouping
179+
_groups: result.groups,
180+
_groupLookup: result.groupLookup,
134181
});
135182
};

src/traces/sankey/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module.exports = {
1616
forceIterations: 5,
1717
forceTicksPerFrame: 10,
1818
duration: 500,
19-
ease: 'cubic-in-out',
19+
ease: 'linear',
2020
cn: {
2121
sankey: 'sankey',
2222
sankeyLinks: 'sankey-links',

src/traces/sankey/defaults.js

+1
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');

0 commit comments

Comments
 (0)