Definition identity is the application update boundary. A framework memo captures the current data and options; Charts rebuilds the scene when that definition changes or when the chart surface changes size.
import { scaleBand } from '@tanstack/charts-scales/band'
import { scaleLinear } from '@tanstack/charts-scales/linear'
function RankingChart({ rows, metric, accent }: Props) {
const definition = useMemo(() => {
const ranked = rankRows(rows, metric)
return defineChart({
animate: { duration: 280, easing: 'ease-out' },
chart: ({ width }) => ({
marks: [
barX(ranked, {
x: 'value',
y: 'label',
fill: accent,
}),
],
x: {
scale: scaleLinear,
nice: true,
axis: { ticks: { count: width < 420 ? 4 : 7 } },
},
y: {
scale: () => scaleBand<string>().padding(0.1),
},
}),
})
}, [rows, metric, accent])
return <Chart definition={definition} ariaLabel="Revenue ranking" />
}Use the framework's native equivalent: computed, createMemo, $derived, Angular computed, or Octane useMemo.
host.update({
...options,
definition: createRankingDefinition(nextRows, nextMetric, nextAccent),
})Built-in marks first use an explicit key, then a unique datum.id, then a unique nested datum.data.id. Marks can also infer identity from semantic positional channels:
barX(rows, {
x: 'value',
y: 'label',
})Here barX uses the unique y value when rows have no id. barY uses x; lineY and areaY use x; areaX uses y; rects and cells use their x/y interval tuple. Dots and text try x, then y, then their x/y tuple. Inference is scoped to each group and falls back to array position when a candidate is incomplete or duplicated. Development builds warn once for each affected mark instance.
Supply key when the inferred value is not the entity's identity or can change independently of it. Stable identity preserves surviving SVG elements, focused points, and transition continuity.
animate accepts true or:
Numeric geometry and compatible path data interpolate. Entering and exiting nodes reconcile by key. If an update interrupts a transition, it begins from the geometry currently painted on screen.
Static SVG, server rendering, and createChartScene do not include animation.
Use this path when a small default-SVG tween is enough. It has no spring solver, definition-local timing cascade, entrance choreography, or retained velocity.
Use motion() when animation quality is part of the chart contract:
import { scaleUtc } from 'd3-scale'
import { motion } from '@tanstack/charts/motion'
import { mountChartRenderer } from '@tanstack/charts/renderer'
import { scaleLinear } from '@tanstack/charts-scales/linear'
const definition = defineChart({
motion: {
transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 },
},
marks: [
lineY(rows, {
x: 'date',
y: 'actual',
key: 'id',
}),
lineY(rows, {
x: 'date',
y: 'forecast',
key: 'id',
motion: { transition: { type: 'spring', mass: 1.25 } },
}),
],
x: { scale: scaleUtc },
y: { scale: scaleLinear },
})
const host = mountChartRenderer(container, {
definition,
renderer: motion(),
width: 640,
height: 360,
ariaLabel: 'Actual and forecast revenue',
})Each host has one animation owner. The default SVG renderer uses animate. When motion() is the renderer, it ignores animate and uses motion declarations from the definition. A motion declaration configures the motion renderer; it does not select it.
Motion declarations can live on the chart, a mark, an axis, its ticks, tick labels, or axis label. A callback can specialize enter, update, and exit by series or datum:
barY(rows, {
x: 'month',
y: 'revenue',
key: 'id',
motion(context) {
if (context.phase === 'enter') {
return { delay: context.datumIndex * 35 }
}
if (context.datum?.status === 'forecast') {
return { transition: { type: 'tween', duration: 240 } }
}
},
})Same-type partial transitions inherit omitted properties. Springs use physical stiffness, damping, and mass; duration is not a spring parameter. Rapid retargeting preserves the currently painted value and velocity. Keyed interaction points move with the presentation geometry rather than jumping to the next scene.
A crosshair is also keyed focus presentation. With the motion renderer, its rules, bands, labels, and marker retain DOM identity and spring velocity while focus retargets. Default SVG, Canvas, and native surfaces paint the same guide at its current target without importing the browser motion runtime.
Spring updates always retarget immediately; a returned update delay is ignored so incoming momentum cannot freeze. Use delays for spring enter/exit choreography or any tween phase.
The renderer grows entering bars from their semantic baseline, reveals entering line groups, staggers bar entrances, morphs compatible numeric SVG geometry, and fades incompatible keyed topology. Authored delay replaces automatic staggering for that target.
motion() respects reduced motion and does not animate resize-only updates by default. It adopts server-rendered SVG without replaying entrance motion. Static SVG and Canvas accept the same definitions but paint the final state.
See the Motion reference for the complete cascade, types, focus-state transitions, compatibility limits, and standalone spring sampler.
For high-rate data:
For a scrolling trace, keep enough overscan before the visible x-domain to cover the largest expected update batch, enable clip, and use a rolling path contract:
const definition = defineChart({
motion: {
path: {
update: 'rolling',
x: 'shift',
y: 'reproject',
fallback: 'snap',
},
transition: { type: 'tween', duration: sampleInterval, easing: 'linear' },
},
marks,
})The keyed retained window moves as one affine path. y: 'reproject' keeps that motion valid while a continuous y-domain changes. A failed rolling invariant snaps instead of occasionally becoming a different interpolation. Set fallback: 'morph' only when path interpolation is intentional. A valid update that arrives during another roll composes from the transform currently painted on screen.
Keep viewport.translate at zero on both the previous and target scene during a rolling update. A nonzero transient viewport translation makes the rolling contract fail and uses its configured fallback. Commit the viewport domain and reset the translation before applying the next live-data window. Keep plot margins fixed and prefer linear segments so appending a sample cannot recompute a visible curve tangent.
The final definition passed to host.update is applied synchronously.