Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for additional Sankey diagram orientations #71

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8de4751
Remove horizontal-only assumption from filenames, exports
jayaddison Jan 8, 2020
cc7805a
Add no-op library-internal 'horizontal' orientation
jayaddison Jan 8, 2020
ee1f37b
Add and export 'vertical' orientation
jayaddison Jan 8, 2020
a26355e
Add linkShape utility function
jayaddison Jan 8, 2020
3f63c25
Orient extents before calculating node positions
jayaddison Jan 8, 2020
584ca50
Update README.
jayaddison Jan 9, 2020
d94e35c
Remove ES6 destructuring assignments
jayaddison Jan 15, 2020
9cfd337
Rename {horizontal, vertical} -> {sankeyHorizontal, sankeyVertical}
jayaddison Jan 15, 2020
bcdabc2
Add nodeOrientation function docs, rendered examples
jayaddison Jan 15, 2020
66afca1
Re-arrange README to improve diff clarity
jayaddison Jan 15, 2020
d49ea06
Enable node re-orientation for individual axis values instead of co-o…
jayaddison Sep 10, 2020
58b77c6
Refactor: include concept of direction for orientation
jayaddison Sep 10, 2020
bd170ab
Add left & up orientations
jayaddison Sep 10, 2020
4aec193
Ensure link width is positive
jayaddison Sep 10, 2020
b8a5222
Implement orientation-specific rendering without collision detection
jayaddison Sep 10, 2020
0cf48c2
Enable orientation-specific collision detection
jayaddison Sep 10, 2020
331af05
Update README orientation documentation
jayaddison Sep 10, 2020
0267ad8
Revert "Ensure link width is positive"
jayaddison Sep 10, 2020
d813a6b
Nit: spacing consistency fixup
jayaddison Sep 11, 2020
863b185
Nit: rename variable z -> ymax to remove dimenson-name ambiguity
jayaddison Sep 11, 2020
f603a6c
Revert "Nit: spacing consistency fixup"
jayaddison Sep 11, 2020
5fbbf1b
Refactor:
jayaddison Sep 14, 2020
e70b772
Bounds checking: check both lower and upper bounds during node collis…
jayaddison Sep 14, 2020
9d3dc99
Merge branch 'collision-bounds-checking' into vertical-orientation
jayaddison Sep 14, 2020
332e27b
Update README
jayaddison Sep 14, 2020
ac05124
Nit: brevity during alignment checks
jayaddison Sep 14, 2020
898a673
Nit: attempt to further clarify distinct variable names
jayaddison Sep 14, 2020
ebfda16
Rename variable: orient -> orientation
jayaddison Nov 11, 2020
d674a34
Rename variables: ystart, yend -> yStart, yEnd
jayaddison Nov 11, 2020
1968a46
Use 'const' declaration for static module-level variables
jayaddison Nov 11, 2020
41d6b59
Fixup: documentation hyperlinks
jayaddison Feb 21, 2021
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
52 changes: 29 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,21 @@ var sankey = d3.sankey();

## API Reference

<a href="#sankey" name="sankey">#</a> d3.<b>sankey</b>() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")
<a href="#sankeyTop" name="sankeyTop">#</a> d3.<b>sankeyTop</b>() [<>](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.

<a href="#sankeyRight" name="sankeyRight">#</a> d3.<b>sankeyRight</b>() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")

Constructs a new right-oriented Sankey generator with the default settings.

<a href="#sankeyBottom" name="sankeyBottom">#</a> d3.<b>sankeyBottom</b>() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")

Constructs a new bottom-oriented Sankey generator with the default settings.

<a href="#sankeyLeft" name="sankeyLeft">#</a> d3.<b>sankeyLeft</b>() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")

Constructs a new left-oriented Sankey generator with the default settings.

<a href="#_sankey" name="_sankey">#</a> <i>sankey</i>(<i>arguments</i>…) [<>](https://github.com/d3/d3-sankey/blob/master/src/sankey.js "Source")

