HAHS.
Back to Catalog

Sankey Diagram

chart

Also known as: alluvial diagram, flow diagram, energy flow diagram

Show flowShow compositionShow relationship CategoricalNumerical Node-Link

Description

A Sankey diagram visualizes flows between a set of nodes (categories) using links whose width encodes the magnitude of each flow. Nodes are arranged in columns representing stages or categories, and curved or straight bands connect them to show how quantities split, merge, or transfer from one node to another. The total width entering a node always equals the total width leaving it (conservation of flow), making the diagram self-consistent and immediately readable.

The chart excels at revealing dominant pathways and surprising transfers within complex systems. Because the link width is proportional to quantity, readers can quickly compare the relative size of different flows without needing to read exact numbers. This makes Sankey diagrams particularly effective for energy budgets, material flows, budget allocations, and user journey analysis.

Sankey diagrams encode two dimensions of information simultaneously: the categorical relationship (which nodes connect) and the quantitative magnitude (link width). Color is typically applied to links or nodes to distinguish categories, and the vertical ordering of nodes can be optimized to minimize link crossings for clarity.

Sankey Diagram — interactive example

When to Use

  • Showing how a total quantity splits across multiple stages (e.g., energy production to consumption)
  • Visualizing user flows through a website or app funnel
  • Displaying budget allocations from revenue sources to expenditure categories
  • Comparing material inputs and outputs in manufacturing or supply chains
  • Revealing where the largest transfers or losses occur in a system

When NOT to Use

  • When you have only two categories with a single flow — use a bar chart instead
  • When temporal change is the primary story — use a line graph or area graph
  • When there are too many nodes (>20 per column), the diagram becomes unreadable — consider aggregating categories first
  • When exact values matter more than relative proportions — use a table or bar chart
  • When flows are cyclical (loops) — standard Sankey layouts assume directed acyclic graphs

Anatomy

  • Nodes: Rectangles (or rounded blocks) representing categories at each stage, sized proportionally to total inflow or outflow
  • Links (bands): Curved paths connecting source nodes to target nodes, with width proportional to flow magnitude
  • Columns (stages): Vertical alignment groups that define the sequence of the flow (e.g., source, intermediate, destination)
  • Labels: Text annotations on nodes identifying each category, sometimes with numeric values
  • Color encoding: Applied to links (by source or target category) or nodes to distinguish groups

Variations

  • Alluvial diagram: A variant where nodes at each stage represent the same categorical variable over time, emphasizing how group memberships change across periods
  • Circular Sankey: Links curve back around, useful for showing cyclical or bidirectional flows
  • Vertical Sankey: Flow runs top-to-bottom rather than left-to-right
  • Gradient-colored links: Links transition color from source to target hue, helping trace individual pathways

Code Reference

// D3 Sankey using d3-sankey plugin
import {sankey, sankeyLinkHorizontal} from "d3-sankey";
import * as d3 from "d3";

const {nodes, links} = sankey()
  .nodeWidth(20)
  .nodePadding(10)
  .extent([[0, 0], [width, height]])({
    nodes: data.nodes.map(d => ({...d})),
    links: data.links.map(d => ({...d}))
  });

const svg = d3.select("#chart").append("svg").attr("viewBox", [0, 0, width, height]);

svg.append("g")
  .selectAll("rect")
  .data(nodes).join("rect")
  .attr("x", d => d.x0).attr("y", d => d.y0)
  .attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0)
  .attr("fill", d => d3.schemeTableau10[d.index % 10]);

svg.append("g")
  .selectAll("path")
  .data(links).join("path")
  .attr("d", sankeyLinkHorizontal())
  .attr("stroke-width", d => Math.max(1, d.width))
  .attr("stroke", d => d3.schemeTableau10[d.source.index % 10])
  .attr("fill", "none").attr("opacity", 0.5);