Skip to content

Commit 320a4c4

Browse files
etpinardantoinerg
authored andcommitted
sankey: add attributes node.(x|y) and update them on drag
1 parent 4b21575 commit 320a4c4

File tree

6 files changed

+241
-105
lines changed

6 files changed

+241
-105
lines changed

src/traces/sankey/attributes.js

+12
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ var attrs = module.exports = overrideAll({
102102
'Multiple groups can be specified.'
103103
].join(' ')
104104
},
105+
x: {
106+
valType: 'data_array',
107+
dflt: [],
108+
role: 'info',
109+
description: 'The normalized horizontal position of the node.'
110+
},
111+
y: {
112+
valType: 'data_array',
113+
dflt: [],
114+
role: 'info',
115+
description: 'The normalized vertical position of the node.'
116+
},
105117
color: {
106118
valType: 'color',
107119
role: 'style',

src/traces/sankey/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3333
}
3434
coerceNode('label');
3535
coerceNode('groups');
36+
coerceNode('x');
37+
coerceNode('y');
3638
coerceNode('pad');
3739
coerceNode('thickness');
3840
coerceNode('line.color');

src/traces/sankey/render.js

+65-11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var repeat = gup.repeat;
2323
var unwrap = gup.unwrap;
2424
var interpolateNumber = require('d3-interpolate').interpolateNumber;
2525

26+
var Plotly = require('../../plot_api/plot_api');
27+
2628
// view models
2729

2830
function sankeyModel(layout, d, traceIndex) {
@@ -163,6 +165,25 @@ function sankeyModel(layout, d, traceIndex) {
163165
}
164166
computeLinkConcentrations();
165167

168+
// Force node position
169+
if(trace.node.x.length !== 0 && trace.node.y.length !== 0) {
170+
var i;
171+
for(i = 0; i < Math.min(trace.node.x.length, trace.node.y.length, graph.nodes.length); i++) {
172+
if(trace.node.x[i] && trace.node.y[i]) {
173+
var pos = [trace.node.x[i] * width, trace.node.y[i] * height];
174+
graph.nodes[i].x0 = pos[0] - nodeThickness / 2;
175+
graph.nodes[i].x1 = pos[0] + nodeThickness / 2;
176+
177+
var nodeHeight = graph.nodes[i].y1 - graph.nodes[i].y0;
178+
graph.nodes[i].y0 = pos[1] - nodeHeight / 2;
179+
graph.nodes[i].y1 = pos[1] + nodeHeight / 2;
180+
}
181+
}
182+
// Update links
183+
sankey.update(graph);
184+
}
185+
186+
166187
return {
167188
circular: circular,
168189
key: traceIndex,
@@ -399,6 +420,7 @@ function nodeModel(d, n) {
399420
partOfGroup: n.partOfGroup || false,
400421
group: n.group,
401422
traceId: d.key,
423+
trace: d.trace,
402424
node: n,
403425
nodePad: d.nodePad,
404426
nodeLineColor: d.nodeLineColor,
@@ -425,7 +447,8 @@ function nodeModel(d, n) {
425447
graph: d.graph,
426448
arrangement: d.arrangement,
427449
uniqueNodeLabelPathId: [d.guid, d.key, key].join('_'),
428-
interactionState: d.interactionState
450+
interactionState: d.interactionState,
451+
figure: d
429452
};
430453
}
431454

@@ -509,7 +532,7 @@ function attachPointerEvents(selection, sankey, eventSet) {
509532
});
510533
}
511534

512-
function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
535+
function attachDragHandler(sankeyNode, sankeyLink, callbacks, gd) {
513536
var dragBehavior = d3.behavior.drag()
514537
.origin(function(d) {
515538
return {
@@ -520,6 +543,9 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
520543

521544
.on('dragstart', function(d) {
522545
if(d.arrangement === 'fixed') return;
546+
Lib.ensureSingle(gd._fullLayout._infolayer, 'g', 'dragcover', function(s) {
547+
gd._fullLayout._dragCover = s;
548+
});
523549
Lib.raiseToTop(this);
524550
d.interactionState.dragInProgress = d.node;
525551

@@ -533,9 +559,9 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
533559
if(d.forceLayouts[forceKey]) {
534560
d.forceLayouts[forceKey].alpha(1);
535561
} else { // make a forceLayout if needed
536-
attachForce(sankeyNode, forceKey, d);
562+
attachForce(sankeyNode, forceKey, d, gd);
537563
}
538-
startForce(sankeyNode, sankeyLink, d, forceKey);
564+
startForce(sankeyNode, sankeyLink, d, forceKey, gd);
539565
}
540566
})
541567

@@ -553,8 +579,9 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
553579
d.node.x0 = x - d.visibleWidth / 2;
554580
d.node.x1 = x + d.visibleWidth / 2;
555581
}
556-
d.node.y0 = Math.max(0, Math.min(d.size - d.visibleHeight, y));
557-
d.node.y1 = d.node.y0 + d.visibleHeight;
582+
y = Math.max(0, Math.min(d.size - d.visibleHeight / 2, y));
583+
d.node.y0 = y - d.visibleHeight / 2;
584+
d.node.y1 = y + d.visibleHeight / 2;
558585
}
559586

560587
saveCurrentDragPosition(d.node);
@@ -570,14 +597,15 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
570597
d.node.childrenNodes[i].x = d.node.x;
571598
d.node.childrenNodes[i].y = d.node.y;
572599
}
600+
if(d.arrangement !== 'snap') persistFinalNodePositions(d, gd);
573601
});
574602

