Skip to content

Commit 6f056eb

Browse files
committed
identify link per label, compute concentration and color accordingly
1 parent 48a8b1b commit 6f056eb

File tree

8 files changed

+199
-5
lines changed

8 files changed

+199
-5
lines changed

src/traces/sankey/attributes.js

+28
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,6 +227,32 @@ var attrs = module.exports = overrideAll({
225227
description: 'Variables `source` and `target` are node objects.',
226228
keys: ['value', 'label']
227229
}),
230+
colorscales: templatedArray('concentrationscales', {
231+
editType: 'calc',
232+
label: {
233+
valType: 'string',
234+
role: 'calc',
235+
description: 'The label of the links to color based on their concentration within a flow.',
236+
dflt: ''
237+
},
238+
cmax: {
239+
valType: 'number',
240+
role: 'calc',
241+
dflt: 1,
242+
description: [
243+
'Sets the upper bound of the color domain.'
244+
].join('')
245+
},
246+
cmin: {
247+
valType: 'number',
248+
role: 'calc',
249+
dflt: 0,
250+
description: [
251+
'Sets the lower bound of the color domain.'
252+
].join('')
253+
},
254+
colorscale: extendFlat(colorAttributes().colorscale, {dflt: [[0, 'white'], [1, 'black']]})
255+
}),
228256
description: 'The links of the Sankey plot.'
229257
}
230258
}, 'calc', 'nested');

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

+1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ module.exports = function plot(gd, calcData) {
288288
};
289289

290290
render(
291+
gd,
291292
svg,
292293
calcData,
293294
{

src/traces/sankey/render.js

+59-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
var c = require('./constants');
1212
var d3 = require('d3');
13+
var sum = require('d3-array').sum;
1314
var tinycolor = require('tinycolor2');
1415
var Color = require('../../components/color');
1516
var Drawing = require('../../components/drawing');
@@ -67,6 +68,57 @@ function sankeyModel(layout, d, traceIndex) {
6768
Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.');
6869
}
6970

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

101153
function linkModel(d, l, i) {
102154
var tc = tinycolor(l.color);
155+
if(l.concentrationscale) {
156+
tc = tinycolor(l.concentrationscale(l.flow.labelConcentration));
157+
}
103158
var basicKey = l.source.label + '|' + l.target.label;
104159
var key = basicKey + '__' + i;
105160

@@ -121,7 +176,8 @@ function linkModel(d, l, i) {
121176
valueSuffix: d.valueSuffix,
122177
sankey: d.sankey,
123178
parent: d,
124-
interactionState: d.interactionState
179+
interactionState: d.interactionState,
180+
flow: l.flow
125181
};
126182
}
127183

@@ -568,7 +624,7 @@ function switchToSankeyFormat(nodes) {
568624
}
569625

570626
// scene graph
571-
module.exports = function(svg, calcData, layout, callbacks) {
627+
module.exports = function(gd, svg, calcData, layout, callbacks) {
572628

573629
var styledData = calcData
574630
.filter(function(d) {return unwrap(d).trace.visible;})
@@ -616,6 +672,7 @@ module.exports = function(svg, calcData, layout, callbacks) {
616672
.attr('d', linkPath())
617673
.call(attachPointerEvents, sankey, callbacks.linkEvents);
618674

675+
619676
sankeyLink
620677
.style('stroke', function(d) {
621678
return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue;
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"node": {
6+
"pad": 25,
7+
"line": {
8+
"color": "white",
9+
"width": 2
10+
},
11+
"label": ["process0", "process1", "process2", "process3", "process4"]
12+
},
13+
"link": {
14+
"source": [
15+
0, 0, 0, 0,
16+
1, 1, 1, 1,
17+
1, 1, 1, 1,
18+
1, 1,
19+
2
20+
],
21+
"target": [
22+
1, 1, 1, 1,
23+
2, 2, 2, 2,
24+
3, 3, 3, 3,
25+
4, 4,
26+
0
27+
],
28+
"value": [
29+
10, 20, 40, 30,
30+
10, 5, 10, 20,
31+
0, 10, 10, 10,
32+
15, 5,
33+
5
34+
35+
],
36+
"label": [
37+
"elementA", "elementB", "elementC", "elementD",
38+
"elementA", "elementB", "elementC", "elementD",
39+
"elementA", "elementB", "elementC", "elementD",
40+
"elementC", "elementC",
41+
"elementA"
42+
],
43+
"line": {
44+
"color": "white",
45+
"width": 2
46+
},
47+
"colorscales": [
48+
{
49+
"label": "elementA",
50+
"colorscale": [[0, "white"], [1, "blue"]]
51+
},
52+
{
53+
"label": "elementB",
54+
"colorscale": [[0, "white"], [1, "red"]]
55+
},
56+
{
57+
"label": "elementC",
58+
"colorscale": [[0, "white"], [1, "green"]]
59+
},
60+
{
61+
"label": "elementD"
62+
}
63+
],
64+
65+
"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}"
66+
}
67+
68+
}],
69+
"layout": {
70+
"title": "Sankey with links colored based on its concentration within a flow",
71+
"width": 800,
72+
"height": 800
73+
}
74+
}

test/jasmine/tests/sankey_test.js

+4
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ describe('sankey tests', function() {
120120

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

125128
it('\'Sankey\' specification should have proper types',
@@ -784,6 +787,7 @@ describe('sankey tests', function() {
784787
var pt = d.points[0];
785788
expect(pt.hasOwnProperty('source')).toBeTruthy();
786789
expect(pt.hasOwnProperty('target')).toBeTruthy();
790+
expect(pt.hasOwnProperty('flow')).toBeTruthy();
787791
})
788792
.then(function() { return _unhover('node'); })
789793
.then(function(d) {

0 commit comments

Comments
 (0)