Skip to content

Commit 12097a2

Browse files
authored
Merge pull request #3501 from plotly/pr-sankey-link-concentration
Sankey: colorscales per component, linked to concentration
2 parents 0714286 + f628b54 commit 12097a2

File tree

8 files changed

+239
-10
lines changed

8 files changed

+239
-10
lines changed

src/traces/sankey/attributes.js

+33-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ var colorAttrs = require('../../components/color/attributes');
1414
var fxAttrs = require('../../components/fx/attributes');
1515
var domainAttrs = require('../../plots/domain').attributes;
1616
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
17+
var colorAttributes = require('../../components/colorscale/attributes');
18+
var templatedArray = require('../../plot_api/plot_template').templatedArray;
1719

1820
var extendFlat = require('../../lib/extend').extendFlat;
1921
var overrideAll = require('../../plot_api/edit_types').overrideAll;
@@ -225,7 +227,37 @@ var attrs = module.exports = overrideAll({
225227
description: 'Variables `source` and `target` are node objects.',
226228
keys: ['value', 'label']
227229
}),
228-
description: 'The links of the Sankey plot.'
230+
colorscales: templatedArray('concentrationscales', {
231+
editType: 'calc',
232+
label: {
233+
valType: 'string',
234+
role: 'info',
235+
editType: 'calc',
236+
description: 'The label of the links to color based on their concentration within a flow.',
237+
dflt: ''
238+
},
239+
cmax: {
240+
valType: 'number',
241+
role: 'info',
242+
editType: 'calc',
243+
dflt: 1,
244+
description: [
245+
'Sets the upper bound of the color domain.'
246+
].join('')
247+
},
248+
cmin: {
249+
valType: 'number',
250+
role: 'info',
251+
editType: 'calc',
252+
dflt: 0,
253+
description: [
254+
'Sets the lower bound of the color domain.'
255+
].join('')
256+
},
257+
colorscale: extendFlat(colorAttributes().colorscale, {dflt: [[0, 'white'], [1, 'black']]})
258+
}),
259+
description: 'The links of the Sankey plot.',
260+
role: 'info'
229261
}
230262
}, 'calc', 'nested');
231263
attrs.transforms = undefined;

src/traces/sankey/calc.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var wrap = require('../../lib/gup').wrap;
1414

1515
var isArrayOrTypedArray = Lib.isArrayOrTypedArray;
1616
var isIndex = Lib.isIndex;
17-
17+
var Colorscale = require('../../components/colorscale');
1818

