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 be misleading. Energy mixes, budget allocations, onboarding funnels — the width of each ribbon is the quantity, and if you label the nodes clearly, people understand it without needing a legend.

One catch: sankey is not part of the core D3 build. You need to add d3-sankey separately. Same SVG approach as the D3.js page, just a different layout function. If your flow is geographic — money moving between countries, that kind of thing — the map example is usually clearer.

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're identifying nodes by name (which you almost certainly are), you have to tell the layout explicitly: .nodeId(d => d.name). The default accessor is the array index, so a typo in a source or target field fails quietly rather than throwing an error.
  • Cycles. d3-sankey expects a directed acyclic graph. If your data has a loop — A flows to B, B flows to A — the layout will produce garbage. Either break the cycle or model it differently.
  • Tiny values. Links with very small values can produce a stroke-width below 1px, which renders as invisible. I clamp with Math.max(1, d.width).

And the most common mistake of all: loading d3-sankey before D3, or not loading it at all. If d3.sankey is undefined, you'll get a blank box with no useful error. Load the plugin after D3 and that fixes it.

Questions I get

What is a D3 sankey chart?

A flow diagram where the width of each ribbon represents a quantity. Nodes are steps, stages, or buckets. Links show amounts moving between them. It's particularly useful for things like energy flows, budget allocation, or conversion funnels — anywhere conservation matters and you want to show where things go. One thing to know: d3-sankey is not part of the core D3 build, so you have to load it separately.

Why is d3.sankey undefined?

Because d3-sankey isn't included in the main D3 bundle. You need to load it separately, after D3. The UMD build attaches sankey and sankeyLinkHorizontal directly onto the d3 global, so once it's loaded you can call d3.sankey() like any other D3 method.

Can a sankey have cycles?

Not with d3-sankey. The layout algorithm assumes a directed acyclic graph — every link flows left to right with no loops. If your data has a cycle (A flows to B, B flows back to A), the layout will break in confusing ways. You'll need to either drop the cycle, model it differently, or use a different diagram type altogether.

Latest posts

All posts