Sankey
Trace weighted flow from categories to connected measures.
Chart ID: sankey. Independent import: @revolist/charts/sankey.
Live example
Section titled “Live example”Loading chart…
Reading the chart
Section titled “Reading the chart”Trace weighted flow through multiple connected stages in a directed acyclic graph, or retain category-to-series matrix inputs. Ribbon width represents link weight; stable identities preserve data and callbacks.
Data requirements
Section titled “Data requirements”Use createChartFlowData for a general directed acyclic node/link graph, or retain a non-negative category-by-series matrix. Explicit source/target dimension bindings and weight measures also support mapped analytical frames. See the general graph contract below.
Use non-negative values for this chart. Negative values are rejected; explicit nulls represent missing observations.
The roles below describe the chart's data contract. Generic category/value roles resolve from the dataset; explicitly bind specialized numeric roles as shown in the complete example.
| Data role | Field count |
|---|---|
source | 1 or more |
target | 1 or more |
weight | 1 or more |
Download the complete sample data and settings (JSON). See Data & Configuration for mapping, null values, updates and lifecycle rules.
Create this chart
Section titled “Create this chart”Install the package using the installation guide, give the host an explicit height, and call createSankeyChart after the host mounts. This factory fixes the chart type to sankey.
<div id="chart" style="width: 100%; height: 450px;"></div>Complete runnable TypeScript example with sample data
import { createSankeyChart } from '@revolist/charts/sankey';
const container = document.getElementById('chart')!;const chart = await createSankeyChart({ container, "dataset": { "revision": 0, "source": "client", "categories": [ { "id": "preview-category-0", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ], "depth": 1, "kind": "leaf" }, { "id": "preview-category-1", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ], "depth": 1, "kind": "leaf" }, { "id": "preview-category-2", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ], "depth": 1, "kind": "leaf" } ], "series": [ { "id": "preview-series-0", "label": "Revenue", "columnPath": [ "Revenue" ], "labels": [ "Revenue" ], "kind": "leaf", "prop": "preview-0" }, { "id": "preview-series-1", "label": "Margin", "columnPath": [ "Margin" ], "labels": [ "Margin" ], "kind": "leaf", "prop": "preview-1" }, { "id": "preview-series-2", "label": "Units", "columnPath": [ "Units" ], "labels": [ "Units" ], "kind": "leaf", "prop": "preview-2" } ], "values": [ [ 42, 24, 34 ], [ 66, 39, 52 ], [ 54, 46, 63 ] ], "analyticalFrame": { "fields": [ { "id": "pivot:category", "label": "pivot:category", "role": "dimension", "semanticType": "category" }, { "id": "pivot:series", "label": "pivot:series", "role": "dimension", "semanticType": "category" }, { "id": "preview-series-0", "label": "Revenue", "role": "measure", "semanticType": "number", "sourceField": "preview-0" }, { "id": "preview-series-1", "label": "Margin", "role": "measure", "semanticType": "number", "sourceField": "preview-1" }, { "id": "preview-series-2", "label": "Units", "role": "measure", "semanticType": "number", "sourceField": "preview-2" } ], "observations": [ { "id": "[\"preview-category-0\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 42 } }, { "id": "[\"preview-category-0\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 24 } }, { "id": "[\"preview-category-0\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 34 } }, { "id": "[\"preview-category-1\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 66 } }, { "id": "[\"preview-category-1\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 39 } }, { "id": "[\"preview-category-1\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 52 } }, { "id": "[\"preview-category-2\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 54 } }, { "id": "[\"preview-category-2\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 46 } }, { "id": "[\"preview-category-2\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 63 } } ], "matrixCells": [ { "categoryId": "preview-category-0", "seriesId": "preview-series-0", "observationId": "[\"preview-category-0\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-0", "seriesId": "preview-series-1", "observationId": "[\"preview-category-0\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-0", "seriesId": "preview-series-2", "observationId": "[\"preview-category-0\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-0", "observationId": "[\"preview-category-1\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-1", "observationId": "[\"preview-category-1\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-2", "observationId": "[\"preview-category-1\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-0", "observationId": "[\"preview-category-2\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-1", "observationId": "[\"preview-category-2\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-2", "observationId": "[\"preview-category-2\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" } ] } }, "bindings": { "source": [ "pivot:category" ], "target": [ "pivot:series" ], "weight": [ "preview-series-0", "preview-series-1", "preview-series-2" ] }, "presentation": { "dataLabels": false }});
// When the host unmounts:// chart.destroy();Await updates before reading a snapshot or exporting. Call chart.destroy() when the host unmounts. See the live catalog to interact with this type.
Supported options
Section titled “Supported options”Set these fields in chartOptions. The option schema validates supported keys, ranges and choices; updating chartOptions replaces that authored object.
| Option | Default | Accepted values |
|---|---|---|
markOpacity | 1 | 0.2 to 1; step 0.05 |
nodeWidth | 14 | 1 to 100; step 1 |
nodePadding | 8 | 0 to 100; step 1 |
layoutIterations | 0 | 0 to 64; step 1 |
linkCurvature | 0.48 | 0 to 1; step 0.01 |
nodeAlignment | left | left, right, center, justify |
nodeSort | auto | auto, fixed, valueAscending, valueDescending, label |
linkSort | fixed | auto, fixed, valueAscending, valueDescending |
showNodeLabels | true | true or false |
showNodeValues | false | true or false |
showLinkValues | false | true or false |
linkColorMode | default | default, source, target |
linkOpacity | 0.46 | 0 to 1; step 0.01 |
nodeHighlight | links | nodes, links, incoming, outgoing, none |
linkHighlight | links | links, nodes, source, target, none |
nodeFade | none | global, none |
linkFade | none | global, none |
Controlled highlighting also persists highlightedFlowItem in chart options. Node/link callbacks, comparator functions and runtime color/value formatters belong to renderer options; see the detailed guide below.
Titles, legends, theme and labels belong to presentation or the corresponding top-level setting. Axis, color, navigation and interaction support depends on the selected chart; see Data & Configuration.
Related charts
Section titled “Related charts”- Chord — Compare weighted relationships through circular ribbons.
Charts overview · Installation · Data & Configuration
General graphs, configuration and runtime controls
Section titled “General graphs, configuration and runtime controls”createChartFlowData converts a directed node/link graph to the shared Charts dataset and semantic bindings. The same data can be used with standalone Charts, an unlinked Pivot chart, or a Pivot chart data provider. Existing category-to-series Sankey datasets remain supported.
import { createChart, createChartFlowData } from '@revolist/charts';
const flow = createChartFlowData({ nodes: [ { id: 'revenue', label: 'Revenue' }, { id: 'gross', label: 'Gross profit' }, { id: 'costs', label: 'Operating costs' }, { id: 'net', label: 'Net profit' }, ], links: [ { id: 'sales', source: 'revenue', target: 'gross', value: 100 }, { id: 'expenses', source: 'gross', target: 'costs', value: 40 }, { id: 'profit', source: 'gross', target: 'net', value: 60 }, ],});const chart = await createChart({ container: document.getElementById('chart')!, chartType: 'sankey', ...flow, chartOptions: { nodeAlignment: 'justify', // left (default), right, center, justify nodeWidth: 14, // pixels; constrained to fit the drawing area nodePadding: 8, // pixels; constrained to fit dense columns layoutIterations: 8, // 0 (default) preserves first-link appearance order linkCurvature: 0.48, // 0..1 },});Layout option limits are validated before rendering:
| Option | Default | Accepted values |
|---|---|---|
nodeWidth | 14 | 1..100 pixels |
nodePadding | 8 | 0..100 pixels |
layoutIterations | 0 | Integer 0..64 |
linkCurvature | 0.48 | 0..1 |
nodeAlignment | left | left, right, center, justify |
Rendered widths/padding can shrink to fit the host. These fields are Sankey-only; unsupported chart options reject.
Nodes are inferred when nodes is omitted. When supplied, node IDs must be unique. Endpoints absent from a partial node list are inferred from links. Equal labels do not merge different IDs. Repeated links remain separate canonical observations. Supply stable link IDs for reorder/update identity; omitted IDs follow input order.
Weights must be finite and non-negative. Zero-weight links remain in data/CSV but have no geometry. Nodes without positive connections are omitted from geometry. Positive links must form a directed acyclic graph; self-links and cycles are rejected. Node heights use the greater of incoming and outgoing weight, with one shared width scale for all links. Overflowing raw totals do not produce infinite SVG coordinates.
The helper returns { dataset, bindings }. Link IDs are category IDs; numeric values remain in the authoritative category-by-series matrix. Source and target identities/labels live in a mapped analytical frame. Saved models retain that mapping; CSV, tooltips and link activation use the ordinary chart pipeline. Nodes expose aggregate keyboard inspection and do not invent a source-cell drill target.
For an existing mapped analytical frame, bind source and target to dimension field IDs and weight to mapped measure/series IDs. Matching endpoint paths (or scalar values when no path is supplied) identify the same node across stages. This lets Pivot data providers and hierarchy dimensions use the shared graph renderer without converting the graph back to category/series partitions. Pivot compatibility preserves valid explicit source/target bindings.
Update both the dataset and bindings when replacing the graph:
await chart.update(createChartFlowData({ links: [{ id: 'sales', source: 'Revenue', target: 'Profit', value: 65 }],}));For Pivot, pass the same dataset and bindings through createStandalonePivotChart from @revolist/pivot/charts or return the mapped dataset from the chart data provider with corresponding model bindings. Source shaping, selection and drill remain owned by Pivot.
Colors, ordering and labels
Section titled “Colors, ordering and labels”Node data accepts color; link data accepts a color or 'source' / 'target'. Colors use the shared hexadecimal color contract. The helper retains declared node order and colors in dataset.analyticalFrame.derived.flowAppearance, so save/restore, SSR and Pivot providers preserve them without duplicating matrix values. Inferred nodes follow declared nodes, in first-link appearance order. Connected nodes alone acquire geometry.
chartOptions also accepts:
nodeSort:'auto'(default),'fixed','valueAscending','valueDescending','label'. Auto useslayoutIterations; fixed and explicit orders skip barycentric reordering. The existing default of zero iterations preserves authored order.linkSort:'fixed'(default),'auto','valueAscending','valueDescending'. Auto orders slots by the opposite node’s vertical position. Incoming and outgoing slots sort independently, with stable ties and conserved widths. Rendering and canonical link data stay in dataset order.linkColorMode:'default'(default),'source','target';linkOpacity:0..1(default0.46). Per-item colors win over defaults.showNodeLabels(defaulttrue),showNodeValuesandshowLinkValues(defaultfalse). Dense/small nodes retain their accessible descriptions when visible labels are omitted.
Pivot exposes the same controls from the shared schema. Chart-type transitions remove unsupported options and controlled highlight state while retaining compatible appearance settings.
Runtime callbacks
Section titled “Runtime callbacks”Pass sankey to rendererOptions in standalone Charts, or to createPivotChartsRenderer in Pivot:
const rendererOptions = { sankey: { nodeSort: (a, b) => a.label.localeCompare(b.label), linkSort: (a, b) => a.value - b.value, nodeColor: (node) => node.label === 'Profit' ? '#16834a' : undefined, linkColor: 'source', valueFormatter: (value, { kind, location }) => location === 'tooltip' ? `${kind}: ${value} USD` : `${value} USD`, onNodeClick: ({ node, originalEvent }) => inspectNode(node), onLinkClick: ({ link, originalEvent }) => inspectLink(link.category.id), onHighlightChange: (item) => showRelatedDetails(item), },};Comparators receive read-only canonical graph descriptors and must return finite numbers; equal values retain authored order. Runtime comparators override the corresponding built-in sort policy. nodeColor also accepts a literal hexadecimal color. Color callbacks return a hexadecimal color or undefined to retain the default. linkColor also accepts 'source' or 'target'. Authored per-item colors take precedence. valueFormatter receives a context containing kind, item (the node/link descriptor) and location. The location is a 'tooltip'/'label' location; node label values require showNodeValues. Returned values must be strings. Callbacks and renderer options remain runtime-only and must be supplied again when restoring a saved model.
rendererOptions.sankey.onNodeClick handles aggregate graph nodes, including keyboard Enter/Space, without inventing a source cell to drill. onLinkClick handles links. Links also retain the ordinary top-level onNodeClick canonical data/drill pipeline, once per activation. Aggregate nodes and links share the chart’s existing roving keyboard targets.
Highlighting
Section titled “Highlighting”Node nodeHighlight policies are 'nodes', 'links' (default), 'incoming', 'outgoing' and 'none'. Link linkHighlight policies are 'links' (default), 'nodes', 'source', 'target' and 'none'. The active item remains highlighted with its related items. 'none' disables highlighting and fading. nodeFade / linkFade accept 'global' or 'none' (default); global fades other flow shapes to 20 percent through a wrapper, preserving authored mark paint. Enabled node captions and link values stay at their authored opacity in a foreground text layer during pointer, keyboard and controlled highlighting; fading does not hide the labels.
Pointer and keyboard focus highlight transiently. Repaint retains an existing semantic target; removing that target clears the transient highlight. onHighlightChange reports item requests and programmatic highlight changes. Use chartOptions.highlightedFlowItem to control/programmatically set highlighting:
await chart.update({ chartOptions: { ...chart.model.chartOptions, highlightedFlowItem: { kind: 'link', categoryId: 'sales', seriesId: 'flow:value' }, },});Node identifiers are { kind: 'node', nodeId }, where the graph’s canonical node ID comes from callbacks or the rendered node. For helper scalar IDs, it is node: followed by the JSON array of that ID (for example node:["revenue"]). Link identifiers use canonical category and series IDs, so parallel links remain distinguishable and reordering does not change their identity. An unknown saved target remains dormant and does not fade the graph. Explicit null controls the graph with no highlight; omit the option to restore transient hover/focus behavior. Controlled state is saved; pointer/focus state is not. SVG export captures the current painted state.
These controls cover the corresponding sorting, styling, callbacks and highlight policies in the MUI Sankey reference. Public composition primitives and the remaining general chart roadmap are separate work.