Data Updates and Morphing
update(data) transitions a rendered diagram to a new dataset instead of redrawing it. Which animation you get depends on how much changed.
Two paths
const sankey = new ApexSankey(document.getElementById('chart'), options)
sankey.render({ nodes, edges: edges2024, options: sankey.options })
// later, on the same instance
sankey.update({ nodes, edges: edges2025, options: sankey.options })
| Change | Animation |
|---|---|
| Same topology, different values or positions | Spring relayout. Nodes and ribbons spring to their new places |
| Topology differs | Grow-in morph. Entering flows unfurl out of their source node, survivors slide, removed flows retract and dissolve |
Either way the diagram at rest is identical to what render() would have produced. The animation is presentation, not state.
When it redraws instantly
update() falls back to an instant redraw when:
animation.enabledisfalse- the user's system requests reduced motion
Both are checked per call, so a diagram that respects prefers-reduced-motion needs no special handling: pass the new data and the right thing happens.
Node identity
The diff is keyed on node id and on the (source, target, type) triple for flows. Ids that change between payloads make everything look new, which produces a full morph where you wanted a value tween.
// stable: the same node across every payload
{ id: 'grid', title: 'National grid' }
// unstable: a title change silently becomes a different node
{ id: slugify(node.title), title: node.title }
Derive ids from something durable in your domain. A node keeping its id can be retitled, recolored and re-ranked and it will still animate rather than blink.
The same applies to flows: give each flow a stable type, since two flows between the same pair with different type values are distinct.
Polling and streaming
For a live diagram, render once and update on each tick:
const sankey = new ApexSankey(el, { theme: 'midnight' })
sankey.render({ nodes, edges: await fetchFlows(), options: sankey.options })
setInterval(async () => {
sankey.update({ nodes, edges: await fetchFlows(), options: sankey.options })
}, 5000)
Leave enough time between updates for the motion to read. An interval shorter than the animation makes a diagram that never settles; pair a fast feed with a slower render cadence rather than updating on every message.
Stepping through frames
Because update() interpolates, stepping through an ordered series of snapshots reads as a single continuous animation. For a scrubber or an autoplaying timeline, the timePlayback plugin already implements the control bar, the interval and the looping:
import { ApexSankey, timePlayback } from 'apexsankey'
sankey.use(timePlayback({
frames: years.map((year) => ({
nodes,
edges: edgesByYear[year],
label: String(year),
})),
interval: 1400,
loop: true,
}))
See Plugins and Events.
Knowing when a transition settles
The rendered event fires after the initial render and after each update() settles, which is the hook for anything that must wait for final geometry:
sankey.on('rendered', () => {
enableExportButton()
})
Comparing rather than animating
An animation shows that something changed; it is poor at showing exactly what. When the difference is the point, render both states side by side with a structural diff instead:
ApexSankey.compare(el, {
before: { nodes, edges: edges2024, title: '2024' },
after: { nodes, edges: edges2025, title: '2025' },
})