ApexSankey
August 12, 2026 · View on GitHub
A JavaScript library to create Sankey diagrams built on SVG
Dependency
Include svg.js
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js"></script>
Installation
To add the ApexSankey to your project and its dependencies, install the package from npm.
npm install apexsankey
Usage
import ApexSankey from 'apexsankey';
To create a basic sankey with minimal configuration, write as follows:
<div id="sankey-container"></div>
const data = {
...(data with format provided below)
}
const options = {
width: 800,
height: 800,
canvasStyle: 'border: 1px solid #caced0; background: #f6f6f6;',
spacing: 100,
nodeWidth: 20,
};
const sankey = new ApexSankey(document.getElementById('sankey-container'), options);
const graph = sankey.render(data);
Setting the License
To use ApexSankey with a commercial license, set your license key before creating any chart instances:
import ApexSankey from 'apexsankey';
// set license key before creating any charts
ApexSankey.setLicense('your-license-key');
const sankey = new ApexSankey(document.getElementById('sankey-container'), options);
const graph = sankey.render(data);
Methods
Methods available on an ApexSankey instance:
| Method | Description |
|---|---|
render(data) | Build the diagram from data ({ nodes, edges }) and write the SVG into the container. Returns the internal graph renderer, which exposes exportToSvg(). |
update(data) | Transition an already-rendered instance to new data. When the new layout shares the current topology (same nodes and flows, only different values or positions), the nodes and ribbons spring smoothly to their new places. When the topology differs, it morphs through the change: entering flows grow out of their source node, survivors slide to their new places, and removed flows retract and dissolve. When animation is disabled or the user prefers reduced motion, it redraws instantly. |
ApexSankey.setLicense(key) | Static. Set the commercial license key before creating any chart instances. |
ApexSankey.registerTheme(name, theme) | Static. Register (or override) a named theme (a SankeyTheme), then reference it by name via the theme option. Use for brand presets. |
ApexSankey.collapseGroups(data, groups, collapsed) | Static. Project a detailed { nodes, edges } to an aggregated one by collapsing the listed groups into super-nodes (re-routing and merging their flows). The pure transform behind the drillDown plugin; use it to seed the initial collapsed render. |
ApexSankey.compare(element, config) | Static. Render two linked diagrams (a "before" and an "after") side by side, outlining each flow by how it changed and highlighting the twin node or flow across both panels on hover. Returns a SankeyComparison handle (before / after / diff / destroy()). See Comparison split-view. |
on(event, handler) | Subscribe to an instance event (see Plugins & Events). Returns an unsubscribe function. |
off(event, handler) | Remove a handler previously registered with on. |
use(plugin) | Install a SankeyPlugin. Its install runs immediately; any teardown it returns runs on destroy(). Returns the instance. |
destroy() | Tear down the chart: run plugin teardowns, emit destroyed, drop all handlers, and remove injected DOM (tooltip container). |
ApexSankey Options
The layout can be configured by passing a second argument to ApexSankey with the properties listed below.
| Option | Type | Default | Description |
|---|---|---|---|
width | number | string | '100%' | Width of the canvas. Accepts a pixel number or CSS percentage string. |
height | number | string | 'auto' | Height of the canvas. 'auto' derives height from width at a 1.6:1 ratio. |
canvasStyle | string | built-in border | Arbitrary CSS injected onto the SVG root container element. |
type | 'sankey' | 'chord' | 'sankey' | Which projection of the flow model to draw. 'chord' renders a radial diagram (nodes as arcs on a ring, flows as ribbons across the interior) for dense many-to-many relationships. See Chord. |
arcCornerRadius | number | 6 | Chord only. Corner radius (px) for the rounded outer corners of each node arc (the inner edge, where ribbons meet, stays flush). 0 gives sharp corners; clamped to the ring band width. |
theme | string | SankeyTheme | undefined | A named built-in theme ('light', 'dark', 'midnight', 'mint', 'sunset'), a name registered via ApexSankey.registerTheme, or an inline SankeyTheme. Seeds coordinated visual defaults (palette, colors, background); options you set explicitly still win. See Themes. |
spacing | number | 20 | Horizontal spacing between node columns in pixels. |
nodeWidth | number | 20 | Width of each node rectangle in pixels. |
nodeBorderWidth | number | 1 | Border width of each node in pixels. |
nodeBorderColor | string | null | null | CSS color for the node border. null disables the border. |
nodePalette | string[] | undefined | Ordered fill colors cycled across nodes that do not set their own color. Overrides the built-in palette; a theme sets this for you. |
draggableNodes | boolean | false | Allow nodes to be repositioned by dragging with a pointer (mouse, touch, or pen). Connected flows follow the node live; the manual position holds until the next render()/update() recomputes the layout. |
onNodeClick | (node: SankeyNode) => void | undefined | Callback fired when the user clicks a node. |
edgeOpacity | number | 0.4 | Opacity of edges (0–1). |
edgeGradientFill | boolean | true | Fill edges with a gradient between source and target node colors. |
particleFlow | boolean | false | Animate particles drifting along each flow ribbon, with density proportional to the ribbon's value. Purely decorative; skipped under prefers-reduced-motion. |
edgeGap | number | 0 | Gap in pixels between adjacent edges at node connection points. |
whitespace | number | 0.18 | Fraction of vertical space used as margins between nodes (0–1). Lower = taller nodes. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Flow direction. 'horizontal' lays ranks out in columns (flows left→right); 'vertical' lays them in rows (flows top→bottom). |
axisTitles | string[] | undefined | Titles drawn above each column (or beside each row when vertical), one per rank — the axis/dimension labels of an alluvial diagram. Index i labels rank i. See Alluvial. |
viewPortWidth | number | 800 | Internal SVG viewport width in pixels. |
viewPortHeight | number | 500 | Internal SVG viewport height in pixels. |
highlightConnectedPath | boolean | true | Highlight connected flows on interaction. Hovering a node or flow gives a one-hop preview; clicking a node or flow pins an isolate of its full upstream/downstream path (click again, or click another, to change/release). |
dimOpacity | number | 0.2 | Opacity for dimmed (unrelated) elements when path highlighting is active. |
animation | { enabled: boolean, duration: number } | { enabled: true, duration: 800 } | Entrance animation. Automatically disabled when prefers-reduced-motion is set. |
enableTooltip | boolean | true | Show edge tooltips on hover. |
enableToolbar | boolean | true | Show the zoom/pan toolbar. |
tooltipId | string | 'apexsankey-tooltip-container' | HTML id for the tooltip container element. |
tooltipTemplate | (content: TooltipContent) => string | built-in | Custom function returning an HTML string for the edge (source→target) tooltip. |
nodeTooltipTemplate | (content: NodeTooltipContent) => string | built-in | Custom function returning an HTML string for the per-node tooltip. |
tooltipTheme | 'light' | 'dark' | undefined | Overrides tooltipBGColor/tooltipBorderColor/tooltipFontColor with a preset. |
tooltipBorderColor | string | '#E2E8F0' | Border color of the tooltip. |
tooltipBGColor | string | '#FFFFFF' | Background color of the tooltip. |
tooltipFontColor | string | '#1a1a1a' | Font color inside the tooltip. |
fontColor | string | '#212121' | CSS color for node labels. |
fontFamily | string | '' | CSS font-family for node labels. Falls back to the page default when empty. |
fontSize | string | '14px' | CSS font-size for node labels. |
fontWeight | string | '400' | CSS font-weight for node labels. |
a11y | { enabled?: boolean, diagramLabel?: string, description?: string } | { enabled: true } | WCAG 2.1 AA accessibility options. |
locale | { direction?: 'ltr' | 'rtl' | 'auto', messages?: Partial<SankeyMessages> } | { direction: 'ltr' } | Localization and text-direction. direction: 'rtl' mirrors the diagram horizontally (flows read right-to-left) and sets dir="rtl" on the container ('auto' defers to the document). messages overrides the screen-reader strings the diagram generates. |
Chord
Set type: 'chord' to draw the radial projection of the same { nodes, edges } model: each node becomes an arc on a ring (its span proportional to total incident flow) and each edge a ribbon crossing the interior. It suits dense many-to-many or symmetric relationships (migration, co-occurrence, adjacency) where a layered Sankey turns into spaghetti. Hovering an arc or ribbon focuses its connections and dims the rest. The arcs' outer corners are rounded by default (arcCornerRadius); set it to 0 for sharp corners.
const sankey = new ApexSankey(el, {type: 'chord'});
sankey.render({
nodes: [
{id: 'A', title: 'A'},
{id: 'B', title: 'B'},
{id: 'C', title: 'C'},
],
edges: [
{source: 'A', target: 'B', value: 12, type: 'x'},
{source: 'B', target: 'C', value: 8, type: 'x'},
{source: 'C', target: 'A', value: 5, type: 'x'},
],
});
Chord mode reuses the tooltip, node/edge click events, and color palette. Sankey-only features (multi-orientation, RTL mirror, animated relayout, node dragging, particle flow) do not apply to it.
Alluvial
An alluvial diagram is the same engine as a Sankey, reached through the same { nodes, edges } model. ApexSankey.buildAlluvialData (also exported as buildAlluvialData) turns categorical records-across-dimensions into that model so you do not hand-build it: it creates one node per (dimension, category) and one edge per adjacent-dimension transition, with each category keeping the same color across every dimension so a cohort reads as one continuous stream. Pair it with axisTitles for the dimension labels.
const input = {
dimensions: ['2019', '2022', '2025'],
records: [
{values: {2019: 'Free', 2022: 'Pro', 2025: 'Pro'}},
{values: {2019: 'Free', 2022: 'Free', 2025: 'Churned'}},
{values: {2019: 'Pro', 2022: 'Pro', 2025: 'Team'}, value: 3},
],
};
const sankey = new ApexSankey(el, {axisTitles: input.dimensions});
sankey.render({...ApexSankey.buildAlluvialData(input), options: sankey.options});
AlluvialInput fields:
| Field | Type | Description |
|---|---|---|
dimensions | string[] | Ordered dimension (axis) ids, left → right. |
records | AlluvialRecord[] | The subjects flowing across the dimensions. |
palette | string[] | Optional category color palette, cycled per distinct category. |
Each AlluvialRecord is { values: Record<string, string>, value?: number } — the category at each dimension (keyed by dimension id), and the weight it contributes (default 1). A record missing a category at some dimension simply skips that adjacency.
Plugins & Events
Subscribe to instance events with on (it returns an unsubscribe function), or package reusable behaviour as a SankeyPlugin and install it with use.
// events
const off = sankey.on('node:click', ({id, node, originalEvent}) => {
console.log('clicked', id);
});
// ...later
off();
// a reusable plugin: log every clicked node, cleaned up on destroy()
sankey.use({
name: 'click-logger',
install: ({chart, on}) => on('node:click', ({id}) => console.log('clicked', id)),
});
Emitted events and their payloads:
| Event | Payload | When |
|---|---|---|
node:click | { id, node, originalEvent } | A node is clicked. |
node:mouseenter | { id, node, originalEvent } | The pointer enters a node. |
node:mouseleave | { id, node, originalEvent } | The pointer leaves a node. |
edge:click | { source, target, value, originalEvent } | A flow (edge) is clicked. |
edge:mouseenter | { source, target, value, originalEvent } | The pointer enters a flow. |
edge:mouseleave | { source, target, value, originalEvent } | The pointer leaves a flow. |
rendered | none | After the initial render and after each update() settles. |
destroyed | none | On destroy(). |
A SankeyPlugin is { name, install(ctx) }. install receives { chart, on } and may return a teardown function that runs on destroy(). Every subscription made through ctx.on is also released automatically on destroy().
Built-in plugins
pathTrace ships with the library (importable as pathTrace, or ApexSankey.plugins.pathTrace). Installing it makes a bright pulse cascade along the connected flow path when a node is picked, ribbon by ribbon, cueing the eye to where the flow goes. It is skipped under prefers-reduced-motion and applies to the Sankey projection (not chord).
sankey.use(pathTrace({direction: 'downstream'}));
PathTraceOptions | Type | Default | Description |
|---|---|---|---|
trigger | 'click' | 'hover' | 'click' | What starts a trace. |
direction | 'downstream' | 'upstream' | 'both' | 'downstream' | Which way flow is traced from the picked node. |
color | string | '#ffffff' | Pulse color. |
duration | number | 700 | Milliseconds for a pulse to cross one ribbon. |
stagger | number | 220 | Milliseconds added per hop, so the trace cascades outward. |
timePlayback steps the diagram through a sequence of data frames, driven by the chart's own update() so topology-stable frames spring smoothly from one to the next. It ships a small control bar (play/pause + a scrubber). Importable as timePlayback or ApexSankey.plugins.timePlayback.
sankey.render({...frames[0], options: sankey.options});
sankey.use(timePlayback({frames, interval: 1500, loop: true}));
TimePlaybackOptions | Type | Default | Description |
|---|---|---|---|
frames | TimePlaybackFrame[] | required | Ordered frames ({ nodes, edges, label? }). Share topology across frames for a smooth morph. |
interval | number | 1600 | Milliseconds each frame is shown before advancing. |
autoplay | boolean | false | Start playing on install. |
loop | boolean | false | Loop back to the first frame after the last. |
controls | boolean | true | Render the built-in control bar. |
mount | HTMLElement | after the chart | Where to render the control bar. |
drillDown turns a large diagram into a set of super-nodes you expand on demand: click a super-node to reveal its constituent flows, click any of its children to collapse it back. The drillable nodes get a pointer cursor so they read as clickable. Expanding animates the children growing out of the super-node (and shrinking back into it on collapse) with their ribbons — driven through the chart's own update() (which accepts an optional SankeyTransition hint for this). It is built on the pure ApexSankey.collapseGroups transform (also exported as collapseGroups), which re-routes and merges the flows of a collapsed group. Importable as drillDown or ApexSankey.plugins.drillDown; applies to the Sankey projection (not chord). Seed the initial collapsed render with ApexSankey.collapseGroups so there is no load-time morph.
const detail = {nodes, edges};
const groups = [{id: 'Fossil', title: 'Fossil', children: ['Coal', 'Gas', 'Oil']}];
sankey.render({...ApexSankey.collapseGroups(detail, groups, ['Fossil']), options: sankey.options});
sankey.use(drillDown({...detail, groups}));
DrillDownOptions | Type | Default | Description |
|---|---|---|---|
nodes | SankeyGraphNode[] | required | The full, detailed leaf nodes before any collapsing. |
edges | SankeyGraphEdge[] | required | The full, detailed flows between the leaf nodes. |
groups | DrillDownGroup[] | required | Group definitions ({ id, title, color?, children }); each collapses its children into one super-node. |
expanded | string[] | [] | Group ids expanded on install; every other group starts collapsed. |
Comparison split-view
ApexSankey.compare(element, config) renders two diagrams of the same flow model side by side — a "before" and an "after". It outlines each flow by how it changed between them (added, removed, or changed, from the pure diffGraphs transform, also exported), draws a legend, and links the panels so hovering a node or flow highlights its twin in the other. It is built on the public surface only (two ordinary ApexSankey instances plus their node:* and edge:* events) and returns a SankeyComparison handle exposing before, after, the computed diff, and destroy().
const cmp = ApexSankey.compare(el, {
before: {nodes, edges: edges2024, title: '2024'},
after: {nodes, edges: edges2025, title: '2025'},
});
// later: cmp.destroy();
ComparisonConfig | Type | Default | Description |
|---|---|---|---|
before | { nodes, edges, title? } | required | The left panel's flow graph and optional heading. |
after | { nodes, edges, title? } | required | The right panel's flow graph and optional heading. |
options | Partial<SankeyOptions> | {} | Base options shared by both panels (each manages its own width). |
syncHighlight | boolean | true | Highlight the twin node or flow in the other panel on hover (a cursor lands on a ribbon at least as readily as on a node). |
showDiff | boolean | true | Outline added / removed / changed flows in each panel. |
showLegend | boolean | true | Render the diff color legend below the panels. |
diffColors | { added?, removed?, changed? } | greens/reds/ambers | Override the diff outline colors. |
Themes
Pass theme to seed a coordinated set of visual defaults in one shot. Built-in themes: 'light' (the default look), 'dark', 'midnight', 'mint', and 'sunset'. A theme sits between the built-in defaults and your explicit options, so anything you set yourself still wins.
const sankey = new ApexSankey(el, {theme: 'dark'});
Register a brand preset once, then reference it by name:
ApexSankey.registerTheme('acme', {
nodePalette: ['#ff5a5f', '#087f8c', '#5d2e8c'],
fontColor: '#1a1a1a',
canvasStyle: 'background: #faf7f2; box-sizing: border-box;',
});
const sankey = new ApexSankey(el, {theme: 'acme'});
You can also pass an inline SankeyTheme object directly as theme. A SankeyTheme may set any subset of the following (each maps to the option of the same name; omitted fields keep their default):
SankeyTheme field | Type | Description |
|---|---|---|
nodePalette | string[] | Ordered node fill colors, cycled across nodes without their own color. |
fontColor | string | CSS color for node labels. |
edgeOpacity | number | Opacity of the flow ribbons (0–1). |
edgeGradientFill | boolean | Fill ribbons with a source→target gradient. |
nodeBorderColor | string | null | CSS color for the node border (null disables). |
canvasStyle | string | CSS on the SVG root container, typically the background and border. |
tooltipTheme | 'light' | 'dark' | Tooltip color preset. |
Localization & RTL
The diagram's screen-reader strings live in SankeyMessages; pass a Partial<SankeyMessages> via locale.messages to translate any subset (unset keys keep their English defaults, exported as DEFAULT_SANKEY_MESSAGES). Visible tooltips are localized separately via tooltipTemplate / nodeTooltipTemplate.
SankeyMessages key | Type | Description |
|---|---|---|
diagramLabel | (ctx: SankeyDiagramLabelContext) => string | SVG root aria-label summary (node/flow counts + largest flow). |
nodeAriaLabel | (ctx: SankeyNodeLabelContext) => string | Per-node aria-label (incoming/outgoing flow summary). |
edgeAriaLabel | (ctx: SankeyEdgeLabelContext) => string | Per-edge aria-label. Default: Flow from {source} to {target}: {value} units. |
nodesGroupLabel | string | aria-label for the <g> wrapping all nodes. Default: 'Sankey nodes'. |
const sankey = new ApexSankey(el, {
locale: {
direction: 'rtl',
messages: {nodesGroupLabel: 'العقد'},
},
});
Default tooltip template
const tooltipTemplate = ({source, target, value}) => {
return `
<div style='display:flex;align-items:center;gap:5px;'>
<div style='width:12px;height:12px;background-color:${source.color}'></div>
<div>${source.title}</div>
<div>=></div>
<div style='width:12px;height:12px;background-color:${target.color}'></div>
<div>${target.title}</div>
<div>: ${value}</div>
</div>
`;
},
Expected data format
Passed data should be an object containing nodes, edges and options. Nodes, edges and options should be in below format.
- nodes : Passed node object should contain id and title. Id is for uniquely identifying nodes and title is for node titles. It will be also used for showing tooltips for node-to-node connections.
{
"id": "1", // required
"title": "A" // required
}
- edges : Passed edge object should contain source, target, value and type. source is id value of source node for edge, target is id value of target node for edge, value indicates edge size and type is for grouping nodes.
{
"source": "a", // required
"target": "b", // required
"value": 1, // required
"type": "x", // optional
},
-
options : ApexSankey supports two options order and alightLinkTypes.
-
order: optional list of layers
If order is not specified, the nodes are automatically assigned to layers. If order is specified, it is used directly and no rank assignment or ordering algorithm takes place.
The order structure has three nested lists: order is a list of layers, each of which is a list of bands, each of which is a list of node ids. For example,
{ "order": [ [["a", "b"]], [["c"]], ], },
-
Example
const data = {
nodes: [
{
id: 'a',
title: 'AAA',
},
{
id: 'b',
title: 'BBB',
},
{
id: 'c',
title: 'CCC',
},
],
edges: [
{
source: 'a',
target: 'c',
value: 1,
type: 'A',
},
{
source: 'b',
target: 'c',
value: 2,
type: 'A',
},
],
options: {
order: [[['a', 'b']], [['c']]],
},
};
Family theme tokens (--apx-*)
Every chart in the ApexCharts family reads the same five root tokens, so a page can state its brand once and have trees, flow diagrams, Gantt charts and plots all follow:
| Token | Role |
|---|---|
--apx-accent | The colour that means interactive or selected |
--apx-fore | Text and anything that must stay legible on the surface |
--apx-grid | Hairlines: borders, gridlines, connectors |
--apx-surface | The plane content sits on |
--apx-series-1 … --apx-series-N | An ordered categorical palette (1-based, stops at the first gap) |
:root {
--apx-accent: #5b21b6;
--apx-fore: #101828;
--apx-grid: #e4e7ec;
--apx-surface: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--apx-fore: #f8fafc;
--apx-grid: #334155;
--apx-surface: #0f172a;
}
}
Custom properties inherit, so declaring them on :root reaches every chart on the page. They resolve below anything you configured explicitly, so adopting them cannot change a chart that was already themed:
product CSS variable > explicit option > --apx-* token > built-in default
An option set to a value equal to its built-in default is indistinguishable from one left alone, and the token wins there. Set a product variable if you need a value pinned regardless.
Named themes
registerTheme from @apex/commons records a named set of tokens on a registry shared by the whole family, so a brand theme registered once from any product is resolvable by name from all of them:
import {registerTheme} from '@apex/commons';
registerTheme('acme', {
tokens: {accent: '#5b21b6', fore: '#101828', grid: '#e4e7ec', surface: '#ffffff'},
});
A named theme's tokens sit one layer below the CSS --apx-* tokens, so the cascade still wins over the registry.
Reference it through the theme option, alongside the Sankey-specific named themes described in Themes. The two registries are complementary: a Sankey theme carries diagram-specific defaults (palette, edge opacity, canvas style), while a family theme carries the cross-product token roles.