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)
EventPayloadFires
node:clickSankeyNodeEventArgsA node is clicked
node:mouseenterSankeyNodeEventArgsPointer enters a node
node:mouseleaveSankeyNodeEventArgsPointer leaves a node
edge:clickSankeyEdgeEventArgsA flow is clicked
edge:mouseenterSankeyEdgeEventArgsPointer enters a flow
edge:mouseleaveSankeyEdgeEventArgsPointer leaves a flow
renderednoneAfter the initial render, and after each update() settles
destroyednoneDuring 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,
}))
OptionTypeDefaultDescription
trigger'click' | 'hover''click'What starts a trace
direction'downstream' | 'upstream' | 'both''downstream'Which way flow is traced from the picked node
colorstring'#ffffff'Pulse color
durationnumber700Milliseconds for a pulse to cross one ribbon
staggernumber220Milliseconds 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,
}))
OptionTypeDefaultDescription
framesTimePlaybackFrame[]requiredThe ordered frames to play through
intervalnumber1600Milliseconds each frame is shown before advancing
autoplaybooleanfalseStart playing on install
loopbooleanfalseReturn to the first frame after the last
controlsbooleantrueRender the built-in control bar
mountHTMLElementjust after the chartWhere 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'],
}))
OptionTypeDefaultDescription
nodesSankeyGraphNode[]requiredThe full, detailed node set before any collapsing
edgesSankeyGraphEdge[]requiredThe full, detailed flows between leaf nodes
groupsDrillDownGroup[]requiredGroup definitions, each collapsing its children into one super-node
expandedstring[][]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.