Expand Down Expand Up @@ -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

<a name="sankey_linkShape" href="#sankey_linkShape">#</a> <i>sankey</i>.<b>linkShape</b>() [<>](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.

<a name="sankey_linkSort" href="#sankey_linkSort">#</a> <i>sankey</i>.<b>linkSort</b>([<i>sort</i>]) [<>](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.
Expand Down Expand Up @@ -190,49 +206,39 @@ If *iterations* is specified, sets the number of relaxation iterations when [gen

See [*sankey*.nodeAlign](#sankey_nodeAlign).

<a name="sankeyLeft" href="#sankeyLeft">#</a> d3.<b>sankeyLeft</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
<a name="sankeyAlignLeft" href="#sankeyAlignLeft">#</a> d3.<b>sankeyAlignLeft</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankeyAlign/blob/master/src/align.js "Source")

[<img alt="left" src="https://raw.githubusercontent.com/d3/d3-sankey/master/img/align-left.png" width="480">](https://observablehq.com/@d3/sankey-diagram?align=left)

Returns *node*.depth.

<a name="sankeyRight" href="#sankeyRight">#</a> d3.<b>sankeyRight</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
<a name="sankeyAlignRight" href="#sankeyAlignRight">#</a> d3.<b>sankeyAlignRight</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankeyAlign/blob/master/src/align.js "Source")

[<img alt="right" src="https://raw.githubusercontent.com/d3/d3-sankey/master/img/align-right.png" width="480">](https://observablehq.com/@d3/sankey-diagram?align=right)

Returns *n* - 1 - *node*.height.

<a name="sankeyCenter" href="#sankeyCenter">#</a> d3.<b>sankeyCenter</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
<a name="sankeyAlignCenter" href="#sankeyAlignCenter">#</a> d3.<b>sankeyAlignCenter</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankeyAlign/blob/master/src/align.js "Source")

[<img alt="center" src="https://raw.githubusercontent.com/d3/d3-sankey/master/img/align-center.png" width="480">](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.

<a name="sankeyJustify" href="#sankeyJustify">#</a> d3.<b>sankeyJustify</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankey/blob/master/src/align.js "Source")
<a name="sankeyAlignJustify" href="#sankeyAlignJustify">#</a> d3.<b>sankeyAlignJustify</b>(<i>node</i>, <i>n</i>) [<>](https://github.com/d3/d3-sankeyAlign/blob/master/src/align.js "Source")

[<img alt="justify" src="https://raw.githubusercontent.com/d3/d3-sankey/master/img/energy.png" width="480">](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

<a name="sankeyLinkHorizontal" href="#sankeyLinkHorizontal">#</a> d3.<b>sankeyLinkHorizontal</b>() [<>](https://github.com/d3/d3-sankey/blob/master/src/sankeyLinkHorizontal.js "Source")
<a name="sankeyLinkHorizontal" href="#sankeyLinkHorizontal">#</a> d3.<b>sankeyLinkHorizontal</b>() [<>](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) {
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
return [d.source.x1, d.y0];
}
```

The [target accessor](https://github.com/d3/d3-shape/blob/master/README.md#link_target) is defined as:
<a name="sankeyLinkVertical" href="#sankeyLinkVertical">#</a> d3.<b>sankeyLinkVertical</b>() [<>](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:

Expand All @@ -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; });
```
6 changes: 3 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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";
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
export {sankeyLinkHorizontal, sankeyLinkVertical} from "./sankeyLink.js";
102 changes: 82 additions & 20 deletions src/sankey.js
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

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;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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];
}
Expand All @@ -209,30 +250,32 @@ 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);
}
}

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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
29 changes: 29 additions & 0 deletions src/sankeyLink.js
Original file line number Diff line number Diff line change
@@ -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)
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
.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);
}
15 changes: 0 additions & 15 deletions src/sankeyLinkHorizontal.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/transform.js
Original file line number Diff line number Diff line change
@@ -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};
}