UNPKG

d3-sankey-diagram

Version:
517 lines (383 loc) 17.8 kB
# d3-sankey-diagram [![Build Status](https://travis-ci.org/ricklupton/d3-sankey-diagram.svg?branch=master)](https://travis-ci.org/ricklupton/d3-sankey-diagram) [Sankey diagrams](https://en.wikipedia.org/wiki/Sankey_diagram) show flows between processes or relationships between sets. This library, is a [reusable d3 diagram](https://bost.ocks.org/mike/chart/) featuring: - automatic layout - multiple types of flow - loops / reversed flows - flow routing across layers See the **[demo](https://ricklupton.github.io/d3-sankey-diagram)** for examples of these. d3-sankey-diagram versions v0.6 and up are based on d3 v4. ## Installation Install using npm if you are using browserify or the like: ```js npm install d3-sankey-diagram ``` Or download the [standalone bundle](https://github.com/ricklupton/d3-sankey-diagram/releases/latest) and include in your page as ```html <script src="d3-sankey-diagram.js" charset="utf-8"></script> ``` ## Usage ```js var layout = d3.sankey() .extent([[100, 10], [840, 580]]); var diagram = d3.sankeyDiagram() .linkColor(function(d) { return d.color; }); d3.json('uk_energy.json', function(energy) { layout.ordering(energy.order); d3.select('#sankey') .datum(layout(energy)) .call(diagram); }); ``` Try more [live examples](https://ricklupton.github.io/d3-sankey-diagram). If you use the Jupyter notebook, try [ipysankeywidget](https://github.com/ricklupton/ipysankeywidget). `d3-sankey-diagram` works both in node (using jsdom) and in the browser. ## Contributing 🎁 Thanks for your interest in contributing! To get started see [CONTRIBUTING.md](CONTRIBUTING.md) and our [code of conduct](CODE_OF_CONDUCT.md). We have a [Roadmap](https://github.com/ricklupton/d3-sankey-diagram/projects/1) showing what we are working on, and you can browse the list of [good first issues](https://github.com/ricklupton/d3-sankey-diagram/labels/good%20first%20bug) for ideas. ## Documentation d3-sankey-diagram is a JavaScript library for creating [Sankey diagrams](https://en.wikipedia.org/wiki/Sankey_diagram) using [d3](https://d3js.org/). See the [live examples](https://ricklupton.github.io/d3-sankey-diagram) to get an idea of what it does. The main high-level components: - [d3.sankey](#sankey): Sankey layout - [d3.sankeyDiagram](#sankeyDiagram): SVG Sankey diagram component Lower-level components: - [d3.sankeyNode](#sankeyNode): SVG Sankey node component - [d3.sankeyLink](#sankeyLink): SVG path generator for Sankey diagram links - [d3.sankeyLinkTitle](#sankeyLinkTitle): Helper function to generate link titles ### Sankey layout <a name="sankey" href="#sankey">#</a> <b>sankey</b>() Creates a new Sankey layout component. <a name="layout" href="#layout">#</a> <b>layout</b>(<i>arguments...</i>) Apply the layout to the given <i>arguments</i>. The <i>arguments</i> are arbitrary; they are simply propagated to the accessor functions [nodes](#layout-nodes) and [links](#layout-links). ```js var layout = d3.sankey(); var graph = layout({ nodes: [ {"id": "a", "title": "Source"}, {"id": "b", "title": "Stage 1"} ], links: [ {"source": "a", "target": "b", "type": "x", "value": 2} ] }); // Resulting graph object can be used as datum for d3.sankeyDiagram() below ``` #### Data accessors <a name="layout-nodes" href="#layout-nodes">#</a> layout.<b>nodes</b>([<i>nodes</i>]) If *nodes* is specified, sets the nodes accessor to the specified function and returns this layout. If *nodes* is not specified, return the current accessor, which defaults to: ```js function nodes(graph) { return graph.nodes; } ``` <a name="layout-links" href="#layout-links">#</a> layout.<b>links</b>([<i>links</i>]) If *links* is specified, sets the links accessor to the specified function and returns this layout. If *links* is not specified, return the current accessor, which defaults to: ```js function links(graph) { return graph.links; } ``` <a name="layout-nodeId" href="#layout-nodeId">#</a> layout.<b>nodeId</b>([<i>nodeId</i>]) If *nodeId* is specified, sets the node id accessor to the specified function and returns this layout. If *nodeId* is not specified, return the current accessor, which defaults to: ```js function nodeId(d) { return d.id; } ``` <a name="layout-sourceId" href="#layout-sourceId">#</a> layout.<b>sourceId</b>([<i>sourceId</i>])<br/> <a name="layout-targetId" href="#layout-targetId">#</a> layout.<b>targetId</b>([<i>targetId</i>]) If *sourceId*/*targetId* is specified, sets the link source/target id accessor to the specified function and returns this layout. If *sourceId*/*targetId* is not specified, return the current accessor, which defaults to: ```js function sourceId (d) { return { id: typeof d.source === 'object' ? d.source.id : d.source, port: typeof d.sourcePort === 'object' ? d.sourcePort.id : d.sourcePort } } // similarly for targetId ``` See below for more discussion of ports. If this accessor returns a string, it is interpreted as the node id and the port is set to `undefined`. <a name="layout-nodeBackwards" href="#layout-nodeBackwards">#</a> layout.<b>nodeBackwards</b>([<i>nodeBackwards</i>]) If *nodeBackwards* is specified, sets the node direction accessor to the specified function and returns this layout. If *nodeBackwards* is not specified, return the current accessor, which defaults to: ```js function nodeBackwards(d) { return d.direction && d.direction.toLowerCase() === 'l'; } ``` <a name="layout-linkValue" href="#layout-linkValue">#</a> layout.<b>linkValue</b>([<i>linkValue</i>]) If *linkValue* is specified, sets the link value accessor to the specified function and returns this layout. If *linkValue* is not specified, return the current accessor, which defaults to: ```js function linkValue(d) { return d.value; } ``` <a name="layout-linkType" href="#layout-linkType">#</a> layout.<b>linkType</b>([<i>linkType</i>]) If *linkType* is specified, sets the link type accessor to the specified function and returns this layout. If *linkType* is not specified, return the current accessor, which defaults to: ```js function linkType(d) { return d.type; } ``` #### Adjusting layout <a name="layout-ordering" href="#layout-ordering">#</a> layout.<b>ordering</b>([<i>ordering</i>]) If *ordering* is specified, sets the node ordering to the specified value and returns this layout. If *ordering* is not specified, return the current value, which defaults to `null`. When *ordering* is null, the node ordering will be calculated automatically. When *ordering* is specified, it is used directly and no rank assignment or ordering algorithm takes place. The *ordering* structure has three nested lists: *ordering* is a list of layers, each of which is a list of bands, each of which is a list of node ids. For example, ```js [ [ ["layer 1 band 1"], ["layer 1 band 2"] ], [ ["layer 2 band 1"], ["layer 2 band 2"] ], ... ] ``` <a name="layout-rankSets" href="#layout-rankSets">#</a> layout.<b>rankSets</b>([<i>rankSets</i>]) If *rankSets* is specified, sets the rank sets to the specified value and returns this layout. If *rankSets* is not specified, return the current value, which defaults to `[]`. Rank sets are optional constraints to keep nodes in the same layer. Each entry has the form ```js { type: 'same|min', // optional, default 'min' nodes: [node ids], // required } ``` <a name="layout-sortPorts" href="#layout-sortPorts">#</a> layout.<b>sortPorts</b>([<i>sortPorts</i>]) If *sortPorts* is specified, sets the port sorting function to the specified function and returns this layout. If *sortPorts* is not specified, return the current value, which defaults to: ```js function sortPorts(a, b) { return a.id.localeCompare(b.id) } ``` **Note**: in a future version this may be changed to sort ports to avoid crossings by default. #### Dimensions <a name="layout-extent" href="#layout-extent">#</a> layout.<b>extent</b>([<i>extent</i>]) If *extent* is specified, sets this layout’s extent to the specified array of points [[*x0*, *y0*], [*x1*, *y1*]], where [*x0*, *y0*] is the top-left corner and [*x1*, *y1*] is the bottom-right corner, and returns this tile layout. If *extent* is not specified, returns the current layout extent. <a name="layout-size" href="#layout-size">#</a> layout.<b>size</b>([<i>size</i>]) If *size* is specified, sets this layout’s size to the specified two-element array of numbers [*width*, *height*] and returns this layout. If *size* is not specified, returns the current layout size. This is a convenience method equivalent to setting the [extent](#layout-extent) to [[0, 0], [*width*, *height*]]. <a name="layout-scale" href="#layout-scale">#</a> diagram.<b>scale</b>([<i>scale</i>]) If *scale* is specified as a number, sets the layout's scale (from data units to pixels). If *scale* is `null`, the scale will be reset and automatically calculated the next time the diagram is called, to achieve the desired whitespace fraction (below). If *scale* is not specified, return the current scale. <a name="layout-whitespace" href="#layout-whitespace">#</a> diagram.<b>whitespace</b>([<i>whitespace</i>]) If *whitespace* is specified as a number, sets the layout's whitespace fraction, used when automatically calculating a scale (above). If *whitespace* is not specified, return the current whitespace. #### Manual positioning <a name="layout-nodePosition" href="#layout-nodePosition">#</a> diagram.<b>nodePosition</b>([<i>nodePosition</i>]) If *nodePosition* is specified, use the specified function to directly set node positions, bypassing the layout algorithm (link positions and shapes are still calculated). If *nodePosition* is not specified, return the current function, which defaults to `null`. ### SVG Sankey diagram component <a name="sankeyDiagram" href="#sankeyDiagram">#</a> <b>sankeyDiagram</b>() Creates a new Sankey diagram component. <a name="diagram" href="#diagram">#</a> <b>diagram</b>(<i>selection</i>) Apply the diagram to a selection, which should be an `svg` element. ```js var diagram = d3.sankeyDiagram(); d3.select('#sankey') .datum(sankey) .call(diagram); ``` The Sankey data is taken from the selection's bound data, which should be a graph object, as generated by [d3.sankey](#sankey). #### Dimensions <a name="margin" href="#margin">#</a> diagram.<b>margin</b>({ [<i>top</i>], [<i>right</i>], [<i>bottom</i>], [<i>left</i>] }) If called with an argument, set the margins of the diagram, otherwise return the current value. #### Titles <a name="nodeTitle" href="#nodeTitle">#</a> diagram.<b>nodeTitle</b>([<i>nodeTitle</i>]) If called with an argument, set the node title to the specified function, otherwise return the current function, which defaults to: ```js function nodeTitle(d) { return d.title !== undefined ? d.title : d.id; } ``` <a name="nodeValue" href="#nodeValue">#</a> diagram.<b>nodeValue</b>([<i>nodeValue</i>]) If called with an argument, set the node value getter to the specified function, otherwise return the current function, which defaults to: ```js function nodeValue(d) { return null; } ``` The node value is shown with an SVG text element within the node body, only when the `nodeWidth` is greater than zero. <a name="linkTitle" href="#linkTitle">#</a> diagram.<b>linkTitle</b>([<i>linkTitle</i>]) If called with an argument, set the link title to the specified function, otherwise return the current function, which defaults to: ```js const fmt = d3.format('.3s') function linkTitle(d) { const parts = [] const sourceTitle = nodeTitle(d.source) const targetTitle = nodeTitle(d.target) const matTitle = d.type parts.push(`${sourceTitle} → ${targetTitle}`) if (matTitle) parts.push(matTitle) parts.push(fmt(d.value)) return parts.join('\n') } ``` The link title is displayed in an SVG `title` element, visible on hover. To make it easier to customise this function to your data, you can use `d3.sankeyLinkTitle` to generate new functions: <a name="sankeyLinkTitle" href="#sankeyLinkTitle">#</a> <b>sankeyLinkTitle</b>(nodeTitle, typeTitle, fmt) Generates a function similar to the one above, with custom accessors for the node title `nodeTitle`, link-type title `typeTitle` and number format `fmt`. <a name="linkLabel" href="#linkLabel">#</a> diagram.<b>linkLabel</b>([<i>linkLabel</i>]) If called with an argument, set the link label to the specified function, otherwise return the current function, which defaults to: ```js function linkLabel(d) { return null } ``` The link label is displayed in an SVG `text` element, so unlike the /title/, it is visible all the time. #### Link appearance <a name="linkColor" href="#linkColor">#</a> diagram.<b>linkColor</b>([<i>linkColor</i>]) If *linkColor* is specified, sets the link color accessor to the specified function, otherwise return the current accessor, which defaults to: ```js function linkColor(d) { return null; } ``` <a name="linkMinWidth" href="#linkMinWidth">#</a> diagram.<b>linkMinWidth</b>([<i>linkMinWidth</i>]) If *linkMinWidth* is specified, sets the minimum link width accessor to the specified function, otherwise return the current accessor, which by default returns `1`. <a name="linkImportance" href="#linkImportance">#</a> diagram.<b>linkImportance</b>([<i>linkImportance</i>]) If *linkImportance* is specified, sets the link importance accessor to the specified function, otherwise return the current accessor, which defaults to: ```js function linkImportance(d) { // Return negative values for special cases to put them at the bottom if (!d.source || (d.target && d.target.direction === 'd')) return -2 if (!d.target || (d.source && d.source.direction === 'd')) return -1 // Return dy (visual width) for normal links return d.dy } ``` The link importance determines the z-order (stacking order) of links in the diagram. Links are grouped by source-target pair, and groups are sorted by their aggregated importance (see [linkImportanceAgg](#linkImportanceAgg) below). This ensures that multiple links between the same nodes are always rendered together, avoiding a "woven" pattern where some links from one group go over and others go under links from another group. <a name="linkImportanceAgg" href="#linkImportanceAgg">#</a> diagram.<b>linkImportanceAgg</b>([<i>linkImportanceAgg</i>]) If *linkImportanceAgg* is specified, sets the link importance aggregation function to the specified function, otherwise return the current function, which defaults to: ```js function linkImportanceAgg(values) { // Sum of importance values for all links in the group return values.reduce((a, b) => a + b, 0) } ``` This function aggregates the individual link importance values (from [linkImportance](#linkImportance)) for all links between the same source and target nodes. The aggregated value is used to determine the z-order of link groups. #### Node groups <a name="groups" href="#groups">#</a> diagram.<b>groups</b>([<i>groups</i>]) If *groups* is specified, sets the list of node groups to the specified value. If *groups* is not specified, return the current list. Node groups display a box around the specified nodes, and should be given in the following format: ```js [ { title: "Group title to be displayed above nodes", nodes: ["nodeid1", "nodeid", ...] } ] ``` #### Events <a name="on" href="#on">#</a> diagram.<b>on</b>(<i>type</i>[, <i>listener</i>]) Adds or removes an event *listener* for the specified *type*. The *type* string is one of `selectNode`, `selectLink` or `selectGroup`. The *listener* is invoked with the context as the element and one argument, the corresponding data. If *listener* is not specified, returns the currently-assigned listener for the specified *type*, if any. ## Tests Run the tests: ```js npm test ``` ## Licence MIT licence. ## Contributors - Rick Lupton - @harisbal - @svwielga4 ## Changelog ### Unreleased ### v0.9.2 - Add `linkImportance` and `linkImportanceAgg`: links between the same source and target node are now kept grouped together in the SVG so they have similar Z-orders, avoiding the effect of links "weaving" through each other. ### v0.9.1 - Fix package.json metadata from ESM module import. ### v0.9.0 - Update many dependencies. ### v0.8.0 - Modified code to assign `type` and `value` attributes to `link` objects (thanks @harisbal) - EXPERIMENTAL: allow short "stub" flows in or out of nodes using `fromElsewhere` and `toElsewhere` attributes. These can be useful for simplifying diagrams by avoiding uninteresting flows while still showing how nodes balance. ### v0.7.3 - Fix packaging to avoid overwriting the d3 global object in UMD module! - Add a basic link label feature to show the values of links with SVG text elements. See [`diagram.linkLabel`](#linkLabel). (#2) ### v0.7.2 - Update packaging to produce different module builds: - `d3-sankey-diagram.esm.js`: An ES module - `d3-sankey-diagram.cjs.js`: A CommonJS module for use with NodeJS - `d3-sankey-diagram.umd.js`: A UMD module for use in `<script>` tags - `d3-sankey-diagram.min.js`: A minified version of the UMD module ### v0.7.1 - Unused code tidied up (thanks svwielga4) - Improved node value labels: with wide nodes, the labels are shown within the nodes. Otherwise they are now shown after the node titles in parentheses. **[See example](https://bl.ocks.org/ricklupton/8a9a9501883a5645202cb439def65d31)**. - By default, node values are not shown by default (the default in v0.7.0 was to show them). Use [`diagram.nodeValue`](#nodeValue) to set the format to show them. ### v0.7.0 - Add option to show node values, customizable with [`diagram.nodeValue`](#nodeValue).