sankeyDiagram lays out a directed weighted graph in final plot pixels, then composes ordinary marks over its nodes and links. Source rows stay semantic; the application does not clone D3 records, resolve mutated endpoints, or materialize positioned DTOs.
import { defineChart, link, rect, text } from '@tanstack/charts'
import { sankeyDiagram } from '@tanstack/charts/network/sankey'
const chart = defineChart({
marks: [
sankeyDiagram({
nodes,
links,
nodeKey: 'id',
source: 'source',
target: 'target',
value: 'value',
align: 'left',
nodeWidth: ({ width }) => Math.max(12, width * 0.025),
nodePadding: ({ height }) => Math.max(12, height * 0.06),
inset: 24,
marks: ({ nodes: layoutNodes, links: layoutLinks }) =>
[
link(layoutLinks, {
x1: 'x1',
y1: 'y1',
x2: 'x2',
y2: 'y2',
key: 'key',
strokeWidth: (flow) => Math.max(1, flow.width),
lineCap: 'butt',
}),
rect(layoutNodes, {
x1: 'x0',
x2: 'x1',
y1: 'y0',
y2: 'y1',
key: 'key',
inset: 0,
}),
text(layoutNodes, {
x: 'x',
y: 'y',
text: (node) => node.data.label,
key: 'key',
}),
] as const,
}),
],
guides: false,
})The exact @tanstack/charts/network/sankey subpath keeps d3-sankey and the resolved flow adapter out of root, universal, force-layout, and ordinary-mark bundles. The Charts package owns d3-sankey; the application needs no direct dependency unless its own source imports that module.
nodes and links accept iterables. nodeKey identifies each node; source and target read those keys from each link; value reads its nonnegative weight.
sankeyDiagram({
nodes,
links,
nodeKey: 'id',
source: 'from',
target: 'to',
value: 'amount',
marks,
})Keys must be strings or finite numbers. Graph validation rejects duplicate node keys, missing endpoints, duplicate authored link keys, and negative or nonfinite values. A nonempty graph must contain at least one positive link; zero-valued links may coexist with positive flow. The layout also rejects cycles.
Use linkKey when links can be reordered or when several links share the same endpoints. Without it, a unique raw id field is used when every link has one; otherwise keys use source, target, and parallel-link occurrence.
Input rows are never mutated. Accessors run when the mark is created; repeated responsive layout passes use fresh private graph records.
| Option | Type | Default | Meaning |
|---|---|---|---|
| nodes | Iterable<TNode> | Required | Semantic node rows |
| links | Iterable<TLink> | Required | Semantic weighted edges |
| nodeKey | TransformValue<TNode, ChartKey> | Required | Node identity |
| source | TransformValue<TLink, ChartKey> | Required | Source-node identity |
| target | TransformValue<TLink, ChartKey> | Required | Target-node identity |
| value | TransformValue<TLink, number> | Required | Nonnegative link weight |
| linkKey | TransformValue<TLink, ChartKey> | Inferred | Stable link identity |
| align | left | right | center | justify | justify | Horizontal node-layer alignment |
| nodeSort | SankeyNodeComparator<TNode> | null | D3 order | Same-column ordering; null preserves input order |
| linkSort | SankeyLinkComparator<TNode, TLink> | null | D3 order | Link ordering inside each node; null preserves input order |
| nodeWidth | number | (chart: ChartBounds) => number | 24 | Positive final-pixel node width |
| nodePadding | number | (chart: ChartBounds) => number | 8 | Nonnegative final-pixel separation within a layer |
| inset | number | SankeyInset | responsive callback | 0 | Final-pixel inset from the resolved plot bounds |
| iterations | number | 6 | Nonnegative integer relaxation-pass count |
| marks | (context: SankeyDiagramContext) => nonempty marks | Required | Ordinary marks over final-pixel node and link rows |
| id | string | Layer-derived | Stable parent mark identity |
| motion | ChartMotionDefinition | None | Parent motion policy merged with each child's motion policy |
Responsive nodeWidth, nodePadding, and inset callbacks receive the final ChartBounds. Insets may be one number or separate top, right, bottom, and left values. The remaining extent must fit the node width and have a positive height.
The marks callback receives { id, chart, nodes, links }. It must return at least one ordinary mark. Positional channels in those child marks are already final pixels, so a Sankey definition needs no x or y scale and normally hides Cartesian guides.
The callback may run more than once while responsive margins and guides converge. Keep it synchronous, deterministic, and free of input mutation or external side effects.
Use the callback for presentation that belongs to the visualization: link curve and paint, node color, label side and wording, backdrops, or a title. Those decisions remain visible beside the marks rather than becoming hidden layout data preparation.
Child scene keys, mark IDs, and interaction points are scoped under the Sankey mark. Color channels, layout labels, focus states, and child motion retain their ordinary behavior. A child cannot itself require resolved mark layout; nested responsive layout passes are rejected.
Every SankeyNode<TNode, TLink> contains:
Every SankeyLink<TNode, TLink> contains:
The output rows and their lineage arrays are immutable. Node and link visual, sort, tooltip, and motion callbacks can reach the original row through data.
The exact entry exports sankeyDiagram, SankeyAlignment, SankeyInset, SankeyLayoutValue, SankeyEndpointContext, SankeyNodeContext, SankeyLinkContext, SankeyNode, SankeyLink, SankeyDiagramContext, SankeyNodeComparator, SankeyLinkComparator, and SankeyDiagramOptions.
See Rules, Links, Arrows, Vectors, and Ticks for link curves and paint, and Networks and Hierarchies for the catalog cases.