Sankey
Flow between stages, where band width is the quantity moving. Nodes are layered by longest path so every link runs forward, and a cycle is reported rather than drawn as something plausible.
src/components/ui/sankey.tsxAbout
From the component's own source, where it was written.
Flow between stages, where the width of a band is the quantity moving.
**This is the chart for "where did it all go".** Traffic from source to landing page to conversion, a budget from department to project, energy from generation to loss — anything where a quantity splits and merges and the interesting part is the *proportion* taking each route. A funnel shows the same drop-off as a single column of shrinking bars; a Sankey shows where the loss went, which is usually the actual question.
**Nodes are placed in layers by longest path from a source, not by input order.** Using the order the caller happened to list them in produces backward-pointing links and a diagram that reads as a tangle. Layering by depth guarantees every link runs left to right.
**It requires a DAG and says so.** A cycle has no consistent layering — the depth of a node in a loop is undefined — so a cycle is detected and reported through onError rather than being drawn as something plausible but wrong.
Bands are cubic Béziers with horizontal control points, which is what keeps them readable where several cross: the eye follows a smooth curve through an intersection and loses a polyline.
Install
npx astralyx-ui add sankey
Usage
import { Sankey } from '@/components/ui/sankey'
<Sankey nodes={nodes} links={links} />Composer
Change the props and the source below follows.
- Organic6.8k
- Paid3.1k
- Referral1.5k
- Landing7.9k
- Docs3.5k
- Sign-up3.8k
- Activated2.6k
- Bounced8.8k
<Sankey
nodes={nodes}
links={links}
nodeWidth={3}
/>Examples
Acquisition to activation
<Sankey nodes={nodes} links={links} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
Sankey
Extends Omit<ComponentProps<'figure'>, 'height' | 'onError'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| nodes | SankeyNode[] | required | { id, label, color? } and { source, target, value }. |
| links | SankeyLink[] | required | { id, label, color? } and { source, target, value }. |
| height | number | 320 | — |
| nodeWidth | number | 3 | Width of a node column, in the 100-wide coordinate space. |
| nodePadding | number | 2 | Vertical gap between nodes in the same layer. |
| valueFormat | (value: number) => string | DEFAULT_FORMAT | — |
| showValues | boolean | true | — |
| onError | (error: Error) => void | — | — |
| emptyLabel | string | 'No flows.' | — |
| label | string | 'Sankey diagram' | — |
Types
SankeyNode
Flow between stages, where the width of a band is the quantity moving.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| label | string | — |
| color? | string | — |
SankeyLink
| Field | Type | Description |
|---|---|---|
| source | string | — |
| target | string | — |
| value | number | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| vs Funnel | where it went | A funnel shows the same drop-off as shrinking bars. A Sankey shows where the loss went, which is usually the actual question. |
| layering | longest path | Derived, not taken from input order — laying nodes out in the order they were listed produces backward links and a tangle. |
| cycles | reported | Depth is undefined inside a loop, so a cycle raises onError instead of being drawn wrongly. |
| ribbons | filled, not stroked | The plot is stretched to fill its box and a stroke under a non-uniform transform scales unevenly — the band width would change with the container’s aspect ratio, which is a lie about the quantity. |