Living Charts
Live data arrives in steps. A server sends a number every second or so, and the chart has to show it. If you render each value the moment it lands, the chart jumps. If you move towards each value over a few hundred milliseconds, the chart appears to breathe. A living chart is one that never teleports.
Both cards below receive the same value at the same time, every second and a half. The left one draws it immediately. The right one tweens from where it was to where it should be.
Requests per second
800
Requests per second
800
Nothing about the data changed. The right card just spreads each change over 900ms with an ease-out curve, so most of the movement happens early and the number settles gently. A tween that finishes before the next update arrives keeps the chart honest: at every tick, both cards agree.
The number is part of the chart
Notice the counter above the sparkline. A number that flips from 620 to 790 gets read as two facts. A number that counts up gets read as a change, which is what it is. Use tabular-nums so the digits do not shift sideways while they move.
Ranked lists
When live values change order, the reorder is the interesting part. A snapped list makes rows swap places between frames, and the reader loses track of which one is which.
Visits by source
With animation on, each row slides to its new slot and its bar grows or shrinks in place. The eye can follow GitHub overtaking X without reading the labels again.
Usage
The tween is a short hook. It keeps the last displayed values in a ref, and on every new target it eases from those to the new ones with requestAnimationFrame.
import { useEffect, useRef, useState } from "react";
export function useTweened(target: number[], duration = 900) {
const [display, setDisplay] = useState(target);
const current = useRef(target);
useEffect(() => {
const from = current.current;
const start = performance.now();
let frame = 0;
const step = (now: number) => {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
const next = target.map((value, i) => {
const origin = from[i] ?? value;
return origin + (value - origin) * eased;
});
current.current = next;
setDisplay(next);
if (t < 1) frame = requestAnimationFrame(step);
};
frame = requestAnimationFrame(step);
return () => cancelAnimationFrame(frame);
}, [target, duration]);
return display;
}
// const smooth = useTweened(series);
// <path d={toPath(smooth)} />Two things to check. Keep the y axis fixed to a known range while data streams, or the whole chart will rescale every tick and undo the calm you just added. And respect prefers-reduced-motion: when it is set, skip the tween and render the target directly.
Resources
Liveline - Benji Taylor on building a live line chart that moves continuously, and the tricks that make it feel right.
Adaptive Polynomial Curve Fitting - How to smooth streaming values without lagging behind them.
Layout animations in Motion - The layout prop used in the second demo, which animates elements that change position in the DOM.
requestAnimationFrame on MDN - The loop the first demo uses to move numbers a little each frame.