575603
sankeyNode
576604
.on('.drag', null) // remove possible previous handlers
577605
.call(dragBehavior);
578606
}
579607

580-
function attachForce(sankeyNode, forceKey, d) {
608+
function attachForce(sankeyNode, forceKey, d, gd) {
581609
// Attach force to nodes in the same column (same x coordinate)
582610
switchToForceFormat(d.graph.nodes);
583611
var nodes = d.graph.nodes
@@ -590,11 +618,11 @@ function attachForce(sankeyNode, forceKey, d) {
590618
.radius(function(n) {return n.dy / 2 + d.nodePad / 2;})
591619
.strength(1)
592620
.iterations(c.forceIterations))
593-
.force('constrain', snappingForce(sankeyNode, forceKey, nodes, d))
621+
.force('constrain', snappingForce(sankeyNode, forceKey, nodes, d, gd))
594622
.stop();
595623
}
596624

597-
function startForce(sankeyNode, sankeyLink, d, forceKey) {
625+
function startForce(sankeyNode, sankeyLink, d, forceKey, gd) {
598626
window.requestAnimationFrame(function faster() {
599627
var i;
600628
for(i = 0; i < c.forceTicksPerFrame; i++) {
@@ -609,6 +637,14 @@ function startForce(sankeyNode, sankeyLink, d, forceKey) {
609637

610638
if(d.forceLayouts[forceKey].alpha() > 0) {
611639
window.requestAnimationFrame(faster);
640+
} else {
641+
// Make sure the final x position is equal to its original value
642+
// necessary because the force simulation will have numerical error
643+
var x = d.node.originalX;
644+
d.node.x0 = x - d.visibleWidth / 2;
645+
d.node.x1 = x + d.visibleWidth / 2;
646+
647+
persistFinalNodePositions(d, gd);
612648
}
613649
});
614650
}
@@ -628,13 +664,31 @@ function snappingForce(sankeyNode, forceKey, nodes, d) {
628664
maxVelocity = Math.max(maxVelocity, Math.abs(n.vx), Math.abs(n.vy));
629665
}
630666
if(!d.interactionState.dragInProgress && maxVelocity < 0.1 && d.forceLayouts[forceKey].alpha() > 0) {
631-
d.forceLayouts[forceKey].alpha(0);
667+
d.forceLayouts[forceKey].alpha(0); // This will stop the animation loop
632668
}
633669
};
634670
}
635671

636672
// basic data utilities
637673

674+
function persistFinalNodePositions(d, gd) {
675+
var x = [];
676+
var y = [];
677+
for(var i = 0; i < d.graph.nodes.length; i++) {
678+
var nodeX = (d.graph.nodes[i].x0 + d.graph.nodes[i].x1) / 2;
679+
var nodeY = (d.graph.nodes[i].y0 + d.graph.nodes[i].y1) / 2;
680+
x.push(nodeX / d.figure.width);
681+
y.push(nodeY / d.figure.height);
682+
}
683+
Plotly.restyle(gd, {
684+
'node.x': [x],
685+
'node.y': [y]
686+
}, d.trace.index)
687+
.then(function() {
688+
if(gd._fullLayout._dragCover) gd._fullLayout._dragCover.remove();
689+
});
690+
}
691+
638692
function persistOriginalPlace(nodes) {
639693
var distinctLayerPositions = [];
640694
var i;
@@ -795,7 +849,7 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
795849

796850
sankeyNode
797851
.call(attachPointerEvents, sankey, callbacks.nodeEvents)
798-
.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink
852+
.call(attachDragHandler, sankeyLink, callbacks, gd); // has to be here as it binds sankeyLink
799853

800854
sankeyNode.transition()
801855
.ease(c.ease).duration(c.duration)

test/image/baselines/sankey_x_y.png

34.1 KB
Loading

test/image/mocks/sankey_x_y.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"arrangement": "freeform",
6+
"node": {
7+
"pad": 5,
8+
"label": ["0", "1", "2", "3", "4", "5"],
9+
"x": [0.128, 0.128, 0.559, 0.785, 0.352, 0.593],
10+
"y": [0.738, 0.165, 0.205, 0.390, 0.165, 0.733]
11+
},
12+
"link": {
13+
"source": [
14+
0, 0, 1, 2, 5, 4, 3, 5
15+
],
16+
"target": [
17+
5, 3, 4, 3, 0, 2, 2, 3
18+
],
19+
"value": [
20+
1, 2, 1, 1, 1, 1, 1, 2
21+
]
22+
}
23+
}],
24+
"layout": {
25+
"title": "Sankey with manually positioned node",
26+
"width": 800,
27+
"height": 800
28+
}
29+
}

0 commit comments

Comments
 (0)