Guide

JavaScript Sankey Diagram

A Sankey shows quantities flowing between stages. Most of the work is getting your data into the shape the renderer wants, so this starts there.

Basic SankeyOpen in new tab

Built with ApexSankey

A Sankey diagram shows quantities flowing between stages, with each band's width proportional to the amount it carries. It answers "where did it all go": traffic through a funnel, energy through a grid, budget through departments, users between plans.

ApexSankey is the one in this family. It renders SVG, has no charting-library dependency, and takes a node list plus an edge list:

npm install apexsankey

<div id="svg-sankey"></div>

<script type="module">
  import ApexSankey from 'apexsankey'

  const sankey = new ApexSankey(document.getElementById('svg-sankey'), {
    width: 800,
    height: 400,
  })

  sankey.render({
    nodes: [
      { id: 'visit', title: 'Visited' },
      { id: 'signup', title: 'Signed up' },
      { id: 'trial', title: 'Started trial' },
      { id: 'paid', title: 'Converted' },
      { id: 'churn', title: 'Lapsed' },
    ],
    edges: [
      { source: 'visit', target: 'signup', type: 'web', value: 4200 },
      { source: 'signup', target: 'trial', type: 'web', value: 1800 },
      { source: 'trial', target: 'paid', type: 'web', value: 640 },
      { source: 'trial', target: 'churn', type: 'web', value: 1160 },
    ],
    options: sankey.options,
  })
</script>

Four fields per edge: source and target reference node ids, value sets the band width, and type is the category the band is coloured and grouped by.

What shape does your data need to be in?

This is where most first attempts stall, because the data you have is rarely the data the renderer wants. There are three shapes in practice, and only one of them is what ApexSankey consumes directly.

What you haveLooks likeWhat to do
Edge listone row per flow: from, to, amountAlready the right shape. Map the column names to source, target, value and derive nodes from the distinct ids.
Pivoted wideone row per subject, one column per stage: user, plan_2024, plan_2025Use buildAlluvialData. It converts stage columns into the node and edge lists for you.
Adjacency matrixa square grid, sources down the side, targets across the topConvert it yourself. There is no builder, but it is a nested loop (below).

Pivoted wide: use the builder

If each row is a subject and each column is a stage it passed through, that is an alluvial diagram, and buildAlluvialData does the reshaping:

import ApexSankey, { buildAlluvialData } from 'apexsankey'

const data = buildAlluvialData({
  dimensions: ['2024', '2025'],            // ordered stages, left to right
  records: [
    { values: { 2024: 'Free', 2025: 'Pro' } },
    { values: { 2024: 'Free', 2025: 'Free' } },
    { values: { 2024: 'Pro', 2025: 'Pro' } },
    { values: { 2024: 'Pro' } },           // no 2025 key: contributes no flow
  ],
})

sankey.render({ ...data, options: sankey.options })

Import buildAlluvialData as a named export, as above. It is also reachable at runtime as ApexSankey.buildAlluvialData, which is what a script tag has to use since it has no import to write, but that form is absent from the bundled type declarations, so TypeScript rejects it.

Three behaviours worth knowing before you rely on it, all of them measured against apexsankey 1.12.0:

  • A category that appears at two stages becomes two nodes, not one. "Free" in 2024 and "Free" in 2025 are separate nodes with generated ids (alluvial-0, alluvial-2) and the same title. That is correct for an alluvial diagram, where each column is its own axis, and it surprises people who expect one node per category.
  • A record missing a stage key contributes no flow across that gap. The fourth record above has no 2025 value, so it produces no edge. It is dropped silently, which is convenient for sparse data and quietly wrong if the missing keys were meant to be a "no change" category. Fill them in explicitly if they matter.
  • value defaults to 1 per record, so an omitted value counts subjects rather than summing an amount, and records sharing an adjacency are summed. The generated edge type is the source category, which is what colours each band by where it came from.

Adjacency matrix: convert it

No builder for this one, and it does not need one:

const labels = ['Raw', 'Refined', 'Export', 'Domestic']
const matrix = [
  [0, 120, 0, 0],      // Raw      -> Refined 120
  [0, 0, 70, 50],      // Refined  -> Export 70, Domestic 50
  [0, 0, 0, 0],
  [0, 0, 0, 0],
]

const nodes = labels.map((title, i) => ({ id: String(i), title }))
const edges = []
for (let row = 0; row < matrix.length; row++) {
  for (let col = 0; col < matrix[row].length; col++) {
    const value = matrix[row][col]
    if (value > 0) {
      edges.push({ source: String(row), target: String(col), type: labels[row], value })
    }
  }
}

sankey.render({ nodes, edges, options: sankey.options })

