Skip to content

Sankey: colorscales per component, linked to concentration #3501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 18, 2019
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