-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
More robust Sankey #2629
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
More robust Sankey #2629
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,9 +16,12 @@ var Drawing = require('../../components/drawing'); | |
var d3sankey = require('@plotly/d3-sankey').sankey; | ||
var d3Force = require('d3-force'); | ||
var Lib = require('../../lib'); | ||
var keyFun = require('../../lib/gup').keyFun; | ||
var repeat = require('../../lib/gup').repeat; | ||
var unwrap = require('../../lib/gup').unwrap; | ||
var isArrayOrTypedArray = Lib.isArrayOrTypedArray; | ||
var isIndex = Lib.isIndex; | ||
var gup = require('../../lib/gup'); | ||
var keyFun = gup.keyFun; | ||
var repeat = gup.repeat; | ||
var unwrap = gup.unwrap; | ||
|
||
// basic data utilities | ||
|
||
|
@@ -63,44 +66,79 @@ function switchToSankeyFormat(nodes) { | |
|
||
// view models | ||
|
||
function sankeyModel(layout, d, i) { | ||
var trace = unwrap(d).trace, | ||
domain = trace.domain, | ||
nodeSpec = trace.node, | ||
linkSpec = trace.link, | ||
arrangement = trace.arrangement, | ||
horizontal = trace.orientation === 'h', | ||
nodePad = trace.node.pad, | ||
nodeThickness = trace.node.thickness, | ||
nodeLineColor = trace.node.line.color, | ||
nodeLineWidth = trace.node.line.width, | ||
linkLineColor = trace.link.line.color, | ||
linkLineWidth = trace.link.line.width, | ||
valueFormat = trace.valueformat, | ||
valueSuffix = trace.valuesuffix, | ||
textFont = trace.textfont; | ||
|
||
var width = layout.width * (domain.x[1] - domain.x[0]), | ||
height = layout.height * (domain.y[1] - domain.y[0]); | ||
|
||
var nodes = nodeSpec.label.map(function(l, i) { | ||
return { | ||
pointNumber: i, | ||
label: l, | ||
color: Lib.isArrayOrTypedArray(nodeSpec.color) ? nodeSpec.color[i] : nodeSpec.color | ||
}; | ||
}); | ||
function sankeyModel(layout, d, traceIndex) { | ||
var trace = unwrap(d).trace; | ||
var domain = trace.domain; | ||
var nodeSpec = trace.node; | ||
var linkSpec = trace.link; | ||
var arrangement = trace.arrangement; | ||
var horizontal = trace.orientation === 'h'; | ||
var nodePad = trace.node.pad; | ||
var nodeThickness = trace.node.thickness; | ||
var nodeLineColor = trace.node.line.color; | ||
var nodeLineWidth = trace.node.line.width; | ||
var linkLineColor = trace.link.line.color; | ||
var linkLineWidth = trace.link.line.width; | ||
var valueFormat = trace.valueformat; | ||
var valueSuffix = trace.valuesuffix; | ||
var textFont = trace.textfont; | ||
|
||
var width = layout.width * (domain.x[1] - domain.x[0]); | ||
var height = layout.height * (domain.y[1] - domain.y[0]); | ||
|
||
var links = []; | ||
var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color); | ||
var linkedNodes = {}; | ||
|
||
var nodeCount = nodeSpec.label.length; | ||
var i; | ||
for(i = 0; i < linkSpec.value.length; i++) { | ||
var val = linkSpec.value[i]; | ||
// remove negative values, but keep zeros with special treatment | ||
var source = linkSpec.source[i]; | ||
var target = linkSpec.target[i]; | ||
if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) { | ||
continue; | ||
} | ||
|
||
source = +source; | ||
target = +target; | ||
linkedNodes[source] = linkedNodes[target] = true; | ||
|
||
var links = linkSpec.value.map(function(d, i) { | ||
return { | ||
links.push({ | ||
pointNumber: i, | ||
label: linkSpec.label[i], | ||
color: Lib.isArrayOrTypedArray(linkSpec.color) ? linkSpec.color[i] : linkSpec.color, | ||
source: linkSpec.source[i], | ||
target: linkSpec.target[i], | ||
value: d | ||
}; | ||
}); | ||
color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color, | ||
source: source, | ||
target: target, | ||
value: +val | ||
}); | ||
} | ||
|
||
var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color); | ||
var nodes = []; | ||
var removedNodes = false; | ||
var nodeIndices = {}; | ||
for(i = 0; i < nodeCount; i++) { | ||
if(linkedNodes[i]) { | ||
var l = nodeSpec.label[i]; | ||
nodeIndices[i] = nodes.length; | ||
nodes.push({ | ||
pointNumber: i, | ||
label: l, | ||
color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color | ||
}); | ||
} | ||
else removedNodes = true; | ||
} | ||
|
||
// need to re-index links now, since we didn't put all the nodes in | ||
if(removedNodes) { | ||
for(i = 0; i < links.length; i++) { | ||
links[i].source = nodeIndices[links[i].source]; | ||
links[i].target = nodeIndices[links[i].target]; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the most part it didn't cause problems to have extra unused nodes in the list, but sometimes the layout algorithm would reserve extra space for these nodes (leaving extra margins or making the whole diagram S-shaped, for example) so it's better to remove them. The |
||
|
||
var sankey = d3sankey() | ||
.size(horizontal ? [width, height] : [height, width]) | ||
|
@@ -120,7 +158,7 @@ function sankeyModel(layout, d, i) { | |
switchToForceFormat(nodes); | ||
|
||
return { | ||
key: i, | ||
key: traceIndex, | ||
trace: trace, | ||
guid: Math.floor(1e12 * (1 + Math.random())), | ||
horizontal: horizontal, | ||
|
@@ -430,7 +468,6 @@ module.exports = function(svg, styledData, layout, callbacks) { | |
.style('left', 0) | ||
.style('shape-rendering', 'geometricPrecision') | ||
.style('pointer-events', 'auto') | ||
.style('box-sizing', 'content-box') | ||
.attr('transform', sankeyTransform); | ||
|
||
sankey.transition() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"data": [ | ||
{ | ||
"type": "sankey", | ||
"node": { | ||
"pad": 5, | ||
"label": [ | ||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", | ||
"10", 11, "12", 13, 14, 15, 16, 17, 18, 19, | ||
20, "21", 22, "23", 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, | ||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t" | ||
] | ||
}, | ||
"link": { | ||
"source": [ | ||
"", "x", 20, 21, 20, 21, | ||
20, 21, 22, 23, 24, 25, 20, 26, 22, 23, 27, 28, | ||
22, 29, 26, 28, 23, 30, 31, 32, 29, 26, 33, 34, | ||
24, 35, 36, 37, 38, 26, 22, 39, 36, 27, 26, 23, | ||
"40", "22", "41", "35", 42, 27, 40, 20, 28 | ||
], | ||
"target": [ | ||
10, 10, "", "q", 10, 10, | ||
10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, | ||
12, 13, 13, 13, 13, 14, 14, 14, 14, 14, 15, 15, | ||
15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, | ||
18, 18, "18", "18", 19, 19, "19", "19", 19 | ||
], | ||
"value": [ | ||
100, 100, 100, 100, -15, "nope", | ||
48, 40, 44, 4, 14, 0, 40, 25, 50, 137, 149, 100, | ||
100, 10, 200, 149, 237, 11, 130, 40, 10, 200, 50, 100, | ||
13, 100, 20, 100, 20, 100, 100, 7, 150, 40, 50, 11, | ||
"50", 80, "2", 40, "50", 50, "90", 125, 50 | ||
] | ||
} | ||
}], | ||
"layout": { | ||
"title": "Sankey with messy data", | ||
"width": 600, | ||
"height": 800 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,30 +58,31 @@ describe('sankey tests', function() { | |
}); | ||
}); | ||
|
||
describe('log warning if issue is encountered with graph structure', | ||
function() { | ||
|
||
it('some nodes are not linked', function() { | ||
|
||
var warnings = []; | ||
spyOn(Lib, 'warn').and.callFake(function(msg) { | ||
warnings.push(msg); | ||
}); | ||
|
||
_supply({ | ||
node: { | ||
label: ['a', 'b', 'c'] | ||
}, | ||
link: { | ||
value: [1], | ||
source: [0], | ||
target: [1] | ||
} | ||
}); | ||
describe('No warnings for missing nodes', function() { | ||
// we used to warn when some nodes were not used in the links | ||
// not doing that anymore, it's not really consistent with | ||
// the rest of our data processing. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nicely done 👌 |
||
it('some nodes are not linked', function() { | ||
|
||
var warnings = []; | ||
spyOn(Lib, 'warn').and.callFake(function(msg) { | ||
warnings.push(msg); | ||
}); | ||
|
||
expect(warnings.length).toEqual(1); | ||
_supply({ | ||
node: { | ||
label: ['a', 'b', 'c'] | ||
}, | ||
link: { | ||
value: [1], | ||
source: [0], | ||
target: [1] | ||
} | ||
}); | ||
|
||
expect(warnings.length).toEqual(0); | ||
}); | ||
}); | ||
|
||
describe('sankey global defaults', function() { | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially made
isIndex
into aLib
function thinking I could reuse it here... but in this context it's in principle important that an index is actually a number, to distinguish an array index from an object property that happens to be a number. That said I don't think it's possible to use a number as an object property innestedProperty
...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh we could use this for validating
selectedpoints
items as done here:plotly.js/src/lib/index.js
Lines 497 to 499 in a132b85
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... by this I mean
Lib.isIndex
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call -> cdcd620
We probably either have or should have this in other places too...
mesh3d
is the other obvious place, but it has bigger issues -> #2630