The if (value > 0) guard is doing real work. Skip it and every empty cell becomes a zero-value flow, which is the first item in the next section.

Why is my Sankey diagram blank?

Measured against apexsankey 1.12.0, in order of how often it is the answer.

SymptomCause
Nothing at alledges is empty. Nodes alone draw nothing, not even their rectangles.
Rectangles missing, no labelsEvery flow has value: 0. The nodes are in the DOM with height="0".
A thrown TypeErrornodes was omitted. It is required; the renderer does not infer it from the edges.

The good news is that most of what people expect to break does not:

  • Cycles render. A flow that loops back (Collect to Recycle to Raw, where Raw started the chain) draws without any configuration and without a flag to set. There is a shipped circular Sankey demo.
  • Self-loops render. An edge whose source and target are the same node draws its own band.
  • String values are coerced. value: '5' works, which matters because a CSV or JSON column arrives as text more often than not. You still want real numbers if you plan to sum them yourself.
  • An edge may reference a node you never declared. ApexSankey creates it rather than failing, using the id as the label. Handy for quick sketches, and worth knowing because it means a typo in a target adds a stray node instead of raising an error.

When is a Sankey the wrong chart?

A Sankey earns its complexity when the magnitude of the flow is the point. If it is not, something simpler reads better.

What you want to showReach for
Quantities splitting and merging across stagesA Sankey. This is the case it exists for.
Drop-off through a fixed linear sequence, no branchingA funnel chart. A Sankey with one path per stage is a funnel with extra steps.
Which categories are biggest, no flow between themA bar chart.
Parts of a whole at one momentA treemap, or a bar chart if the labels matter.
A hierarchy, where each node has exactly one parentA tree or org chart. Sankey nodes can have many inputs; if yours cannot, you have a tree.
Relationships with no direction or quantityA network or chord diagram. ApexSankey ships a chord demo for the circular case.
More than roughly 30 nodesAggregate first. Beyond that the bands get thinner than the gaps between them and nobody can trace a path.

The hierarchy row is the one that catches people most. If every node has a single parent, a Sankey draws it, but the layout spends its width proving something a tree shows at a glance.

What ApexSankey ships

ApexSankey is a commercial library, included from the Pro plan upward. It is not part of the Community tier, so unlike ApexCharts.js it is not covered by the under-$2M waiver. Nothing here is open source: source published on GitHub is not the same thing as an open licence. Everything below renders in full without a licence key, watermarked, so you can build the real diagram with your own data before deciding. The pricing page has the plan matrix.

Included
Horizontal and vertical orientationYes
Circular flows and chord layoutYes
Alluvial builder (buildAlluvialData)Yes
Node dragging, path highlightingYes
Drill-down and group collapsingYes
Before/after comparison viewYes
Time playback across framesYes
Particle flow animationYes
Theming, CSS custom properties, custom fontsYes
Localization and RTLYes
WCAG 2.1 AA accessibility supportYes
SVG exportYes

Using a Sankey with the rest of a dashboard

A flow diagram is usually one panel among several, and clicking a band to filter the others is the interaction people ask for first. The cross-product wiring is its own recipe:

Drill down from a chart into a grid

See the pieces running

Reference documentation

Frequently Asked Questions

What is a Sankey diagram used for?

Showing quantities that flow between stages, where each band's width is proportional to the amount it carries. Common cases are conversion funnels that branch, energy or material flow, budget allocation, and users moving between plans or states over time.

Why is my Sankey diagram blank?

Almost always because `edges` is empty: nodes on their own draw nothing in ApexSankey, not even their rectangles. The second most common cause is every flow having `value: 0`, which renders the nodes with a height of zero and no labels. Omitting `nodes` entirely is different again: it throws a TypeError rather than rendering empty.

Can a Sankey diagram show circular flows?

In ApexSankey, yes, with no configuration and no flag to set. A flow that loops back on itself renders, and so does a self-loop whose source and target are the same node. Verified on apexsankey 1.12.0.

How do I convert wide data into a Sankey diagram?

If each row is a subject and each column is a stage it passed through, use the `buildAlluvialData` named export: pass an ordered `dimensions` array and a `records` array, and it returns the `{ nodes, edges }` pair that `render()` takes. A category appearing at two stages becomes two nodes, and a record missing a stage key contributes no flow across that gap.

When should I use a funnel chart instead of a Sankey?

When the sequence is linear and nothing branches or merges. A Sankey with exactly one path per stage is a funnel chart with more layout machinery. Reach for the Sankey the moment flows split, rejoin, or come from more than one source.

Related

Start with ApexSankey

Zero dependencies, and free for organizations under $2M in annual revenue.

Get started