D3 Sankey Chart

When a stacked bar would lie about the flow, I draw a sankey. Here's one in D3 with the plugin the core build leaves out.

D3 Sankey Examples
← All posts
D3 Sankey Chart

A D3 sankey chart is how I show “this much went there” when a stacked bar would lie. Energy mixes, budget splits, onboarding funnels: the width of the ribbon is the number. People read it without a legend if you label the nodes.

Core D3 will not do this. You add d3-sankey. Same SVG story as the D3.js page. If the flow is geographic, start from the map example instead.

When a sankey helps

Use it when conservation matters: the sum of the inflows should match the outflows, give or take a “loss” node. Don't use it for a time series. That's a line, and I already have D3 graph examples for that.

A D3 sankey chart

Dummy energy: solar, wind, and gas into a grid, then homes, industry, and loss. Hover a ribbon for the value. The numbers are made up. The layout is the real API.

Preview

The code

Clone nodes and links before you pass them in. The layout mutates the objects. If you skip the clone, the second render is a mess.

Code

const data = {
  nodes: [{ name: "Solar" }, { name: "Grid" }, { name: "Homes" }],
  links: [
    { source: "Solar", target: "Grid", value: 24 },
    { source: "Grid", target: "Homes", value: 24 },
  ],
};

const layout = d3.sankey()
  .nodeId((d) => d.name)
  .nodeWidth(14)
  .nodePadding(20)
  .extent([[4, 12], [636, 328]]);

const graph = layout({
  nodes: data.nodes.map((d) => ({ ...d })),
  links: data.links.map((d) => ({ ...d })),
});

svg.selectAll("path")
  .data(graph.links)
  .join("path")
  .attr("d", d3.sankeyLinkHorizontal())
  .attr("fill", "none")
  .attr("stroke", "#e8b84a")
  .attr("stroke-opacity", 0.35)
  .attr("stroke-width", (d) => Math.max(1, d.width));

Gotchas

  • nodeId. If you identify nodes by name, tell the layout. The default is index, and a typo in source fails silently-ish.
  • Cycles. d3-sankey wants a directed acyclic graph. A loop and the layout gives up.
  • Tiny values. stroke-width can go under 1px. I clamp it.

Load the plugin after D3, or d3.sankey is undefined and the preview is a blank box. That's always the bug.

Questions I get

What is a D3 sankey chart?

A flow diagram. Nodes are steps or buckets. Links are amounts moving between them. Width of a link is the value. D3 does not include this in the core build; you add d3-sankey.

Why is d3.sankey undefined?

Core d3.js has no sankey. Load d3-sankey after d3. The UMD build hangs sankey and sankeyLinkHorizontal on the d3 global.

Can a sankey have cycles?

Not in d3-sankey. The layout assumes a DAG. If your data loops, drop the cycle or pick a different diagram.

Latest posts

All posts

Cytoscape JS Examples

Cytoscape JS examples are node-link graphs, not bar charts. Here are four I actually paste: force, hierarchy, circle, and tap to highlight.

Cytoscape Examples Networks
Read post

Bar Charts Explained

A bar is a number you can compare at a glance. Here's when to use one, the four layouts I actually ship, and the mistakes that make a bar chart lie.

Charts Examples JavaScript
Read post