Living Charts
Live data arrives in steps. A server sends a number every second or so, and if the chart draws each one the moment it lands, the whole line jumps.
A living chart never teleports. Each new value eases in over a few hundred milliseconds, so the chart moves the way the data feels like it should.
Requests per second
800
Requests per second
800
Both cards get the same value every 1.5 seconds. The left one redraws on
the spot: every point hops one slot to the left and the counter flips to a
new number. The right one spends 900ms on the same change with an
ease-out curve. The line slides left, the new point comes in from the
right edge, and the dot and the counter ride up or down the new segment
with it.
Animate the change, not the data. The tween finishes well before the next value arrives, so at every update both cards agree.
The number is part of the chart
A counter that flips from 620 to 790 reads as two separate facts. One that
counts up reads as a change, which is what it is. Use tabular-nums so the
digits don't shift sideways while they count.
Ranked lists
When live values change order, the reorder is the interesting part. A snapped list swaps rows between frames, and you have to read the labels again to work out what moved.
Visits by source
With animation on, each row slides to its new place over 600ms while its
bar and number ease to the new value. You can follow a single source as it
climbs or falls without reading anything.
Usage
The hook eases a progress value from 0 to 1 every time a new point arrives. Everything else is derived from it.
import { useEffect, useState } from "react";
// Eases from 0 to 1 over `duration` ms every time `key` changes.
export function useProgress(key: number, duration = 900) {
const [state, setState] = useState({ key, t: 1 });
useEffect(() => {
const start = performance.now();
let frame = 0;
const step = (now: number) => {
const p = Math.min(1, (now - start) / duration);
setState({ key, t: 1 - (1 - p) ** 3 }); // ease-out
if (p < 1) frame = requestAnimationFrame(step);
};
frame = requestAnimationFrame(step);
return () => cancelAnimationFrame(frame);
}, [key, duration]);
// A new key starts at 0 before the first frame runs.
return state.key === key ? state.t : 0;
}
// values keeps one extra point past the left edge, clipped away.
const t = useProgress(tick);
const x = (i: number) => (i - t) * slot;
const head = previous + (latest - previous) * t;Keep the y axis fixed to a known range while data streams in, or the chart
rescales on every update and undoes the calm. When prefers-reduced-motion
is set, return 1 from the hook and the chart steps like the left card.
Resources
Liveline - A React component for real-time line charts that eases toward every new value instead of jumping.
Layout animations in Motion - The layout prop the ranked list uses to slide rows into their new places.
requestAnimationFrame - The per-frame loop that drives the tween in the first demo.
prefers-reduced-motion - The media query for people who would rather the chart stepped than moved.