Every series plays an intro the first time it receives data, and the choreography is pluggable: a small function that reads the frame and returns declarative directives, while the engine keeps owning scales, data and drawing. Here we hand-write one — a reveal that opens from the line's own peak outward, a glowing head riding each front — and walk the whole contract.
137.0137.5138.0138.5139.0139.5140.0140.5
07:40:0007:45:00
01 — AN INTRO IS A FUNCTION
The initial-load reveal isn't a preset you pick from — it's a
LineIntroFn the renderer calls once per frame while the intro plays. It receives the frame (linear progress over the whole reveal window, the visible range, plot-area width/height, live timeToX/xToTime/valueToY scales, memoized data accessors) and returns directives — a declarative description of what the frame should look like. The renderer owns the drawing; your function owns the choreography. The shipped styles (unfoldIntro, sweepIntro, traceIntro, plotterIntro, centerOutIntro) are plain factories over this same contract, so a custom intro is their peer — not a second-class hook.type LineIntroFn = (frame: LineIntroFrame) => LineIntroDirectives; interface LineIntroDirectives { clip?: { fromX?: number; toX?: number }; // reveal window, bitmap px ghostAlpha?: number; // faint full-line pre-pass heads?: Array<{ x: number; time: number }>; // glow dots on the front value?: (args: LineIntroValueArgs) => number; // per-value transform}02 — SHAPE THE REVEAL WITH CLIP + HEADS
This demo opens the line from its own peak outward, instead of from a fixed pane-relative point. It walks
frame.layerData(0) once per frame to find the highest value's bitmap X, then computes a clip window centered on that anchor (a smoothstep over progress) — only pixels inside it are drawn — plus two heads. A head is an anchor { x, time }: the renderer interpolates the series value at time, draws the same glowing pulse dot the live stream uses, and bell-fades it over the intro window. Omitted clip sides default to the pane edges, so { toX } alone is a left-to-right sweep — that's the built-in centerOutIntro(), anchored at the fixed center instead of a data-driven point.const peakOut: LineIntroFn = (frame) => { let anchorX = frame.width / 2; let peak = -Infinity; for (const point of frame.layerData(0)) { if (!Number.isFinite(point.value) || point.value <= peak) continue; peak = point.value; anchorX = frame.timeToX(point.time); } const eased = smoothstep(frame.progress); const half = Math.max(anchorX, frame.width - anchorX) * eased; return { clip: { fromX: anchorX - half, toX: anchorX + half }, heads: [ { x: anchorX - half, time: frame.xToTime(anchorX - half) }, { x: anchorX + half, time: frame.xToTime(anchorX + half) }, ], };};03 — OR TRANSFORM THE VALUES
A clip is one kind of reveal; the
value directive is the other. Return a function and every rendered value — stroke, area, stacked cumulative, the overlay pulse — flows through it. The default unfoldIntro() is nothing more than this: each point lerps from its layer's visible mean toward its real value with a soft overshoot, staggered left-to-right by the point's position. Frame accessors like layerMean and primaryPath are memoized per frame, so a directive function stays pure and cheap.// The whole default intro, essentially:return { value: ({ layerIndex, value, position }) => { const mean = frame.layerMean(layerIndex); if (mean === null) return value; const local = clamp01((frame.progress - 0.35 * position) / 0.65); return mean + (value - mean) * easeOutBack(local); },};04 — HAND IT TO THE SERIES
Pass the function to
introAnimation; introMs controls the duration. The reference doesn't need to be memoized — the React wrapper latches the latest function behind a stable identity, so an inline definition costs nothing. (Hoisting a stateful factory to module scope is still a good habit: a factory re-created every render resets its closure state mid-intro.) The intro arms exactly once — on the first empty → non-empty data seed; bulk re-seeds of a live series never replay it, and it's skipped entirely under prefers-reduced-motion. That's why the Replay button remounts the chart: a fresh mount is the same transition a real page load goes through.<LineSeries data={data} options={{ introAnimation: peakOut, introMs: 700 }}/>05 — BARS AND CANDLES, SAME IDEA
Bar and candlestick series expose mirror contracts —
BarIntroFn and CandleIntroFn — with per-element directives instead of a path clip: each element gets an eased, left-to-right staggered progress, and you return how it enters — a value scale (growth from the baseline, stacking-safe) and an element transform (translation, alpha). The shipped styles are factories over the same shapes (growIntro, springIntro, candleUnfoldIntro, wickBodyIntro, and the shared riseIntro/fadeIntro/wipeIntro), so everything in this walkthrough carries over element-wise.const shy: BarIntroFn = () => ({ value: ({ value, progress }) => value * progress, // grow up element: (el) => ({ alpha: 0.3 + 0.7 * el.progress }), // …fading in}); <BarSeries data={data} options={{ introAnimation: shy }} />