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 })
ChangeAnimation
Same topology, different values or positionsSpring relayout. Nodes and ribbons spring to their new places
Topology differsGrow-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.enabled is false
  • 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' },
})

See Comparison Split-view.