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 18 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
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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 @@ -158,6 +162,10 @@ This is particularly useful when representing graphs in JSON, as JSON does not a

If *align* is specified, sets the node [alignment method](#alignments) to the specified function and returns this Sankey generator. If *align* is not specified, returns the current node alignment method, which defaults to [d3.sankeyJustify](#sankeyJustify). The specified function is evaluated for each input *node* in order, being passed the current *node* and the total depth *n* of the graph (one plus the maximum *node*.depth), and must return an integer between 0 and *n* - 1 that indicates the desired horizontal position of the node in the generated Sankey diagram.

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

If *orientation* is specified, sets the node [orientation method](#orientations) to the specified function and returns this Sankey generator. If *orientation* is not specified, returns the current node orientation method, which defaults to [d3.orientRight](#orientRight).

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

If *sort* is specified, sets the node sort method and returns this Sankey generator. If *sort* is not specified, returns the current node sort method, which defaults to *undefined*, indicating that vertical order of nodes within each column 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 nodes, and must return a value less than 0 if the first node should be above the second, and a value greater than 0 if the second node should be above the first, or 0 if the order is not specified.
Expand Down Expand Up @@ -216,23 +224,13 @@ Like [d3.sankeyLeft](#sankeyLeft), except that nodes without any outgoing links

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

The [target accessor](https://github.com/d3/d3-shape/blob/master/README.md#link_target) is defined as:

```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 +242,17 @@ 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; });
```

### Orientations

See [*sankey*.nodeOrientation](#sankey_nodeOrientation).

Four Sankey diagram rendering orientations are currently available:

* <a name="orientUp" href="#orientUp">#</a> d3.<b>orientUp</b> [<>](https://github.com/d3/d3-sankey/blob/master/src/orientation.js "Source")
* <a name="orientDown" href="#orientDown">#</a> d3.<b>orientDown</b> [<>](https://github.com/d3/d3-sankey/blob/master/src/orientation.js "Source")
* <a name="orientLeft" href="#orientLeft">#</a> d3.<b>orientLeft</b> [<>](https://github.com/d3/d3-sankey/blob/master/src/orientation.js "Source")
* <a name="orientRight" href="#orientRight">#</a> d3.<b>orientRight</b> [<>](https://github.com/d3/d3-sankey/blob/master/src/orientation.js "Source")
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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 {orientUp, orientDown, orientLeft, orientRight} from "./orientation.js";
export {sankeyLinkHorizontal, sankeyLinkVertical} from "./sankeyLink.js";
15 changes: 15 additions & 0 deletions src/orientation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function orientUp(x0, y0, x1, y1) {
return {x0: y0, y0: x1, x1: y1, y1: x0};
}

export function orientDown(x0, y0, x1, y1) {
return {x0: y0, y0: x0, x1: y1, y1: x1};
}

export function orientLeft(x0, y0, x1, y1) {
return {x0: x1, y0: y0, x1: x0, y1: y1};
}

export function orientRight(x0, y0, x1, y1) {
return {x0: x0, y0: y0, x1: x1, y1: y1};
}
64 changes: 52 additions & 12 deletions src/sankey.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {max, min, sum} from "d3-array";
import {justify} from "./align.js";
import {orientUp, orientDown, orientLeft, orientRight} from "./orientation.js";
import {sankeyLinkHorizontal, sankeyLinkVertical} from "./sankeyLink.js";
import constant from "./constant.js";

function ascendingSourceBreadth(a, b) {
Expand Down Expand Up @@ -51,12 +53,23 @@ function computeLinkBreadths({nodes}) {
}
}

function orientNodes({nodes}, orientation) {
for (const node of nodes) {
const nodeOrientation = orientation(node.x0, node.y0, node.x1, node.y1);
node.x0 = nodeOrientation.x0;
node.y0 = nodeOrientation.y0;
node.x1 = nodeOrientation.x1;
node.y1 = nodeOrientation.y1;
}
}

export default function Sankey() {
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 orientation = orientRight;
let sort;
let linkSort;
let nodes = defaultNodes;
Expand All @@ -65,15 +78,25 @@ export default function Sankey() {

function sankey() {
const graph = {nodes: nodes.apply(null, arguments), links: links.apply(null, arguments)};
orientExtents();
computeNodeLinks(graph);
computeNodeValues(graph);
computeNodeDepths(graph);
computeNodeHeights(graph);
computeNodeBreadths(graph);
computeLinkBreadths(graph);
orientNodes(graph, orientation);
return graph;
}

function orientExtents() {
const extentOrientation = orientation(x0, y0, x1, y1);
x0 = extentOrientation.x0;
y0 = extentOrientation.y0;
x1 = extentOrientation.x1;
y1 = extentOrientation.y1;
}

sankey.update = function(graph) {
computeLinkBreadths(graph);
return graph;
Expand All @@ -87,6 +110,10 @@ export default function Sankey() {
return arguments.length ? (align = typeof _ === "function" ? _ : constant(_), sankey) : align;
};

sankey.nodeOrientation = function(_) {
return arguments.length ? (orientation = _, sankey) : orientation;
};

sankey.nodeSort = function(_) {
return arguments.length ? (sort = _, sankey) : sort;
};
Expand All @@ -107,6 +134,11 @@ export default function Sankey() {
return arguments.length ? (links = typeof _ === "function" ? _ : constant(_), sankey) : links;
};

sankey.linkShape = function() {
const horizontal = [orientLeft, orientRight];
return horizontal.includes(orientation) ? sankeyLinkHorizontal() : sankeyLinkVertical();
};

sankey.linkSort = function(_) {
return arguments.length ? (linkSort = _, sankey) : linkSort;
};
Expand Down Expand Up @@ -192,13 +224,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 === orientUp ? x1 : x0;
const dir = orientation === orientLeft || orientation === orientUp ? -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,9 +243,9 @@ 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 y = orientation === orientUp ? y1 : y0;
for (const node of nodes) {
node.y0 = y;
node.y1 = y + node.value * ky;
Expand All @@ -220,7 +254,8 @@ export default function Sankey() {
link.width = link.value * ky;
}
}
y = (y1 - y + py) / (nodes.length + 1);
let z = orientation === orientUp ? y0 : y1;
y = (z - y + py) / (nodes.length + 1);
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
node.y0 += y * (i + 1);
Expand All @@ -232,7 +267,9 @@ export default function Sankey() {

function computeNodeBreadths(graph) {
const columns = computeNodeLayers(graph);
py = Math.min(dy, (y1 - y0) / (max(columns, c => c.length) - 1));
const horizontal = [orientLeft, orientRight];
const breadth = horizontal.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,11 +327,14 @@ 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 === orientUp ? inverted : node;
const dir = orientation === orientUp ? -1 : 1;
resolveCollisionsBottomToTop(nodes, subject.y0 - py * dir, i - dir, alpha);
resolveCollisionsTopToBottom(nodes, subject.y1 + py * dir, i + dir, alpha);
resolveCollisionsBottomToTop(nodes, orientation === orientUp ? y0 : y1, nodes.length - 1, alpha);
resolveCollisionsTopToBottom(nodes, orientation === orientUp ? y1 : y0, 0, alpha);
}

// Push any overlapping nodes down.
Expand Down
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.