1919
function convertToD3Sankey(trace) {
2020
var nodeSpec = trace.node;
@@ -24,8 +24,17 @@ function convertToD3Sankey(trace) {
2424
var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color);
2525
var linkedNodes = {};
2626

27-
var nodeCount = nodeSpec.label.length;
27+
var components = {};
28+
var componentCount = linkSpec.colorscales.length;
2829
var i;
30+
for(i = 0; i < componentCount; i++) {
31+
var cscale = linkSpec.colorscales[i];
32+
var specs = Colorscale.extractScale(cscale, {cLetter: 'c'});
33+
var scale = Colorscale.makeColorScaleFunc(specs);
34+
components[cscale.label] = scale;
35+
}
36+
37+
var nodeCount = nodeSpec.label.length;
2938
for(i = 0; i < linkSpec.value.length; i++) {
3039
var val = linkSpec.value[i];
3140
// remove negative values, but keep zeros with special treatment
@@ -42,10 +51,14 @@ function convertToD3Sankey(trace) {
4251
var label = '';
4352
if(linkSpec.label && linkSpec.label[i]) label = linkSpec.label[i];
4453

54+
var concentrationscale = null;
55+
if(label && components.hasOwnProperty(label)) concentrationscale = components[label];
56+
4557
links.push({
4658
pointNumber: i,
4759
label: label,
4860
color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color,
61+
concentrationscale: concentrationscale,
4962
source: source,
5063
target: target,
5164
value: +val

src/traces/sankey/defaults.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var tinycolor = require('tinycolor2');
1515
var handleDomainDefaults = require('../../plots/domain').defaults;
1616
var handleHoverLabelDefaults = require('../../components/fx/hoverlabel_defaults');
1717
var Template = require('../../plot_api/plot_template');
18+
var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
1819

1920
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
2021
function coerce(attr, dflt) {
@@ -48,7 +49,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
4849
}));
4950

5051
// link attributes
51-
var linkIn = traceIn.link;
52+
var linkIn = traceIn.link || {};
5253
var linkOut = Template.newContainer(traceOut, 'link');
5354

5455
function coerceLink(attr, dflt) {
@@ -70,6 +71,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7071

7172
coerceLink('color', Lib.repeat(defaultLinkColor, linkOut.value.length));
7273

74+
handleArrayContainerDefaults(linkIn, linkOut, {
75+
name: 'colorscales',
76+
handleItemDefaults: concentrationscalesDefaults
77+
});
78+
7379
handleDomainDefaults(traceOut, layout, coerce);
7480

7581
coerce('orientation');
@@ -83,3 +89,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
8389
// don't match, between nodes and links
8490
traceOut._length = null;
8591
};
92+
93+
function concentrationscalesDefaults(In, Out) {
94+
function coerce(attr, dflt) {
95+
return Lib.coerce(In, Out, attributes.link.colorscales, attr, dflt);
96+
}
97+
98+
coerce('label');
99+
coerce('cmin');
100+
coerce('cmax');
101+
coerce('colorscale');
102+
}

src/traces/sankey/plot.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,21 @@ function linkHoveredStyle(d, sankey, visitNodes, sankeyLink) {
7272

7373
var label = sankeyLink.datum().link.label;
7474

75-
sankeyLink.style('fill-opacity', 0.4);
75+
sankeyLink.style('fill-opacity', function(l) {
76+
if(!l.link.concentrationscale) {
77+
return 0.4;
78+
}
79+
});
7680

7781
if(label) {
7882
ownTrace(sankey, d)
7983
.selectAll('.' + cn.sankeyLink)
8084
.filter(function(l) {return l.link.label === label;})
81-
.style('fill-opacity', 0.4);
85+
.style('fill-opacity', function(l) {
86+
if(!l.link.concentrationscale) {
87+
return 0.4;
88+
}
89+
});
8290
}
8391

8492
if(visitNodes) {
@@ -143,6 +151,7 @@ module.exports = function plot(gd, calcData) {
143151

144152
var sourceLabel = _(gd, 'source:') + ' ';
145153
var targetLabel = _(gd, 'target:') + ' ';
154+
var concentrationLabel = _(gd, 'concentration:') + ' ';
146155
var incomingLabel = _(gd, 'incoming flow count:') + ' ';
147156
var outgoingLabel = _(gd, 'outgoing flow count:') + ' ';
148157

@@ -172,7 +181,8 @@ module.exports = function plot(gd, calcData) {
172181
text: [
173182
d.link.label || '',
174183
sourceLabel + d.link.source.label,
175-
targetLabel + d.link.target.label
184+
targetLabel + d.link.target.label,
185+
d.link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(d.link.flow.labelConcentration) : ''
176186
].filter(renderableValuePresent).join('<br>'),
177187
color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
178188
borderColor: castHoverOption(obj, 'bordercolor'),
@@ -190,7 +200,9 @@ module.exports = function plot(gd, calcData) {
190200
gd: gd
191201
});
192202

193-
makeTranslucent(tooltip, 0.65);
203+
if(!d.link.concentrationscale) {
204+
makeTranslucent(tooltip, 0.65);
205+
}
194206
makeTextContrasty(tooltip);
195207
};
196208

@@ -288,6 +300,7 @@ module.exports = function plot(gd, calcData) {
288300
};
289301

290302
render(
303+
gd,
291304
svg,
292305
calcData,
293306
{

src/traces/sankey/render.js

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

70+
function computeLinkConcentrations() {
71+
var i, j, k;
72+
for(i = 0; i < graph.nodes.length; i++) {
73+
var node = graph.nodes[i];
74+
// Links connecting the same two nodes are part of a flow
75+
var flows = {};
76+
var flowKey;
77+
var link;
78+
for(j = 0; j < node.targetLinks.length; j++) {
79+
link = node.targetLinks[j];
80+
flowKey = link.source.pointNumber + ':' + link.target.pointNumber;
81+
if(!flows.hasOwnProperty(flowKey)) flows[flowKey] = [];
82+
flows[flowKey].push(link);
83+
}
84+
85+
// Compute statistics for each flow
86+
var keys = Object.keys(flows);
87+
for(j = 0; j < keys.length; j++) {
88+
flowKey = keys[j];
89+
var flowLinks = flows[flowKey];
90+
91+
// Find the total size of the flow and total size per label
92+
var total = 0;
93+
var totalPerLabel = {};
94+
for(k = 0; k < flowLinks.length; k++) {
95+
link = flowLinks[k];
96+
if(!totalPerLabel[link.label]) totalPerLabel[link.label] = 0;
97+
totalPerLabel[link.label] += link.value;
98+
total += link.value;
99+
}
100+
101+
// Find the ratio of the link's value and the size of the flow
102+
for(k = 0; k < flowLinks.length; k++) {
103+
link = flowLinks[k];
104+
link.flow = {
105+
value: total,
106+
labelConcentration: totalPerLabel[link.label] / total,
107+
concentration: link.value / total,
108+
links: flowLinks
109+
};
110+
}
111+
}
112+
113+
// Gather statistics of all links at current node
114+
var totalOutflow = 0;
115+
for(j = 0; j < node.sourceLinks.length; j++) {
116+
totalOutflow += node.sourceLinks[j].value;
117+
}
118+
for(j = 0; j < node.sourceLinks.length; j++) {
119+
link = node.sourceLinks[j];
120+
link.concentrationOut = link.value / totalOutflow;
121+
}
122+
123+
var totalInflow = 0;
124+
for(j = 0; j < node.targetLinks.length; j++) {
125+
totalInflow += node.targetLinks[j].value;
126+
}
127+
128+
for(j = 0; j < node.targetLinks.length; j++) {
129+
link = node.targetLinks[j];
130+
link.concenrationIn = link.value / totalInflow;
131+
}
132+
}
133+
}
134+
computeLinkConcentrations();
135+
70136
return {
71137
circular: circular,
72138
key: traceIndex,
@@ -100,6 +166,9 @@ function sankeyModel(layout, d, traceIndex) {
100166

101167
function linkModel(d, l, i) {
102168
var tc = tinycolor(l.color);
169+
if(l.concentrationscale) {
170+
tc = tinycolor(l.concentrationscale(l.flow.labelConcentration));
171+
}
103172
var basicKey = l.source.label + '|' + l.target.label;
104173
var key = basicKey + '__' + i;
105174

@@ -121,7 +190,8 @@ function linkModel(d, l, i) {
121190
valueSuffix: d.valueSuffix,
122191
sankey: d.sankey,
123192
parent: d,
124-
interactionState: d.interactionState
193+
interactionState: d.interactionState,
194+
flow: l.flow
125195
};
126196
}
127197

@@ -568,7 +638,7 @@ function switchToSankeyFormat(nodes) {
568638
}
569639

570640
// scene graph
571-
module.exports = function(svg, calcData, layout, callbacks) {
641+
module.exports = function(gd, svg, calcData, layout, callbacks) {
572642

573643
var styledData = calcData
574644
.filter(function(d) {return unwrap(d).trace.visible;})
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"node": {
6+
"pad": 25,
7+
"line": {
8+
"color": "white",
9+
"width": 2
10+
},
11+
"color": "black",
12+
"label": ["process0", "process1", "process2", "process3", "process4"]
13+
},
14+
"link": {
15+
"source": [
16+
0, 0, 0, 0,
17+
1, 1, 1, 1,
18+
1, 1, 1, 1,
19+
1, 1,
20+
2
21+
],
22+
"target": [
23+
1, 1, 1, 1,
24+
2, 2, 2, 2,
25+
3, 3, 3, 3,
26+
4, 4,
27+
0
28+
],
29+
"value": [
30+
10, 20, 40, 30,
31+
10, 5, 10, 20,
32+
0, 10, 10, 10,
33+
15, 5,
34+
20
35+
36+
],
37+
"label": [
38+
"elementA", "elementB", "elementC", "elementD",
39+
"elementA", "elementB", "elementC", "elementD",
40+
"elementA", "elementB", "elementC", "elementD",
41+
"elementC", "elementC",
42+
"elementA"
43+
],
44+
"line": {
45+
"color": "white",
46+
"width": 2
47+
},
48+
"colorscales": [
49+
{
50+
"label": "elementA",
51+
"colorscale": [[0, "white"], [1, "blue"]]
52+
},
53+
{
54+
"label": "elementB",
55+
"colorscale": [[0, "white"], [1, "red"]]
56+
},
57+
{
58+
"label": "elementC",
59+
"colorscale": [[0, "white"], [1, "green"]]
60+
},
61+
{
62+
"label": "elementD"
63+
}
64+
],
65+
66+
"hovertemplate": "%{label}<br><b>flow.labelConcentration</b>: %{flow.labelConcentration:%0.2f}<br><b>flow.concentration</b>: %{flow.concentration:%0.2f}<br><b>flow.value</b>: %{flow.value}"
67+
}
68+
69+
}],
70+
"layout": {
71+
"title": "Sankey diagram with links colored based on their concentration within a flow",
72+
"width": 800,
73+
"height": 800
74+
}
75+
}

test/jasmine/tests/sankey_test.js

+9
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ describe('sankey tests', function() {
121121

122122
expect(fullTrace.link.label)
123123
.toEqual([], 'presence of link target array is guaranteed');
124+
125+
expect(fullTrace.link.colorscales)
126+
.toEqual([], 'presence of link colorscales array is guaranteed');
124127
});
125128

126129
it('\'Sankey\' specification should have proper types',
@@ -826,6 +829,12 @@ describe('sankey tests', function() {
826829
var pt = d.points[0];
827830
expect(pt.hasOwnProperty('source')).toBeTruthy();
828831
expect(pt.hasOwnProperty('target')).toBeTruthy();
832+
expect(pt.hasOwnProperty('flow')).toBeTruthy();
833+
834+
expect(pt.flow.hasOwnProperty('concentration')).toBeTruthy();
835+
expect(pt.flow.hasOwnProperty('labelConcentration')).toBeTruthy();
836+
expect(pt.flow.hasOwnProperty('value')).toBeTruthy();
837+
expect(pt.flow.hasOwnProperty('links')).toBeTruthy();
829838
})
830839
.then(function() { return _unhover('node'); })
831840
.then(function(d) {

0 commit comments

Comments
 (0)