Skip to content

Commit

Permalink
Merge pull request #3501 from plotly/pr-sankey-link-concentration
Browse files Browse the repository at this point in the history
Sankey: colorscales per component, linked to concentration
  • Loading branch information
antoinerg authored Feb 18, 2019
2 parents 0714286 + f628b54 commit 12097a2
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 10 deletions.
34 changes: 33 additions & 1 deletion src/traces/sankey/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
17 changes: 15 additions & 2 deletions src/traces/sankey/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/traces/sankey/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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');
Expand All @@ -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');
}
21 changes: 17 additions & 4 deletions src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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:') + ' ';

Expand Down Expand Up @@ -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('<br>'),
color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
borderColor: castHoverOption(obj, 'bordercolor'),
Expand All @@ -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);
};

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

render(
gd,
svg,
calcData,
{
Expand Down
74 changes: 72 additions & 2 deletions src/traces/sankey/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand All @@ -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
};
}

Expand Down Expand Up @@ -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;})
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions test/image/mocks/sankey_link_concentration.json
Original file line number Diff line number Diff line change
@@ -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}<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}"
}

}],
"layout": {
"title": "Sankey diagram with links colored based on their concentration within a flow",
"width": 800,
"height": 800
}
}
9 changes: 9 additions & 0 deletions test/jasmine/tests/sankey_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 12097a2

Please sign in to comment.