diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js
index 905901c7bd3..b9144ad0e4c 100644
--- a/src/traces/sankey/attributes.js
+++ b/src/traces/sankey/attributes.js
@@ -14,6 +14,8 @@ var colorAttrs = require('../../components/color/attributes');
var fxAttrs = require('../../components/fx/attributes');
var domainAttrs = require('../../plots/domain').attributes;
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
+var colorAttributes = require('../../components/colorscale/attributes');
+var templatedArray = require('../../plot_api/plot_template').templatedArray;
var extendFlat = require('../../lib/extend').extendFlat;
var overrideAll = require('../../plot_api/edit_types').overrideAll;
@@ -225,7 +227,37 @@ var attrs = module.exports = overrideAll({
description: 'Variables `source` and `target` are node objects.',
keys: ['value', 'label']
}),
- description: 'The links of the Sankey plot.'
+ colorscales: templatedArray('concentrationscales', {
+ editType: 'calc',
+ label: {
+ valType: 'string',
+ role: 'info',
+ editType: 'calc',
+ description: 'The label of the links to color based on their concentration within a flow.',
+ dflt: ''
+ },
+ cmax: {
+ valType: 'number',
+ role: 'info',
+ editType: 'calc',
+ dflt: 1,
+ description: [
+ 'Sets the upper bound of the color domain.'
+ ].join('')
+ },
+ cmin: {
+ valType: 'number',
+ role: 'info',
+ editType: 'calc',
+ dflt: 0,
+ description: [
+ 'Sets the lower bound of the color domain.'
+ ].join('')
+ },
+ colorscale: extendFlat(colorAttributes().colorscale, {dflt: [[0, 'white'], [1, 'black']]})
+ }),
+ description: 'The links of the Sankey plot.',
+ role: 'info'
}
}, 'calc', 'nested');
attrs.transforms = undefined;
diff --git a/src/traces/sankey/calc.js b/src/traces/sankey/calc.js
index 8e9a5756517..861ce4f0397 100644
--- a/src/traces/sankey/calc.js
+++ b/src/traces/sankey/calc.js
@@ -14,7 +14,7 @@ var wrap = require('../../lib/gup').wrap;
var isArrayOrTypedArray = Lib.isArrayOrTypedArray;
var isIndex = Lib.isIndex;
-
+var Colorscale = require('../../components/colorscale');
function convertToD3Sankey(trace) {
var nodeSpec = trace.node;
@@ -24,8 +24,17 @@ function convertToD3Sankey(trace) {
var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color);
var linkedNodes = {};
- var nodeCount = nodeSpec.label.length;
+ var components = {};
+ var componentCount = linkSpec.colorscales.length;
var i;
+ for(i = 0; i < componentCount; i++) {
+ var cscale = linkSpec.colorscales[i];
+ var specs = Colorscale.extractScale(cscale, {cLetter: 'c'});
+ var scale = Colorscale.makeColorScaleFunc(specs);
+ components[cscale.label] = scale;
+ }
+
+ var nodeCount = nodeSpec.label.length;
for(i = 0; i < linkSpec.value.length; i++) {
var val = linkSpec.value[i];
// remove negative values, but keep zeros with special treatment
@@ -42,10 +51,14 @@ function convertToD3Sankey(trace) {
var label = '';
if(linkSpec.label && linkSpec.label[i]) label = linkSpec.label[i];
+ var concentrationscale = null;
+ if(label && components.hasOwnProperty(label)) concentrationscale = components[label];
+
links.push({
pointNumber: i,
label: label,
color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color,
+ concentrationscale: concentrationscale,
source: source,
target: target,
value: +val
diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js
index a135eecbdee..c5283910ade 100644
--- a/src/traces/sankey/defaults.js
+++ b/src/traces/sankey/defaults.js
@@ -15,6 +15,7 @@ var tinycolor = require('tinycolor2');
var handleDomainDefaults = require('../../plots/domain').defaults;
var handleHoverLabelDefaults = require('../../components/fx/hoverlabel_defaults');
var Template = require('../../plot_api/plot_template');
+var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
@@ -48,7 +49,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}));
// link attributes
- var linkIn = traceIn.link;
+ var linkIn = traceIn.link || {};
var linkOut = Template.newContainer(traceOut, 'link');
function coerceLink(attr, dflt) {
@@ -70,6 +71,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerceLink('color', Lib.repeat(defaultLinkColor, linkOut.value.length));
+ handleArrayContainerDefaults(linkIn, linkOut, {
+ name: 'colorscales',
+ handleItemDefaults: concentrationscalesDefaults
+ });
+
handleDomainDefaults(traceOut, layout, coerce);
coerce('orientation');
@@ -83,3 +89,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
// don't match, between nodes and links
traceOut._length = null;
};
+
+function concentrationscalesDefaults(In, Out) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(In, Out, attributes.link.colorscales, attr, dflt);
+ }
+
+ coerce('label');
+ coerce('cmin');
+ coerce('cmax');
+ coerce('colorscale');
+}
diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js
index cc04eb441ed..6de168a3ed3 100644
--- a/src/traces/sankey/plot.js
+++ b/src/traces/sankey/plot.js
@@ -72,13 +72,21 @@ function linkHoveredStyle(d, sankey, visitNodes, sankeyLink) {
var label = sankeyLink.datum().link.label;
- sankeyLink.style('fill-opacity', 0.4);
+ sankeyLink.style('fill-opacity', function(l) {
+ if(!l.link.concentrationscale) {
+ return 0.4;
+ }
+ });
if(label) {
ownTrace(sankey, d)
.selectAll('.' + cn.sankeyLink)
.filter(function(l) {return l.link.label === label;})
- .style('fill-opacity', 0.4);
+ .style('fill-opacity', function(l) {
+ if(!l.link.concentrationscale) {
+ return 0.4;
+ }
+ });
}
if(visitNodes) {
@@ -143,6 +151,7 @@ module.exports = function plot(gd, calcData) {
var sourceLabel = _(gd, 'source:') + ' ';
var targetLabel = _(gd, 'target:') + ' ';
+ var concentrationLabel = _(gd, 'concentration:') + ' ';
var incomingLabel = _(gd, 'incoming flow count:') + ' ';
var outgoingLabel = _(gd, 'outgoing flow count:') + ' ';
@@ -172,7 +181,8 @@ module.exports = function plot(gd, calcData) {
text: [
d.link.label || '',
sourceLabel + d.link.source.label,
- targetLabel + d.link.target.label
+ targetLabel + d.link.target.label,
+ d.link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(d.link.flow.labelConcentration) : ''
].filter(renderableValuePresent).join('
'),
color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
borderColor: castHoverOption(obj, 'bordercolor'),
@@ -190,7 +200,9 @@ module.exports = function plot(gd, calcData) {
gd: gd
});
- makeTranslucent(tooltip, 0.65);
+ if(!d.link.concentrationscale) {
+ makeTranslucent(tooltip, 0.65);
+ }
makeTextContrasty(tooltip);
};
@@ -288,6 +300,7 @@ module.exports = function plot(gd, calcData) {
};
render(
+ gd,
svg,
calcData,
{
diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js
index 249ebab281d..88b27dbdafc 100644
--- a/src/traces/sankey/render.js
+++ b/src/traces/sankey/render.js
@@ -67,6 +67,72 @@ function sankeyModel(layout, d, traceIndex) {
Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.');
}
+ function computeLinkConcentrations() {
+ var i, j, k;
+ for(i = 0; i < graph.nodes.length; i++) {
+ var node = graph.nodes[i];
+ // Links connecting the same two nodes are part of a flow
+ var flows = {};
+ var flowKey;
+ var link;
+ for(j = 0; j < node.targetLinks.length; j++) {
+ link = node.targetLinks[j];
+ flowKey = link.source.pointNumber + ':' + link.target.pointNumber;
+ if(!flows.hasOwnProperty(flowKey)) flows[flowKey] = [];
+ flows[flowKey].push(link);
+ }
+
+ // Compute statistics for each flow
+ var keys = Object.keys(flows);
+ for(j = 0; j < keys.length; j++) {
+ flowKey = keys[j];
+ var flowLinks = flows[flowKey];
+
+ // Find the total size of the flow and total size per label
+ var total = 0;
+ var totalPerLabel = {};
+ for(k = 0; k < flowLinks.length; k++) {
+ link = flowLinks[k];
+ if(!totalPerLabel[link.label]) totalPerLabel[link.label] = 0;
+ totalPerLabel[link.label] += link.value;
+ total += link.value;
+ }
+
+ // Find the ratio of the link's value and the size of the flow
+ for(k = 0; k < flowLinks.length; k++) {
+ link = flowLinks[k];
+ link.flow = {
+ value: total,
+ labelConcentration: totalPerLabel[link.label] / total,
+ concentration: link.value / total,
+ links: flowLinks
+ };
+ }
+ }
+
+ // Gather statistics of all links at current node
+ var totalOutflow = 0;
+ for(j = 0; j < node.sourceLinks.length; j++) {
+ totalOutflow += node.sourceLinks[j].value;
+ }
+ for(j = 0; j < node.sourceLinks.length; j++) {
+ link = node.sourceLinks[j];
+ link.concentrationOut = link.value / totalOutflow;
+ }
+
+ var totalInflow = 0;
+ for(j = 0; j < node.targetLinks.length; j++) {
+ totalInflow += node.targetLinks[j].value;
+ }
+
+ for(j = 0; j < node.targetLinks.length; j++) {
+ link = node.targetLinks[j];
+ link.concenrationIn = link.value / totalInflow;
+ }
+ }
+ }
+ computeLinkConcentrations();
+
return {
circular: circular,
key: traceIndex,
@@ -100,6 +166,9 @@ function sankeyModel(layout, d, traceIndex) {
function linkModel(d, l, i) {
var tc = tinycolor(l.color);
+ if(l.concentrationscale) {
+ tc = tinycolor(l.concentrationscale(l.flow.labelConcentration));
+ }
var basicKey = l.source.label + '|' + l.target.label;
var key = basicKey + '__' + i;
@@ -121,7 +190,8 @@ function linkModel(d, l, i) {
valueSuffix: d.valueSuffix,
sankey: d.sankey,
parent: d,
- interactionState: d.interactionState
+ interactionState: d.interactionState,
+ flow: l.flow
};
}
@@ -568,7 +638,7 @@ function switchToSankeyFormat(nodes) {
}
// scene graph
-module.exports = function(svg, calcData, layout, callbacks) {
+module.exports = function(gd, svg, calcData, layout, callbacks) {
var styledData = calcData
.filter(function(d) {return unwrap(d).trace.visible;})
diff --git a/test/image/baselines/sankey_link_concentration.png b/test/image/baselines/sankey_link_concentration.png
new file mode 100644
index 00000000000..9b512b8fac7
Binary files /dev/null and b/test/image/baselines/sankey_link_concentration.png differ
diff --git a/test/image/mocks/sankey_link_concentration.json b/test/image/mocks/sankey_link_concentration.json
new file mode 100644
index 00000000000..789380539d0
--- /dev/null
+++ b/test/image/mocks/sankey_link_concentration.json
@@ -0,0 +1,75 @@
+{
+ "data": [
+ {
+ "type": "sankey",
+ "node": {
+ "pad": 25,
+ "line": {
+ "color": "white",
+ "width": 2
+ },
+ "color": "black",
+ "label": ["process0", "process1", "process2", "process3", "process4"]
+ },
+ "link": {
+ "source": [
+ 0, 0, 0, 0,
+ 1, 1, 1, 1,
+ 1, 1, 1, 1,
+ 1, 1,
+ 2
+ ],
+ "target": [
+ 1, 1, 1, 1,
+ 2, 2, 2, 2,
+ 3, 3, 3, 3,
+ 4, 4,
+ 0
+ ],
+ "value": [
+ 10, 20, 40, 30,
+ 10, 5, 10, 20,
+ 0, 10, 10, 10,
+ 15, 5,
+ 20
+
+ ],
+ "label": [
+ "elementA", "elementB", "elementC", "elementD",
+ "elementA", "elementB", "elementC", "elementD",
+ "elementA", "elementB", "elementC", "elementD",
+ "elementC", "elementC",
+ "elementA"
+ ],
+ "line": {
+ "color": "white",
+ "width": 2
+ },
+ "colorscales": [
+ {
+ "label": "elementA",
+ "colorscale": [[0, "white"], [1, "blue"]]
+ },
+ {
+ "label": "elementB",
+ "colorscale": [[0, "white"], [1, "red"]]
+ },
+ {
+ "label": "elementC",
+ "colorscale": [[0, "white"], [1, "green"]]
+ },
+ {
+ "label": "elementD"
+ }
+ ],
+
+ "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:%0.2f}
flow.concentration: %{flow.concentration:%0.2f}
flow.value: %{flow.value}"
+ }
+
+ }],
+ "layout": {
+ "title": "Sankey diagram with links colored based on their concentration within a flow",
+ "width": 800,
+ "height": 800
+ }
+}
diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js
index d2a7391d8ed..6471906d6e1 100644
--- a/test/jasmine/tests/sankey_test.js
+++ b/test/jasmine/tests/sankey_test.js
@@ -121,6 +121,9 @@ describe('sankey tests', function() {
expect(fullTrace.link.label)
.toEqual([], 'presence of link target array is guaranteed');
+
+ expect(fullTrace.link.colorscales)
+ .toEqual([], 'presence of link colorscales array is guaranteed');
});
it('\'Sankey\' specification should have proper types',
@@ -826,6 +829,12 @@ describe('sankey tests', function() {
var pt = d.points[0];
expect(pt.hasOwnProperty('source')).toBeTruthy();
expect(pt.hasOwnProperty('target')).toBeTruthy();
+ expect(pt.hasOwnProperty('flow')).toBeTruthy();
+
+ expect(pt.flow.hasOwnProperty('concentration')).toBeTruthy();
+ expect(pt.flow.hasOwnProperty('labelConcentration')).toBeTruthy();
+ expect(pt.flow.hasOwnProperty('value')).toBeTruthy();
+ expect(pt.flow.hasOwnProperty('links')).toBeTruthy();
})
.then(function() { return _unhover('node'); })
.then(function(d) {