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") [left](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") [right](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") [center](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") [justify](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}; +}