Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7870_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `right-left` and `bottom-up` values to Sankey `orientation`, with `left-right` and `top-down` as aliases for `h` and `v` [[#7870](https://github.com/plotly/plotly.js/pull/7870)]
11 changes: 9 additions & 2 deletions src/traces/sankey/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ var attrs = (module.exports = overrideAll(

orientation: {
valType: 'enumerated',
values: ['v', 'h'],
values: ['v', 'h', 'left-right', 'right-left', 'top-down', 'bottom-up'],
dflt: 'h',
description: 'Sets the orientation of the Sankey diagram.'
description: [
'Sets the orientation of the Sankey diagram.',
'`left-right` (synonym of the legacy value `h`) places sources on the left',
'with the flow running rightward; `right-left` places sources on the right',
'with the flow running leftward; `top-down` (synonym of the legacy value `v`)',
'places sources at the top with the flow running downward; `bottom-up` places',
'sources at the bottom with the flow running upward.'
].join(' ')
},

valueformat: {
Expand Down
9 changes: 8 additions & 1 deletion src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,15 @@ module.exports = function plot(gd, calcData) {
hoverCenterX = (link.source.x1 + link.target.x0) / 2;
hoverCenterY = (link.y0 + link.y1) / 2;
}
var orientation = link.trace.orientation;
var center = [hoverCenterX, hoverCenterY];
if(link.trace.orientation === 'v') center.reverse();
// Vertical orientations transpose x/y to match the group transform.
if(orientation === 'v' || orientation === 'top-down' || orientation === 'bottom-up') {
center.reverse();
}
// bottom-up / right-left additionally mirror the flow axis (matching the translate).
if(orientation === 'bottom-up') center[1] = d.parent.height - center[1];
if(orientation === 'right-left') center[0] = d.parent.width - center[0];
center[0] += d.parent.translateX;
center[1] += d.parent.translateY;
return center;
Expand Down
66 changes: 46 additions & 20 deletions src/traces/sankey/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ function sankeyModel(layout, d, traceIndex) {
var calcData = unwrap(d);
var trace = calcData.trace;
var domain = trace.domain;
var horizontal = trace.orientation === 'h';
var horizontal = trace.orientation === 'h' ||
trace.orientation === 'left-right' ||
trace.orientation === 'right-left';
var rightLeft = trace.orientation === 'right-left';
var bottomUp = trace.orientation === 'bottom-up';
var nodePad = trace.node.pad;
var nodeThickness = trace.node.thickness;
var nodeAlign = {
Expand Down Expand Up @@ -271,6 +275,8 @@ function sankeyModel(layout, d, traceIndex) {
trace: trace,
guid: Lib.randstr(),
horizontal: horizontal,
rightLeft: rightLeft,
bottomUp: bottomUp,
width: width,
height: height,
nodePad: trace.node.pad,
Expand Down Expand Up @@ -577,6 +583,8 @@ function nodeModel(d, n) {
sizeAcross: d.width,
forceLayouts: d.forceLayouts,
horizontal: d.horizontal,
rightLeft: d.rightLeft,
bottomUp: d.bottomUp,
darkBackground: tc.getBrightness() <= 128,
tinyColorHue: Color.tinyRGB(tc),
tinyColorAlpha: tc.getAlpha(),
Expand Down Expand Up @@ -618,8 +626,21 @@ function sizeNode(rect) {
function salientEnough(d) {return (d.link.width > 1 || d.linkLineWidth > 0);}

function sankeyTransform(d) {
var offset = strTranslate(d.translateX, d.translateY);
return offset + (d.horizontal ? 'matrix(1 0 0 1 0 0)' : 'matrix(0 1 1 0 0 0)');
if(d.horizontal) {
if(d.rightLeft) {
// right-left: sources on the right, flow leftward; horizontal mirror of left-right.
return strTranslate(d.translateX + d.width, d.translateY) + 'matrix(-1 0 0 1 0 0)';
}
// h / left-right: sources on the left, flow rightward.
return strTranslate(d.translateX, d.translateY) + 'matrix(1 0 0 1 0 0)';
}
if(d.bottomUp) {
// bottom-up: sources at the bottom, flow upward; a vertical mirror of top-down.
// Pure 90deg rotation (det +1) keeps the cross axis intact.
return strTranslate(d.translateX, d.translateY + d.height) + 'matrix(0 -1 1 0 0 0)';
}
// top-down (also 'v'): reflection about y=x, sources at the top, flow downward.
return strTranslate(d.translateX, d.translateY) + 'matrix(0 1 1 0 0 0)';
}

// event handling
Expand Down Expand Up @@ -1048,7 +1069,10 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
svgTextUtils.convertToTspans(e, gd);
})
.attr('text-anchor', function(d) {
return (d.horizontal && d.left) ? 'end' : 'start';
// vertical: labels are centered over the node. horizontal: aligned to the outer
// edge (right-left mirrors the layout, so the outer side and anchor flip).
if(!d.horizontal) return 'middle';
return (d.left !== d.rightLeft) ? 'end' : 'start';
})
.attr('transform', function(d) {
var e = d3.select(this);
Expand All @@ -1058,24 +1082,26 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
(nLines - 1) * LINE_SPACING - CAP_SHIFT
);

var posX = d.nodeLineWidth / 2 + TEXTPAD;
var posY = ((d.horizontal ? d.visibleHeight : d.visibleWidth) - blockHeight) / 2;
if(d.horizontal) {
if(d.left) {
posX = -posX;
} else {
posX += d.visibleWidth;
}
}
var pad = d.nodeLineWidth / 2 + TEXTPAD;

var flipText = d.horizontal ? '' : (
'scale(-1,1)' + strRotate(90)
);
if(!d.horizontal) {
var posY = d.visibleHeight / 2;
// last Column (originalLayer === 1): put label towards center.
var posX = d.bottomUp ?
(d.left ? -(pad + CAP_SHIFT * d.textFont.size) : (d.visibleWidth + pad)) : (d.left ? -pad : (d.visibleWidth + pad + CAP_SHIFT * d.textFont.size));
var flipV = d.bottomUp ? strRotate(90) : ('scale(-1,1)' + strRotate(90));
return strTranslate(posX, posY) + flipV;
}

return strTranslate(
d.horizontal ? posX : posY,
d.horizontal ? posY : posX
) + flipText;
// horizontal: center along the node length, place just past the thickness edge.
var posX = pad;
var posY = (d.visibleHeight - blockHeight) / 2;
if(d.left) {
posX = -posX;
} else {
posX += d.visibleWidth;
}
return strTranslate(posX, posY) + (d.rightLeft ? 'scale(-1,1)' : '');
});

nodeLabel
Expand Down
48 changes: 48 additions & 0 deletions test/jasmine/tests/sankey_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ describe('sankey tests', function() {
.toEqual(attributes.domain.y.dflt, 'y domain by default');
});

it('coerces the vertical orientation values', function() {
['h', 'v', 'left-right', 'right-left', 'top-down', 'bottom-up'].forEach(function(o) {
expect(_supply({orientation: o}).orientation)
.toBe(o, o + ' is a valid orientation');
});
expect(_supply({orientation: 'sideways'}).orientation)
.toBe(attributes.orientation.dflt, 'invalid orientation falls back to default');
});

it('\'Sankey\' layout dependent specification should have proper types',
function() {
var fullTrace = _supplyWithLayout({}, {font: {
Expand Down Expand Up @@ -372,6 +381,45 @@ describe('sankey tests', function() {
});
afterEach(destroyGraphDiv);

it('applies the correct group transform per orientation', function(done) {
function groupTransform() {
return d3Select('.sankey').attr('transform');
}
function plotWith(orientation) {
var fig = Lib.extendDeep({}, mock);
fig.data[0].orientation = orientation;
// newPlot re-enters the trace, so the transform is set synchronously
// (no mid-transition interpolation to race against).
return Plotly.newPlot(gd, fig);
}

plotWith('h')
.then(function() {
expect(groupTransform()).toContain('matrix(1 0 0 1 0 0)');
return plotWith('left-right'); // legacy synonym of h
})
.then(function() {
expect(groupTransform()).toContain('matrix(1 0 0 1 0 0)');
return plotWith('right-left');
})
.then(function() {
expect(groupTransform()).toContain('matrix(-1 0 0 1 0 0)');
return plotWith('top-down');
})
.then(function() {
expect(groupTransform()).toContain('matrix(0 1 1 0 0 0)');
return plotWith('v'); // legacy synonym of top-down
})
.then(function() {
expect(groupTransform()).toContain('matrix(0 1 1 0 0 0)');
return plotWith('bottom-up');
})
.then(function() {
expect(groupTransform()).toContain('matrix(0 -1 1 0 0 0)');
})
.then(done, done.fail);
});

it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) {
var mockCopy = Lib.extendDeep({}, mock);
var mockCopy2 = Lib.extendDeep({}, mockDark);
Expand Down