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 })
| Field | Type | Description |
|---|---|---|
dimensions | string[] | Ordered dimension (axis) ids, left to right |
records | AlluvialRecord[] | The subjects flowing across those dimensions |
palette | string[] | Category color palette, cycled per distinct category |
Each record is { values, value? }:
| Field | Type | Default | Description |
|---|---|---|---|
values | Record<string, string> | required | Category label at each dimension, keyed by dimension id |
value | number | 1 | Weight 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.