Alluvial Diagrams

An alluvial diagram shows how a population redistributes across categories from one dimension to the next: plan tier in 2019 versus 2022, department last quarter versus this one, survey answer before versus after. The underlying picture is a Sankey, but the input is not nodes and edges. It is a table of subjects and their category at each step.

buildAlluvialData does that conversion.

The input shape

import { ApexSankey } from 'apexsankey'

const data = ApexSankey.buildAlluvialData({
  dimensions: ['2019', '2022'],
  records: [
    { values: { '2019': 'Free', '2022': 'Pro' } },
    { values: { '2019': 'Free', '2022': 'Free' } },
    { values: { '2019': 'Pro',  '2022': 'Team' } },
  ],
})

const sankey = new ApexSankey(document.getElementById('chart'), {
  axisTitles: ['2019', '2022'],
})
sankey.render({ ...data, options: sankey.options })
FieldTypeDescription
dimensionsstring[]Ordered dimension (axis) ids, left to right
recordsAlluvialRecord[]The subjects flowing across those dimensions
palettestring[]Category color palette, cycled per distinct category

Each record is { values, value? }:

FieldTypeDefaultDescription
valuesRecord<string, string>requiredCategory label at each dimension, keyed by dimension id
valuenumber1Weight this record contributes to each flow it takes part in

Weights

With no value, every record counts as 1, so band widths are headcounts. Supply value to weight by something else, such as revenue or hours:

records: [
  { values: { q1: 'Trial', q2: 'Paid' }, value: 12_400 },
  { values: { q1: 'Trial', q2: 'Churned' }, value: 3_100 },
]

Aggregate first if your source is already grouped. One record per distinct path with value set to that path's count is equivalent to, and much cheaper than, one record per subject.

Missing values

A record with no entry for a dimension drops that adjacency rather than inventing a category. This is the right behavior for a subject that did not exist yet, or had left:

records: [
  // joined in 2022, so no 2019 category
  { values: { '2022': 'Pro' } },
  // left before 2022, so no 2022 category
  { values: { '2019': 'Team' } },
]

Neither record contributes a 2019-to-2022 band. If you want joiners and leavers visible as flows, model them explicitly with a category of their own ('None', 'Joined', 'Churned') rather than omitting the key.

Axis titles

axisTitles labels each rank: index i labels rank i. It is an ordinary layout option, independent of the builder, so it also works on a hand-built Sankey:

const sankey = new ApexSankey(el, {
  axisTitles: ['2019', '2020', '2021', '2022'],
})

Titles are drawn above each column, or beside each row when orientation: 'vertical'. Leave the array short to skip trailing axes.

More than two dimensions

The builder takes any number of dimensions, and each adjacent pair becomes a set of flows:

const data = ApexSankey.buildAlluvialData({
  dimensions: ['signup', 'day7', 'day30', 'day90'],
  records: cohort.map((user) => ({
    values: {
      signup: user.planAtSignup,
      day7: user.planAtDay7,
      day30: user.planAtDay30,
      day90: user.planAtDay90,
    },
  })),
})

const sankey = new ApexSankey(el, {
  axisTitles: ['Signup', 'Day 7', 'Day 30', 'Day 90'],
  spacing: 28,
})
sankey.render({ ...data, options: sankey.options })

Categories keep a consistent color across every axis, so a single cohort is followable left to right by color alone.

Vertical alluvial

Long category labels usually read better stacked. orientation: 'vertical' puts ranks in rows and flows top to bottom, and axisTitles moves beside each row:

const sankey = new ApexSankey(el, {
  orientation: 'vertical',
  axisTitles: ['Before', 'After'],
})

See Orientation and Circular Links.

Animating between periods

Because the builder returns ordinary { nodes, edges }, an alluvial diagram animates like any other. Rebuild for a new window and call update():

function showWindow(from, to) {
  const data = ApexSankey.buildAlluvialData({
    dimensions: [from, to],
    records: cohortRecords,
  })
  sankey.update({ ...data, options: sankey.options })
}

See Data Updates and Morphing.