Skip to content
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

Sankey: colorscales per component, linked to concentration #3501

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');
}
1 change: 1 addition & 0 deletions src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ module.exports = function plot(gd, calcData) {
};

render(
gd,
svg,
calcData,
{
Expand Down
61 changes: 59 additions & 2 deletions src/traces/sankey/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

var c = require('./constants');
var d3 = require('d3');
var sum = require('d3-array').sum;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha this here only works between d3-sankey already depends on d3-array.

image

We should add d3-array to the (plotly.js) dependencies OR use plain loops and save one line in our package.json.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will do the latter and save one line in our package.json.

I should probably replace all the forEach while I'm at it right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably replace all the forEach while I'm at it right?

That would be amazing 💪

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c228b37

var tinycolor = require('tinycolor2');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
Expand Down Expand Up @@ -67,6 +68,57 @@ function sankeyModel(layout, d, traceIndex) {
Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.');
}

function computeLinkConcentrations() {
graph.nodes.forEach(function(node) {
// Links connecting the same two nodes are part of a flow
var flows = {};
node.targetLinks.forEach(function(link) {
var flowKey = link.source.pointNumber + ':' + link.target.pointNumber;
if(!flows.hasOwnProperty(flowKey)) flows[flowKey] = [];
flows[flowKey].push(link);
});

// Compute statistics for each flow
Object.keys(flows).forEach(function(flowKey) {
var flowLinks = flows[flowKey];

// Find the total size of the flow and total size per label
var total = 0;
var totalPerLabel = {};
flowLinks.forEach(function(link) {
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
flowLinks.forEach(function(link) {
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 = sum(node.sourceLinks, function(n) {
return n.value;
});
node.sourceLinks.forEach(function(link) {
link.concentrationOut = link.value / totalOutflow;
});
var totalInflow = sum(node.targetLinks, function(n) {
return n.value;
});
node.targetLinks.forEach(function(link) {
link.concenrationIn = link.value / totalInflow;
});
});
}
computeLinkConcentrations();

return {
circular: circular,
key: traceIndex,
Expand Down Expand Up @@ -100,6 +152,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 +176,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 +624,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 Expand Up @@ -616,6 +672,7 @@ module.exports = function(svg, calcData, layout, callbacks) {
.attr('d', linkPath())
.call(attachPointerEvents, sankey, callbacks.linkEvents);


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ no need to commit this newline.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f628b54

sankeyLink
.style('stroke', function(d) {
return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions test/image/mocks/sankey_link_concentration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"data": [
{
"type": "sankey",
"node": {
"pad": 25,
"line": {
"color": "white",
"width": 2
},
"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,
5

],
"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 with links colored based on its concentration within a flow",
"width": 800,
"height": 800
}
}
4 changes: 4 additions & 0 deletions test/jasmine/tests/sankey_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,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 @@ -784,6 +787,7 @@ describe('sankey tests', function() {
var pt = d.points[0];
expect(pt.hasOwnProperty('source')).toBeTruthy();
expect(pt.hasOwnProperty('target')).toBeTruthy();
expect(pt.hasOwnProperty('flow')).toBeTruthy();
})
.then(function() { return _unhover('node'); })
.then(function(d) {
Expand Down