Skip to content

ricklupton/d3-sankey-diagram

Repository files navigation

d3-sankey-diagram

Build Status

Sankey diagrams show flows between processes or relationships between sets. This library, is a reusable d3 diagram featuring:

  • automatic layout
  • multiple types of flow
  • loops / reversed flows
  • flow routing across layers

See the demo 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:

npm install d3-sankey-diagram

Or download the standalone bundle and include in your page as

<script src="d3-sankey-diagram.js" charset="utf-8"></script>

Usage

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.

If you use the Jupyter notebook, try 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 and our code of conduct. We have a Roadmap showing what we are working on, and you can browse the list of good first issues for ideas.

Documentation

d3-sankey-diagram is a JavaScript library for creating Sankey diagrams using d3. See the live examples to get an idea of what it does.

The main high-level components:

Lower-level components:

Sankey layout

# sankey()

Creates a new Sankey layout component.

# layout(arguments...)

Apply the layout to the given arguments. The arguments are arbitrary; they are simply propagated to the accessor functions nodes and links.

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

# layout.nodes([nodes])

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:

function nodes(graph) {
  return graph.nodes;
}

# layout.links([links])

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:

function links(graph) {
  return graph.links;
}

# layout.nodeId([nodeId])

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:

function nodeId(d) {
  return d.id;
}

# layout.sourceId([sourceId])
# layout.targetId([targetId])

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:

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.

# layout.nodeBackwards([nodeBackwards])

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:

function nodeBackwards(d) {
  return d.direction && d.direction.toLowerCase() === 'l';
}

# layout.linkValue([linkValue])

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:

function linkValue(d) {
  return d.value;
}

# layout.linkType([linkType])

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:

function linkType(d) {
  return d.type;
}

Adjusting layout

# layout.ordering([ordering])

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,

[
  [ ["layer 1 band 1"], ["layer 1 band 2"] ],
  [ ["layer 2 band 1"], ["layer 2 band 2"] ],
  ...
]

# layout.rankSets([rankSets])

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

{
    type: 'same|min',   // optional, default 'min'
    nodes: [node ids],  // required
}

# layout.sortPorts([sortPorts])

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:

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

# layout.extent([extent])

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.

# layout.size([size])

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 to [[0, 0], [width, height]].

# diagram.scale([scale])

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.

# diagram.whitespace([whitespace])

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

# diagram.nodePosition([nodePosition])

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

# sankeyDiagram()

Creates a new Sankey diagram component.

# diagram(selection)

Apply the diagram to a selection, which should be an svg element.

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.

Dimensions

# diagram.margin({ [top], [right], [bottom], [left] })

If called with an argument, set the margins of the diagram, otherwise return the current value.

Titles

# diagram.nodeTitle([nodeTitle])

If called with an argument, set the node title to the specified function, otherwise return the current function, which defaults to:

function nodeTitle(d) {
  return d.title !== undefined ? d.title : d.id;
}

# diagram.nodeValue([nodeValue])

If called with an argument, set the node value getter to the specified function, otherwise return the current function, which defaults to:

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.

# diagram.linkTitle([linkTitle])

If called with an argument, set the link title to the specified function, otherwise return the current function, which defaults to:

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:

# sankeyLinkTitle(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.

# diagram.linkLabel([linkLabel])

If called with an argument, set the link label to the specified function, otherwise return the current function, which defaults to:

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

# diagram.linkColor([linkColor])

If linkColor is specified, sets the link color accessor to the specified function, otherwise return the current accessor, which defaults to:

function linkColor(d) {
  return null;
}

# diagram.linkMinWidth([linkMinWidth])

If linkMinWidth is specified, sets the minimum link width accessor to the specified function, otherwise return the current accessor, which by default returns 1.

Node groups

# diagram.groups([groups])

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:

[
    { title: "Group title to be displayed above nodes",
      nodes: ["nodeid1", "nodeid", ...]
    }
]

Events

# diagram.on(type[, listener])

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:

npm test

Licence

MIT licence.

Contributors

  • Rick Lupton
  • @harisbal
  • @svwielga4

Changelog

Unreleased

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. (#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.
  • By default, node values are not shown by default (the default in v0.7.0 was to show them). Use diagram.nodeValue to set the format to show them.

v0.7.0