Skip to content

Commit 348aa23

Browse files
author
wfr
committed
Add bottom-up and right-left orientations to Sankey
\Extend the Sankey 'orientation' attribute with four direction-namedvalues while keeping the legacy ones as synonyms: - left-right (synonym of h): sources left, flow rightward - right-left: sources right, flow leftward (new) - top-down (synonym of v): sources top, flow downward - bottom-up: sources bottom, flow upward (new)right-left and bottom-up are mirrors of the existing horizontal andvertical layouts, each expressed as a single group-level matrix plustranslate in sankeyTransform(). right-left counts as horizontal forlayout sizing, dragging and hover-axis mapping; only the group ismirrored. Node labels get an updated counter-transform so glyphs stayupright, and right-left flips the outer-side text-anchor.plot.js: replace the '=== v' link-hover check with a proper verticaltest so left-right/right-left are not transposed, and mirror the flowaxis for bottom-up (y) and right-left (x).Default stays h; existing h/v figures render identically.Tests: orientation coercion for all values plus invalid fallback, anda group-transform assertion per orientation.
1 parent 61cca14 commit 348aa23

4 files changed

Lines changed: 95 additions & 10 deletions

File tree

src/traces/sankey/attributes.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ var attrs = (module.exports = overrideAll(
3131

3232
orientation: {
3333
valType: 'enumerated',
34-
values: ['v', 'h'],
34+
values: ['v', 'h', 'left-right', 'right-left', 'top-down', 'bottom-up'],
3535
dflt: 'h',
36-
description: 'Sets the orientation of the Sankey diagram.'
36+
description: [
37+
'Sets the orientation of the Sankey diagram.',
38+
'`left-right` (synonym of the legacy value `h`) places sources on the left',
39+
'with the flow running rightward; `right-left` places sources on the right',
40+
'with the flow running leftward; `top-down` (synonym of the legacy value `v`)',
41+
'places sources at the top with the flow running downward; `bottom-up` places',
42+
'sources at the bottom with the flow running upward.'
43+
].join(' ')
3744
},
3845

3946
valueformat: {

src/traces/sankey/plot.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,15 @@ 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 orientation = link.trace.orientation;
196197
var center = [hoverCenterX, hoverCenterY];
197-
if(link.trace.orientation === 'v') center.reverse();
198+
// Vertical orientations transpose x/y to match the group transform.
199+
if(orientation === 'v' || orientation === 'top-down' || orientation === 'bottom-up') {
200+
center.reverse();
201+
}
202+
// bottom-up / right-left additionally mirror the flow axis (matching the translate).
203+
if(orientation === 'bottom-up') center[1] = d.parent.height - center[1];
204+
if(orientation === 'right-left') center[0] = d.parent.width - center[0];
198205
center[0] += d.parent.translateX;
199206
center[1] += d.parent.translateY;
200207
return center;

src/traces/sankey/render.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ function sankeyModel(layout, d, traceIndex) {
3232
var calcData = unwrap(d);
3333
var trace = calcData.trace;
3434
var domain = trace.domain;
35-
var horizontal = trace.orientation === 'h';
35+
var horizontal = trace.orientation === 'h' ||
36+
trace.orientation === 'left-right' ||
37+
trace.orientation === 'right-left';
38+
var rightLeft = trace.orientation === 'right-left';
39+
var bottomUp = trace.orientation === 'bottom-up';
3640
var nodePad = trace.node.pad;
3741
var nodeThickness = trace.node.thickness;
3842
var nodeAlign = {
@@ -271,6 +275,8 @@ function sankeyModel(layout, d, traceIndex) {
271275
trace: trace,
272276
guid: Lib.randstr(),
273277
horizontal: horizontal,
278+
rightLeft: rightLeft,
279+
bottomUp: bottomUp,
274280
width: width,
275281
height: height,
276282
nodePad: trace.node.pad,
@@ -577,6 +583,8 @@ function nodeModel(d, n) {
577583
sizeAcross: d.width,
578584
forceLayouts: d.forceLayouts,
579585
horizontal: d.horizontal,
586+
rightLeft: d.rightLeft,
587+
bottomUp: d.bottomUp,
580588
darkBackground: tc.getBrightness() <= 128,
581589
tinyColorHue: Color.tinyRGB(tc),
582590
tinyColorAlpha: tc.getAlpha(),
@@ -618,8 +626,21 @@ function sizeNode(rect) {
618626
function salientEnough(d) {return (d.link.width > 1 || d.linkLineWidth > 0);}
619627

620628
function sankeyTransform(d) {
621-
var offset = strTranslate(d.translateX, d.translateY);
622-
return offset + (d.horizontal ? 'matrix(1 0 0 1 0 0)' : 'matrix(0 1 1 0 0 0)');
629+
if(d.horizontal) {
630+
if(d.rightLeft) {
631+
// right-left: sources on the right, flow leftward; horizontal mirror of left-right.
632+
return strTranslate(d.translateX + d.width, d.translateY) + 'matrix(-1 0 0 1 0 0)';
633+
}
634+
// h / left-right: sources on the left, flow rightward.
635+
return strTranslate(d.translateX, d.translateY) + 'matrix(1 0 0 1 0 0)';
636+
}
637+
if(d.bottomUp) {
638+
// bottom-up: sources at the bottom, flow upward; a vertical mirror of top-down.
639+
// Pure 90deg rotation (det +1) keeps the cross axis intact.
640+
return strTranslate(d.translateX, d.translateY + d.height) + 'matrix(0 -1 1 0 0 0)';
641+
}
642+
// top-down (also 'v'): reflection about y=x, sources at the top, flow downward.
643+
return strTranslate(d.translateX, d.translateY) + 'matrix(0 1 1 0 0 0)';
623644
}
624645

625646
// event handling
@@ -1048,7 +1069,8 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10481069
svgTextUtils.convertToTspans(e, gd);
10491070
})
10501071
.attr('text-anchor', function(d) {
1051-
return (d.horizontal && d.left) ? 'end' : 'start';
1072+
// right-left mirrors the layout horizontally, so the outer side (and anchor) flips.
1073+
return (d.horizontal && (d.left !== d.rightLeft)) ? 'end' : 'start';
10521074
})
10531075
.attr('transform', function(d) {
10541076
var e = d3.select(this);
@@ -1068,9 +1090,10 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10681090
}
10691091
}
10701092

1071-
var flipText = d.horizontal ? '' : (
1072-
'scale(-1,1)' + strRotate(90)
1073-
);
1093+
var flipText = d.horizontal ?
1094+
(d.rightLeft ? 'scale(-1,1)' : '') : (
1095+
d.bottomUp ? strRotate(90) : ('scale(-1,1)' + strRotate(90))
1096+
);
10741097

10751098
return strTranslate(
10761099
d.horizontal ? posX : posY,

test/jasmine/tests/sankey_test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,15 @@ describe('sankey tests', function() {
152152
.toEqual(attributes.domain.y.dflt, 'y domain by default');
153153
});
154154

155+
it('coerces the vertical orientation values', function() {
156+
['h', 'v', 'left-right', 'right-left', 'top-down', 'bottom-up'].forEach(function(o) {
157+
expect(_supply({orientation: o}).orientation)
158+
.toBe(o, o + ' is a valid orientation');
159+
});
160+
expect(_supply({orientation: 'sideways'}).orientation)
161+
.toBe(attributes.orientation.dflt, 'invalid orientation falls back to default');
162+
});
163+
155164
it('\'Sankey\' layout dependent specification should have proper types',
156165
function() {
157166
var fullTrace = _supplyWithLayout({}, {font: {
@@ -372,6 +381,45 @@ describe('sankey tests', function() {
372381
});
373382
afterEach(destroyGraphDiv);
374383

384+
it('applies the correct group transform per orientation', function(done) {
385+
function groupTransform() {
386+
return d3Select('.sankey').attr('transform');
387+
}
388+
function plotWith(orientation) {
389+
var fig = Lib.extendDeep({}, mock);
390+
fig.data[0].orientation = orientation;
391+
// newPlot re-enters the trace, so the transform is set synchronously
392+
// (no mid-transition interpolation to race against).
393+
return Plotly.newPlot(gd, fig);
394+
}
395+
396+
plotWith('h')
397+
.then(function() {
398+
expect(groupTransform()).toContain('matrix(1 0 0 1 0 0)');
399+
return plotWith('left-right'); // legacy synonym of h
400+
})
401+
.then(function() {
402+
expect(groupTransform()).toContain('matrix(1 0 0 1 0 0)');
403+
return plotWith('right-left');
404+
})
405+
.then(function() {
406+
expect(groupTransform()).toContain('matrix(-1 0 0 1 0 0)');
407+
return plotWith('top-down');
408+
})
409+
.then(function() {
410+
expect(groupTransform()).toContain('matrix(0 1 1 0 0 0)');
411+
return plotWith('v'); // legacy synonym of top-down
412+
})
413+
.then(function() {
414+
expect(groupTransform()).toContain('matrix(0 1 1 0 0 0)');
415+
return plotWith('bottom-up');
416+
})
417+
.then(function() {
418+
expect(groupTransform()).toContain('matrix(0 -1 1 0 0 0)');
419+
})
420+
.then(done, done.fail);
421+
});
422+
375423
it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) {
376424
var mockCopy = Lib.extendDeep({}, mock);
377425
var mockCopy2 = Lib.extendDeep({}, mockDark);

0 commit comments

Comments
 (0)