Skip to content

Commit e9599e7

Browse files
authored
Merge pull request #7870 from wf-r/Add_bottom-up_and_right-left_orientation_for_Sankey
Add directions attribute for Sankey diagrams
2 parents 445ce41 + d27db14 commit e9599e7

31 files changed

Lines changed: 664 additions & 29 deletions

draftlogs/7870_add.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- Add `direction` attribute to the Sankey trace, controlling the flow direction along the `orientation` axis [[#7870](https://github.com/plotly/plotly.js/pull/7870)], with thanks to @wf-r for the contribution!
2+
- `forward` keeps sources on the left (horizontal) or top (vertical)
3+
- `reversed` moves them to the right or bottom

src/traces/sankey/attributes.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,23 @@ var attrs = (module.exports = overrideAll(
3333
valType: 'enumerated',
3434
values: ['v', 'h'],
3535
dflt: 'h',
36-
description: 'Sets the orientation of the Sankey diagram.'
36+
description: [
37+
'Sets the orientation of the Sankey diagram.',
38+
'With `h` (the default), the flow runs horizontally.',
39+
'With `v`, the flow runs vertically.',
40+
'Use `direction` to control which side the sources are placed on.'
41+
].join(' ')
42+
},
43+
44+
direction: {
45+
valType: 'enumerated',
46+
values: ['forward', 'reversed'],
47+
dflt: 'forward',
48+
description: [
49+
'Sets the direction of the flow along the `orientation` axis.',
50+
'With `forward` (the default), sources are on the left (horizontal) or top (vertical).',
51+
'With `reversed`, sources are on the right (horizontal) or bottom (vertical).',
52+
].join(' ')
3753
},
3854

3955
valueformat: {

src/traces/sankey/defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
101101
handleDomainDefaults(traceOut, layout, coerce);
102102

103103
coerce('orientation');
104+
coerce('direction');
104105
coerce('valueformat');
105106
coerce('valuesuffix');
106107

src/traces/sankey/plot.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,14 @@ module.exports = function plot(gd, calcData) {
193193
hoverCenterX = (link.source.x1 + link.target.x0) / 2;
194194
hoverCenterY = (link.y0 + link.y1) / 2;
195195
}
196+
var vertical = link.trace.orientation === 'v';
197+
var reversed = link.trace.direction === 'reversed';
196198
var center = [hoverCenterX, hoverCenterY];
197-
if(link.trace.orientation === 'v') center.reverse();
199+
// Vertical orientation transposes x/y to match the group transform.
200+
if(vertical) center.reverse();
201+
// reversed direction additionally mirrors the flow axis (matching the translate).
202+
if(vertical && reversed) center[1] = d.parent.height - center[1];
203+
if(!vertical && reversed) center[0] = d.parent.width - center[0];
198204
center[0] += d.parent.translateX;
199205
center[1] += d.parent.translateY;
200206
return center;

src/traces/sankey/render.js

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ function sankeyModel(layout, d, traceIndex) {
3232
var trace = calcData.trace;
3333
var domain = trace.domain;
3434
var horizontal = trace.orientation === 'h';
35+
// reversed flips the source side along the flow axis: horizontal -> sources on the
36+
// right (flow leftward), vertical -> sources at the bottom (flow upward).
37+
var reversed = trace.direction === 'reversed';
3538
var nodePad = trace.node.pad;
3639
var nodeThickness = trace.node.thickness;
3740
var nodeAlign = {
@@ -285,6 +288,7 @@ function sankeyModel(layout, d, traceIndex) {
285288
trace: trace,
286289
guid: Lib.randstr(),
287290
horizontal: horizontal,
291+
reversed: reversed,
288292
width: width,
289293
height: height,
290294
nodePad: trace.node.pad,
@@ -588,6 +592,7 @@ function nodeModel(d, n) {
588592
sizeAcross: d.width,
589593
forceLayouts: d.forceLayouts,
590594
horizontal: d.horizontal,
595+
reversed: d.reversed,
591596
darkBackground: Color.color(n.color).isDark(),
592597
rgb: Color.rgb(n.color),
593598
alpha: Color.color(n.color).alpha(),
@@ -629,8 +634,21 @@ function sizeNode(rect) {
629634
function salientEnough(d) {return (d.link.width > 1 || d.linkLineWidth > 0);}
630635

631636
function sankeyTransform(d) {
632-
var offset = strTranslate(d.translateX, d.translateY);
633-
return offset + (d.horizontal ? 'matrix(1 0 0 1 0 0)' : 'matrix(0 1 1 0 0 0)');
637+
if(d.horizontal) {
638+
if(d.reversed) {
639+
// horizontal + reversed: sources on the right, flow leftward; a mirror of forward.
640+
return strTranslate(d.translateX + d.width, d.translateY) + 'matrix(-1 0 0 1 0 0)';
641+
}
642+
// horizontal + forward: sources on the left, flow rightward.
643+
return strTranslate(d.translateX, d.translateY) + 'matrix(1 0 0 1 0 0)';
644+
}
645+
if(d.reversed) {
646+
// vertical + reversed: sources at the bottom, flow upward; a mirror of forward.
647+
// Pure 90deg rotation (det +1) keeps the cross axis intact.
648+
return strTranslate(d.translateX, d.translateY + d.height) + 'matrix(0 -1 1 0 0 0)';
649+
}
650+
// vertical + forward: reflection about y=x, sources at the top, flow downward.
651+
return strTranslate(d.translateX, d.translateY) + 'matrix(0 1 1 0 0 0)';
634652
}
635653

636654
// event handling
@@ -1049,7 +1067,10 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10491067
svgTextUtils.convertToTspans(e, gd);
10501068
})
10511069
.attr('text-anchor', function(d) {
1052-
return (d.horizontal && d.left) ? 'end' : 'start';
1070+
// horizontal: aligned to the outer edge (reversed flips which side is outer).
1071+
// vertical: inside-node placement, anchored at 'start'.
1072+
if(!d.horizontal) return 'start';
1073+
return (d.left !== d.reversed) ? 'end' : 'start';
10531074
})
10541075
.attr('transform', function(d) {
10551076
var e = d3.select(this);
@@ -1059,27 +1080,35 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10591080
(nLines - 1) * LINE_SPACING - CAP_SHIFT
10601081
);
10611082

1062-
var posX = d.nodeLineWidth / 2 + TEXTPAD;
1063-
var posY = ((d.horizontal ? d.visibleHeight : d.visibleWidth) - blockHeight) / 2;
1064-
if(d.horizontal) {
1065-
if(d.left) {
1066-
posX = -posX;
1067-
} else {
1068-
posX += d.visibleWidth;
1069-
}
1083+
var pad = d.nodeLineWidth / 2 + TEXTPAD;
1084+
1085+
if(!d.horizontal) {
1086+
// vertical: label sits inside the node, centered along its length.
1087+
// forward's local flip (scale(-1,1) + rotate(90)) is a reflection,
1088+
// reversed's (rotate(90) alone) a pure rotation; the group
1089+
// matrix has the same opposing handedness. That flips the sign with which
1090+
// the CAP_SHIFT baseline correction (baked into blockHeight) lands on
1091+
// screen, so it must be applied with the opposite sign for reversed -
1092+
// otherwise the label renders about half a line height too high.
1093+
var posY = d.reversed
1094+
? (d.visibleWidth + blockHeight) / 2
1095+
: (d.visibleWidth - blockHeight) / 2;
1096+
var flipV = d.reversed ? strRotate(90) : ('scale(-1,1)' + strRotate(90));
1097+
return strTranslate(posY, pad) + flipV;
10701098
}
10711099

1072-
var flipText = d.horizontal ? '' : (
1073-
'scale(-1,1)' + strRotate(90)
1074-
);
1075-
1076-
return strTranslate(
1077-
d.horizontal ? posX : posY,
1078-
d.horizontal ? posY : posX
1079-
) + flipText;
1100+
// horizontal: center along the node length, place just past the thickness edge.
1101+
var posX = pad;
1102+
var posY = (d.visibleHeight - blockHeight) / 2;
1103+
if(d.left) {
1104+
posX = -posX;
1105+
} else {
1106+
posX += d.visibleWidth;
1107+
}
1108+
return strTranslate(posX, posY) + (d.reversed ? 'scale(-1,1)' : '');
10801109
});
10811110

10821111
nodeLabel
10831112
.transition()
10841113
.ease(c.ease).duration(c.duration);
1085-
};
1114+
};

src/traces/sankey/select.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,30 @@ module.exports = function selectPoints(searchInfo, selectionTester) {
55
var selection = [];
66
var fullData = cd[0].trace;
77

8-
var nodes = fullData._sankey.graph.nodes;
8+
var model = fullData._sankey;
9+
var nodes = model.graph.nodes;
10+
var vertical = fullData.orientation === 'v';
11+
var reversed = fullData.direction === 'reversed';
912

1013
for(var i = 0; i < nodes.length; i++) {
1114
var node = nodes[i];
1215
if(node.partOfGroup) continue; // Those are invisible
1316

14-
// Position of node's centroid
15-
var pos = [(node.x0 + node.x1) / 2, (node.y0 + node.y1) / 2];
17+
// Position of node's centroid in the layout frame
18+
var cx = (node.x0 + node.x1) / 2;
19+
var cy = (node.y0 + node.y1) / 2;
1620

17-
// Swap x and y if trace is vertical
18-
if(fullData.orientation === 'v') pos.reverse();
21+
// Mirror/swap to match the group transform applied in render.js (sankeyTransform):
22+
// h + forward: (cx, cy)
23+
// h + reversed: (width - cx, cy) -> matrix(-1 0 0 1) + translate(width, 0)
24+
// v + forward: (cy, cx) -> matrix( 0 1 1 0) (swap x/y)
25+
// v + reversed: (cy, height - cx) -> matrix( 0 -1 1 0) + translate(0, height)
26+
var pos;
27+
if(vertical) {
28+
pos = [cy, reversed ? model.height - cx : cx];
29+
} else {
30+
pos = [reversed ? model.width - cx : cx, cy];
31+
}
1932

2033
if(selectionTester && selectionTester.contains(pos, false, i, searchInfo)) {
2134
selection.push({

src/types/generated/schema.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6901,6 +6901,11 @@ export interface SankeyData {
69016901
arrangement?: 'snap' | 'perpendicular' | 'freeform' | 'fixed';
69026902
/** Assigns extra data each datum. This may be useful when listening to hover, click and selection events. Note that, *scatter* traces also appends customdata items in the markers DOM elements */
69036903
customdata?: Datum[] | Datum[][] | TypedArray;
6904+
/**
6905+
* Sets the direction of the flow along the `orientation` axis. With `forward` (the default), sources are on the left (horizontal) or top (vertical). With `reversed`, sources are on the right (horizontal) or bottom (vertical).
6906+
* @default 'forward'
6907+
*/
6908+
direction?: 'forward' | 'reversed';
69046909
domain?: Domain;
69056910
/**
69066911
* Determines what trace information appears on hover. If `none` or `skip` are set, no information is displayed upon hovering. But, if `none` is set, click and hover events are still fired. Note that this attribute is superseded by `node.hoverinfo` and `node.hoverinfo` for nodes and links respectively.
@@ -7073,7 +7078,7 @@ export interface SankeyData {
70737078
y?: Datum[] | Datum[][] | TypedArray;
70747079
};
70757080
/**
7076-
* Sets the orientation of the Sankey diagram.
7081+
* Sets the orientation of the Sankey diagram. With `h` (the default), the flow runs horizontally. With `v`, the flow runs vertically. Use `direction` to control which side the sources are placed on.
70777082
* @default 'h'
70787083
*/
70797084
orientation?: 'v' | 'h';
37.9 KB
Loading
36.7 KB
Loading
37.9 KB
Loading

0 commit comments

Comments
 (0)