Plugins and Events
Behavior that is not every diagram's business lives in plugins rather than in options. The same event bus plugins are built on is public, so anything a built-in plugin does, your own code can do too.
Events
on(event, handler) subscribes and returns an unsubscribe function:
const off = sankey.on('node:click', ({ id }) => showDetail(id))
// later
off()
// or
sankey.off('node:click', handler)
| Event | Payload | Fires |
|---|---|---|
node:click | SankeyNodeEventArgs | A node is clicked |
node:mouseenter | SankeyNodeEventArgs | Pointer enters a node |
node:mouseleave | SankeyNodeEventArgs | Pointer leaves a node |
edge:click | SankeyEdgeEventArgs | A flow is clicked |
edge:mouseenter | SankeyEdgeEventArgs | Pointer enters a flow |
edge:mouseleave | SankeyEdgeEventArgs | Pointer leaves a flow |
rendered | none | After the initial render, and after each update() settles |
destroyed | none | During destroy(), before handlers are dropped |
The map is typed, so a handler's argument is narrowed by the event name in TypeScript.
rendered is the hook for anything that needs final geometry, since it fires after a transition settles rather than when it starts:
sankey.on('rendered', () => enableExportButton())
Writing a plugin
A plugin is an object with a name and an install function. install receives a context and may return a teardown, which runs on destroy():
sankey.use({
name: 'click-logger',
install: ({ on }) => on('node:click', ({ id }) => console.log(id)),
})
Because on returns its own unsubscribe function, returning it directly is usually all the teardown a plugin needs.
use() returns the instance, so installs chain:
sankey
.use(pathTrace())
.use(myPlugin())
Install runs immediately, so use() after render() applies to the rendered diagram.
Built-in plugins
The three built-ins are named exports, and are also reachable as ApexSankey.plugins.*:
import { ApexSankey, pathTrace, timePlayback, drillDown } from 'apexsankey'
pathTrace
Sends a pulse along the flows connected to a picked node, so a path through a dense diagram is followable.
sankey.use(pathTrace({
trigger: 'hover',
direction: 'both',
color: '#ffffff',
duration: 700,
stagger: 220,
}))
| Option | 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 traversal depth, so the trace cascades outward |
The traversal is cycle-guarded, so a diagram with circular links terminates.
timePlayback
Plays through an ordered series of frames with a play/pause control and a scrubber.
sankey.use(timePlayback({
frames: years.map((y) => ({ nodes, edges: edgesByYear[y], label: String(y) })),
interval: 1400,
autoplay: false,
loop: true,
}))
| Option | Type | Default | Description |
|---|---|---|---|
frames | TimePlaybackFrame[] | required | The ordered frames to play through |
interval | number | 1600 | Milliseconds each frame is shown before advancing |
autoplay | boolean | false | Start playing on install |
loop | boolean | false | Return to the first frame after the last |
controls | boolean | true | Render the built-in control bar |
mount | HTMLElement | just after the chart | Where to mount the control bar |
Each frame is { nodes, edges, label? }, and label defaults to Frame i / n. Frames advance through update(), so each step animates. See Data Updates and Morphing.
drillDown
Collapses groups of nodes into super-nodes that expand on interaction, so a large diagram opens at a summary level.
sankey.use(drillDown({
nodes,
edges,
groups: [
{ id: 'Fossil', title: 'Fossil', children: ['Coal', 'Gas', 'Oil'] },
{ id: 'Renewable', title: 'Renewable', children: ['Solar', 'Wind', 'Hydro'] },
],
expanded: ['Renewable'],
}))
| Option | Type | Default | Description |
|---|---|---|---|
nodes | SankeyGraphNode[] | required | The full, detailed node set before any collapsing |
edges | SankeyGraphEdge[] | required | The full, detailed flows between leaf nodes |
groups | DrillDownGroup[] | required | Group definitions, each collapsing its children into one super-node |
expanded | string[] | [] | Group ids expanded on install; every other group starts collapsed |
A group is { id, title, children, color? }. Its id must be distinct from every leaf-node id, since the super-node takes that id while collapsed.
Expanding and collapsing grows entering nodes out of the super-node and shrinks leaving ones into it, so the change reads as a drilldown rather than a cross-fade.
The pure transform behind it is exposed as ApexSankey.collapseGroups(data, groups, collapsed), which is how you seed a first render that is already collapsed:
sankey.render({
...ApexSankey.collapseGroups({ nodes, edges }, groups, ['Fossil']),
options: sankey.options,
})
Cleanup
destroy() runs every installed plugin's teardown, emits destroyed, drops all handlers, then releases the chart context. It is idempotent. In a framework component, calling destroy() on unmount is enough to clean up plugins and subscriptions together; the React and Vue wrappers already do this.