diff --git a/README.md b/README.md
index 6f2f08a..38309b5 100644
--- a/README.md
+++ b/README.md
@@ -27,9 +27,21 @@ var sankey = d3.sankey();
## API Reference
-# d3.sankey() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
+# d3.sankeyTop() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
-Constructs a new Sankey generator with the default settings.
+Constructs a new top-oriented Sankey generator with the default settings.
+
+# d3.sankeyRight() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
+
+Constructs a new right-oriented Sankey generator with the default settings.
+
+# d3.sankeyBottom() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
+
+Constructs a new bottom-oriented Sankey generator with the default settings.
+
+# d3.sankeyLeft() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
+
+Constructs a new left-oriented Sankey generator with the default settings.
# sankey(arguments…) [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
@@ -100,6 +112,10 @@ For convenience, a link’s source and target may be initialized using numeric o
* *link*.width - the link’s width (proportional to *link*.value)
* *link*.index - the zero-based index of *link* within the array of links
+# sankey.linkShape() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
+
+Returns a [link shape](https://github.com/d3/d3-shape#links) suitable for rendering paths between the nodes of this Sankey diagram. This will return either a [horizontal](#sankeyLinkHorizontal) or [vertical](#sankeyLinkVertical) link shape.
+
# sankey.linkSort([sort]) [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
If *sort* is specified, sets the link sort method and returns this Sankey generator. If *sort* is not specified, returns the current link sort method, which defaults to *undefined*, indicating that vertical order of links within each node will be determined automatically by the layout. If *sort* is null, the order is fixed by the input. Otherwise, the specified *sort* function determines the order; the function is passed two links, and must return a value less than 0 if the first link should be above the second, and a value greater than 0 if the second link should be above the first, or 0 if the order is not specified.
@@ -190,49 +206,39 @@ If *iterations* is specified, sets the number of relaxation iterations when [gen
See [*sankey*.nodeAlign](#sankey_nodeAlign).
-# d3.sankeyLeft(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
+# d3.sankeyAlignLeft(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
[](https://observablehq.com/@d3/sankey-diagram?align=left)
Returns *node*.depth.
-# d3.sankeyRight(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
+# d3.sankeyAlignRight(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
[](https://observablehq.com/@d3/sankey-diagram?align=right)
Returns *n* - 1 - *node*.height.
-# d3.sankeyCenter(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
+# d3.sankeyAlignCenter(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
[](https://observablehq.com/@d3/sankey-diagram?align=center)
-Like [d3.sankeyLeft](#sankeyLeft), except that nodes without any incoming links are moved as right as possible.
+Like [d3.sankeyAlignLeft](#sankeyAlignLeft), except that nodes without any incoming links are moved as right as possible.
-# d3.sankeyJustify(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
+# d3.sankeyAlignJustify(node, n) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
[](https://observablehq.com/@d3/sankey-diagram)
-Like [d3.sankeyLeft](#sankeyLeft), except that nodes without any outgoing links are moved to the far right.
+Like [d3.sankeyAlignLeft](#sankeyAlignLeft), except that nodes without any outgoing links are moved to the far right.
### Links
-# d3.sankeyLinkHorizontal() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankeyLinkHorizontal.js "Source")
+# d3.sankeyLinkHorizontal() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankeyLink.js "Source")
-Returns a [horizontal link shape](https://github.com/d3/d3-shape/blob/master/README.md#linkHorizontal) suitable for a Sankey diagram. The [source accessor](https://github.com/d3/d3-shape/blob/master/README.md#link_source) is defined as:
+Returns a [horizontal link shape](https://github.com/d3/d3-shape/blob/master/README.md#linkHorizontal) suitable for a Sankey diagram rendered in a horizontal orientation.
-```js
-function source(d) {
- return [d.source.x1, d.y0];
-}
-```
-
-The [target accessor](https://github.com/d3/d3-shape/blob/master/README.md#link_target) is defined as:
+# d3.sankeyLinkVertical() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankeyLink.js "Source")
-```js
-function target(d) {
- return [d.target.x0, d.y1];
-}
-```
+Returns a [vertical link shape](https://github.com/d3/d3-shape/blob/master/README.md#linkVertical) suitable for a Sankey diagram rendered in a vertical orientation.
For example, to render the links of a Sankey diagram in SVG, you might say:
@@ -244,6 +250,6 @@ svg.append("g")
.selectAll("path")
.data(graph.links)
.join("path")
- .attr("d", d3.sankeyLinkHorizontal())
+ .attr("d", graph.linkShape())
.attr("stroke-width", function(d) { return d.width; });
```
diff --git a/src/index.js b/src/index.js
index 8ee1bdd..9fee22e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,3 @@
-export {default as sankey} from "./sankey.js";
-export {center as sankeyCenter, left as sankeyLeft, right as sankeyRight, justify as sankeyJustify} from "./align.js";
-export {default as sankeyLinkHorizontal} from "./sankeyLinkHorizontal.js";
+export {sankeyTop, sankeyRight, sankeyBottom, sankeyLeft} from "./sankey.js";
+export {center as sankeyAlignCenter, left as sankeyAlignLeft, right as sankeyAlignRight, justify as sankeyAlignJustify} from "./align.js";
+export {sankeyLinkHorizontal, sankeyLinkVertical} from "./sankeyLink.js";
diff --git a/src/sankey.js b/src/sankey.js
index 56ad642..34c4ab9 100644
--- a/src/sankey.js
+++ b/src/sankey.js
@@ -1,7 +1,21 @@
import {max, min, sum} from "d3-array";
import {justify} from "./align.js";
+import {transformTop, transformRight, transformBottom, transformLeft} from "./transform.js";
+import {sankeyLinkHorizontal, sankeyLinkVertical} from "./sankeyLink.js";
import constant from "./constant.js";
+const top = 1,
+ right = 2,
+ bottom = 3,
+ left = 4;
+
+const transforms = {
+ [top]: transformTop,
+ [right]: transformRight,
+ [bottom]: transformBottom,
+ [left]: transformLeft
+};
+
function ascendingSourceBreadth(a, b) {
return ascendingBreadth(a.source, b.source) || a.index - b.index;
}
@@ -51,12 +65,13 @@ function computeLinkBreadths({nodes}) {
}
}
-export default function Sankey() {
+function Sankey(orientation) {
let x0 = 0, y0 = 0, x1 = 1, y1 = 1; // extent
let dx = 24; // nodeWidth
let dy = 8, py; // nodePadding
let id = defaultId;
let align = justify;
+ let transform = transforms[orientation];
let sort;
let linkSort;
let nodes = defaultNodes;
@@ -65,15 +80,35 @@ export default function Sankey() {
function sankey() {
const graph = {nodes: nodes.apply(null, arguments), links: links.apply(null, arguments)};
+ transformExtents();
computeNodeLinks(graph);
computeNodeValues(graph);
computeNodeDepths(graph);
computeNodeHeights(graph);
computeNodeBreadths(graph);
computeLinkBreadths(graph);
+ transformNodes(graph);
return graph;
}
+ function transformExtents() {
+ const transformedExtents = transform(x0, y0, x1, y1);
+ x0 = transformedExtents.x0;
+ y0 = transformedExtents.y0;
+ x1 = transformedExtents.x1;
+ y1 = transformedExtents.y1;
+ }
+
+ function transformNodes({nodes}) {
+ for (const node of nodes) {
+ const transformedNode = transform(node.x0, node.y0, node.x1, node.y1);
+ node.x0 = transformedNode.x0;
+ node.y0 = transformedNode.y0;
+ node.x1 = transformedNode.x1;
+ node.y1 = transformedNode.y1;
+ }
+ }
+
sankey.update = function(graph) {
computeLinkBreadths(graph);
return graph;
@@ -107,6 +142,10 @@ export default function Sankey() {
return arguments.length ? (links = typeof _ === "function" ? _ : constant(_), sankey) : links;
};
+ sankey.linkShape = function() {
+ return [left, right].includes(orientation) ? sankeyLinkHorizontal() : sankeyLinkVertical();
+ };
+
sankey.linkSort = function(_) {
return arguments.length ? (linkSort = _, sankey) : linkSort;
};
@@ -192,13 +231,15 @@ export default function Sankey() {
function computeNodeLayers({nodes}) {
const x = max(nodes, d => d.depth) + 1;
- const kx = (x1 - x0 - dx) / (x - 1);
+ const kx = (Math.abs(x1 - x0) - dx) / (x - 1);
+ const origin = orientation === bottom ? x1 : x0;
+ const dir = orientation === left || orientation === bottom ? -1 : 1;
const columns = new Array(x);
for (const node of nodes) {
const i = Math.max(0, Math.min(x - 1, Math.floor(align.call(null, node, x))));
node.layer = i;
- node.x0 = x0 + i * kx;
- node.x1 = node.x0 + dx;
+ node.x0 = origin + i * kx * dir;
+ node.x1 = node.x0 + dx * dir;
if (columns[i]) columns[i].push(node);
else columns[i] = [node];
}
@@ -209,22 +250,23 @@ export default function Sankey() {
}
function initializeNodeBreadths(columns) {
- const ky = min(columns, c => (y1 - y0 - (c.length - 1) * py) / sum(c, value));
+ const ky = min(columns, c => (Math.abs(y1 - y0) - (c.length - 1) * py) / sum(c, value));
for (const nodes of columns) {
- let y = y0;
+ let yStart = orientation === bottom ? y1 : y0;
for (const node of nodes) {
- node.y0 = y;
- node.y1 = y + node.value * ky;
- y = node.y1 + py;
+ node.y0 = yStart;
+ node.y1 = yStart + node.value * ky;
+ yStart = node.y1 + py;
for (const link of node.sourceLinks) {
link.width = link.value * ky;
}
}
- y = (y1 - y + py) / (nodes.length + 1);
+ let yEnd = orientation === bottom ? y0 : y1;
+ yStart = (yEnd - yStart + py) / (nodes.length + 1);
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
- node.y0 += y * (i + 1);
- node.y1 += y * (i + 1);
+ node.y0 += yStart * (i + 1);
+ node.y1 += yStart * (i + 1);
}
reorderLinks(nodes);
}
@@ -232,7 +274,8 @@ export default function Sankey() {
function computeNodeBreadths(graph) {
const columns = computeNodeLayers(graph);
- py = Math.min(dy, (y1 - y0) / (max(columns, c => c.length) - 1));
+ const breadth = [left, right].includes(orientation) ? x1 - x0 : y1 - y0;
+ py = Math.min(dy, Math.abs(breadth) / (max(columns, c => c.length) - 1));
initializeNodeBreadths(columns);
for (let i = 0; i < iterations; ++i) {
const alpha = Math.pow(0.99, i);
@@ -290,16 +333,19 @@ export default function Sankey() {
function resolveCollisions(nodes, alpha) {
const i = nodes.length >> 1;
- const subject = nodes[i];
- resolveCollisionsBottomToTop(nodes, subject.y0 - py, i - 1, alpha);
- resolveCollisionsTopToBottom(nodes, subject.y1 + py, i + 1, alpha);
- resolveCollisionsBottomToTop(nodes, y1, nodes.length - 1, alpha);
- resolveCollisionsTopToBottom(nodes, y0, 0, alpha);
+ const node = nodes[i];
+ const inverted = {y0: node.y1, y1: node.y0};
+ const subject = orientation === bottom ? inverted : node;
+ const dir = orientation === bottom ? -1 : 1;
+ resolveCollisionsBottomToTop(nodes, subject.y0 - py * dir, i - dir, alpha);
+ resolveCollisionsTopToBottom(nodes, subject.y1 + py * dir, i + dir, alpha);
+ resolveCollisionsBottomToTop(nodes, orientation === bottom ? y0 : y1, nodes.length - 1, alpha);
+ resolveCollisionsTopToBottom(nodes, orientation === bottom ? y1 : y0, 0, alpha);
}
// Push any overlapping nodes down.
function resolveCollisionsTopToBottom(nodes, y, i, alpha) {
- for (; i < nodes.length; ++i) {
+ for (; i >= 0 && i < nodes.length; ++i) {
const node = nodes[i];
const dy = (y - node.y0) * alpha;
if (dy > 1e-6) node.y0 += dy, node.y1 += dy;
@@ -309,7 +355,7 @@ export default function Sankey() {
// Push any overlapping nodes up.
function resolveCollisionsBottomToTop(nodes, y, i, alpha) {
- for (; i >= 0; --i) {
+ for (; i >= 0 && i < nodes.length; --i) {
const node = nodes[i];
const dy = (node.y1 - y) * alpha;
if (dy > 1e-6) node.y0 -= dy, node.y1 -= dy;
@@ -367,3 +413,19 @@ export default function Sankey() {
return sankey;
}
+
+export function sankeyTop() {
+ return Sankey(top);
+}
+
+export function sankeyRight() {
+ return Sankey(right);
+}
+
+export function sankeyBottom() {
+ return Sankey(bottom);
+}
+
+export function sankeyLeft() {
+ return Sankey(left);
+}
diff --git a/src/sankeyLink.js b/src/sankeyLink.js
new file mode 100644
index 0000000..11a25ef
--- /dev/null
+++ b/src/sankeyLink.js
@@ -0,0 +1,29 @@
+import {linkHorizontal, linkVertical} from "d3-shape";
+
+function horizontalSource(d) {
+ return [d.source.x1, d.y0];
+}
+
+function horizontalTarget(d) {
+ return [d.target.x0, d.y1];
+}
+
+export function sankeyLinkHorizontal() {
+ return linkHorizontal()
+ .source(horizontalSource)
+ .target(horizontalTarget);
+}
+
+function verticalSource(d) {
+ return [d.y0, d.source.y1];
+}
+
+function verticalTarget(d) {
+ return [d.y1, d.target.y0];
+}
+
+export function sankeyLinkVertical() {
+ return linkVertical()
+ .source(verticalSource)
+ .target(verticalTarget);
+}
diff --git a/src/sankeyLinkHorizontal.js b/src/sankeyLinkHorizontal.js
deleted file mode 100644
index 785bb7b..0000000
--- a/src/sankeyLinkHorizontal.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import {linkHorizontal} from "d3-shape";
-
-function horizontalSource(d) {
- return [d.source.x1, d.y0];
-}
-
-function horizontalTarget(d) {
- return [d.target.x0, d.y1];
-}
-
-export default function() {
- return linkHorizontal()
- .source(horizontalSource)
- .target(horizontalTarget);
-}
diff --git a/src/transform.js b/src/transform.js
new file mode 100644
index 0000000..210c8cb
--- /dev/null
+++ b/src/transform.js
@@ -0,0 +1,15 @@
+export function transformTop(x0, y0, x1, y1) {
+ return {x0: y0, y0: x0, x1: y1, y1: x1};
+}
+
+export function transformRight(x0, y0, x1, y1) {
+ return {x0: x0, y0: y0, x1: x1, y1: y1};
+}
+
+export function transformBottom(x0, y0, x1, y1) {
+ return {x0: y0, y0: x1, x1: y1, y1: x0};
+}
+
+export function transformLeft(x0, y0, x1, y1) {
+ return {x0: x1, y0: y0, x1: x0, y1: y1};
+}