Sankey Chart
A generic flow diagram — nodes connected by value-proportional links — for visualising money (or any proportional) flows between stages.
"use client";import { SankeyChart, type SankeyLinkDatum, type SankeyNodeDatum } from "@reva/ui";const nodes: SankeyNodeDatum[] = [{ id: "salary", label: "Salary", color: "var(--chart-1)" },{ id: "dividend", label: "Dividend", color: "var(--chart-2)" },{ id: "household", label: "Household", color: "var(--chart-accent-fill)" },{ id: "spend", label: "Living costs", color: "var(--chart-3)" },{ id: "tax", label: "Tax", color: "var(--chart-negative-fill)" },{ id: "savings", label: "Savings", color: "var(--chart-positive-fill)" },];const links: SankeyLinkDatum[] = [{ source: "salary", target: "household", value: 85_000 },{ source: "dividend", target: "household", value: 40_000 },{ source: "household", target: "spend", value: 62_000 },{ source: "household", target: "tax", value: 28_000 },{ source: "household", target: "savings", value: 35_000 },];export function HouseholdCashflow() {return ( <SankeyChart nodes={nodes} links={links} label="Household cashflow: income sources into spend, tax and savings" />);}Usage
import { SankeyChart, type SankeyLinkDatum, type SankeyNodeDatum } from "@reva/ui";
<SankeyChart nodes={nodes} links={links} />SankeyChart renders nodes and value-proportional links built on recharts' native Sankey chart. Nodes and links are addressed by a stable id rather than recharts' index-based shape, so callers never hand-manage array positions — a link is just { source: "salary", target: "household", value: 85_000 }.
Node placement is inferred from the graph itself: a node with no incoming links is a source (label to its right), one with no outgoing links is a sink (label to its left), and anything in between is a waypoint (label above). This makes the component shape-agnostic — a single source fanning out to several sinks, a funnel of many sources into one node, or a full three-tier flow all lay out sensibly with no extra configuration.
Stable references
Pass nodes / links with stable references — module scope, state, or useMemo.
Customisation
Omit a node's color to fall back to the categorical --chart-1…--chart-7 ramp by index; pass chart role tokens (--chart-positive-fill, --chart-negative-fill, --chart-accent-fill, …) when a node carries meaning — a tax sink, a savings sink. A link's colour defaults to its source node's colour and renders as a gradient into its target's, so a link visually hands off from one node's colour to the next.
valueFormatter shapes the tooltip values, defaulting to compact en-GB numbers. Pass a compact-GBP formatter for money flows:
const gbp = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
notation: "compact",
maximumFractionDigits: 1,
});
<SankeyChart nodes={nodes} links={links} valueFormatter={(value) => gbp.format(value)} />Examples
A single source, several sinks
The layout isn't tied to a three-tier "sources → hub → sinks" shape — a lone source fanning straight out to multiple sinks lays out just as well.
const nodes: SankeyNodeDatum[] = [ { id: "company", label: "Acme Ltd", color: "var(--chart-accent-fill)" }, { id: "salary", label: "Salary", color: "var(--chart-1)" }, { id: "dividend", label: "Dividend", color: "var(--chart-2)" }, { id: "pension", label: "Pension", color: "var(--chart-3)" },];const links: SankeyLinkDatum[] = [ { source: "company", target: "salary", value: 40_000 }, { source: "company", target: "dividend", value: 25_000 }, { source: "company", target: "pension", value: 10_000 },];<SankeyChart nodes={nodes} links={links} height={200} label="Company extraction split by salary, dividend and pension"/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | SankeyNodeDatum[] | — | { id, label, color? } — required |
links | SankeyLinkDatum[] | — | { source, target, value, color? }, source/target are node ids — required |
height | number | 280 | Chart height in px; width fills the container |
valueFormatter | (value: number) => string | compact en-GB number | Formats tooltip values |
label | string | — | Accessible label for the chart region |
className | string | — | Applied to the chart container |