@iart-ai/chart-animation

This skill should be used when the user asks to "make a bar chart race", "turn a CSV into a video", "build an animated data visualization", "create an animated statistics video", "animate a chart/graph", "make a number counter/ticker", "animate a ranking over time", or "batch-render one chart template across many datasets". Covers data-driven keyframing, value interpolation, rank transitions, counters, pacing, annotation, and template×data batch output.

View in AI SkillSafe app
Scanned · no findings
0 downloads
0 stars
0 demos
SKILL.md
namechart-animation
descriptionThis skill should be used when the user asks to "make a bar chart race", "turn a CSV into a video", "build an animated data visualization", "create an animated statistics video", "animate a chart/graph", "make a number counter/ticker", "animate a ranking over time", or "batch-render one chart template across many datasets". Covers data-driven keyframing, value interpolation, rank transitions, counters, pacing, annotation, and template×data batch output.
version0.1.0

Data Video

Turn a dataset into a chart that moves with intent: a bar chart race, a growing line, a count-up stat. The craft is mapping numbers to pixels every frame, easing the value (not just the opacity), and pacing the data so a viewer can actually read it.

When to use

  • Bar chart race / ranking-over-time (the headline use case).
  • Animated line/area reveal, animated counters and number tickers.
  • One chart design rendered across many datasets (template × CSV → N videos).

The one rule that prevents 90% of bugs

Drive every value from the current frame — never from wall-clock time or a library's internal animation loop. A video frame is rendered deterministically; if a bar's height comes from a CSS transition or a D3/GSAP/Chart.js animation, it flickers or desyncs because the renderer and the animation clock disagree. Compute the displayed value as a pure function of frame.

import { useCurrentFrame, interpolate } from "remotion";
const frame = useCurrentFrame();
const value = interpolate(frame, [0, 60], [0, 1287], { extrapolateRight: "clamp" });

In Remotion, disable all third-party chart animations (animation: false in Chart.js, no D3 .transition()) and let useCurrentFrame() be the only clock. D3 is still great for scales and shapes (scaleLinear, scaleBand, line()) — just not its timers.

Map data → pixels

A chart is two functions: a value scale and a frame interpolator. Keep them separate.

Layer Tool Job
Value scale scaleLinear / scaleBand data units → px (height, x-position)
Frame interpolation interpolate / spring frame → eased progress 0→1
Display value round + Intl.NumberFormat progress → the number a human reads
import { scaleLinear } from "d3-scale";
const yScale = scaleLinear().domain([0, maxValue]).range([0, chartHeight]);
const barHeight = yScale(interpolate(frame, [0, 45], [0, datum.value], { extrapolateRight: "clamp" }));

Easing value changes

Ease the value the same way you'd ease motion. For a count-up or a bar growing in, easeOutCubic reads as "settling on a number." For rank changes, prefer spring so a bar that overtakes another has a little weight.

const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
const t = easeOutCubic(interpolate(frame, [start, end], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }));
const shown = from + (to - from) * t;

Never linearly interpolate a value the viewer is reading unless it's a steady time axis (a clock, a year counter) — linear motion on a headline number feels robotic.

Counters / number tickers

Interpolate the underlying number, then format on render. Two musts: round before formatting (no 1287.4013), and fix the digit box so the layout doesn't jump as digits change.

const n = Math.round(interpolate(frame, [0, 50], [0, 1287], { extrapolateRight: "clamp" }));
const label = new Intl.NumberFormat("en-US").format(n); // "1,287"
// CSS: font-variant-numeric: tabular-nums; line-height: 1;  → no width jitter, no clipping

For currency/percent use Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }). Tabular figures keep each digit the same width so the number doesn't wiggle.

Bar chart race — rank transitions

The signature move. Real datasets are sparse (yearly/monthly rows); a smooth race needs interpolated keyframes between data rows plus bars that slide to new ranks. The y-position comes from a rank that is itself interpolated, so an overtake animates as a glide, not a jump.

// frame → continuous time between sparse rows, then rank each item by its interpolated value
const t = frame / fps;                  // seconds
const i = Math.min(Math.floor(t), rows.length - 2);
const f = easeInOut(t - i);             // 0→1 within the current row pair
const vals = items.map((it) => ({
  it,
  v: rows[i][it] + (rows[i + 1][it] - rows[i][it]) * f, // interpolated value
}));
vals.sort((a, b) => b.v - a.v);         // rank this frame
vals.forEach((d, rank) => { d.y = rank * (barH + gap); }); // y from interpolated rank

Render with stable keys per item (so React tracks each bar across reorders), animate y and width off the per-frame value, and show a moving year/date label. See references/bar-chart-race.md for a complete, runnable Remotion component.

Pacing data over time

Animation that's too fast is unreadable. Budget time per data event, not per second of polish.

Beat Budget Why
Intro / first frame hold 1–1.5s let the viewer read axes + units before motion
Per data row (race) 0.3–0.6s fast enough to feel alive, slow enough to track the leader
A highlighted moment +0.5–1s slow-down dwell on the crossover / record / inflection
Final state hold 2–3s the takeaway needs to land and be screenshot-able

Reveal progressively — one series, then the next — instead of all data at once; viewers can only track one moving thing at a time.

Labeling & annotation

Numbers without context are noise. Always render: an always-visible value label on each bar/point (it animates with the value), a time indicator (year/date), and axis units once. Add a callout only at the moment it matters (a crossover, a peak) — annotate the inflection, then let it fade.

Template × data — batch output

The payoff of code-driven charts: one design, many datasets. Make the dataset an input prop, never a hardcoded constant, then render once per file.

// Remotion: data is a prop; same composition, different CSV → different video
export const Race = ({ data }) => { /* …reads `data`, hardcodes nothing… */ };
# render the same template for every dataset in /data
for f in data/*.json; do
  name=$(basename "$f" .json)
  npx remotion render Race "out/$name.mp4" --props="$f"
done

Keep colors/fonts/layout in a single theme object so 50 videos stay brand-consistent and only the numbers change. See references/data-pipeline.md for CSV→props parsing, validation, and the full batch script.

Output checklist

  • Every animated value is a pure function of useCurrentFrame(); no library timers.
  • Numbers are rounded and formatted (Intl.NumberFormat), digits use tabular-nums.
  • Race bars carry stable keys; rank changes glide via interpolated rank.
  • Axes/units labeled once; value labels animate with their value.
  • Intro hold, readable per-row pace, final hold ≥2s.
  • Dataset is an input prop — one template renders every CSV in the folder.

Deliver & verify (rendered stills → MP4)

Packaged helper (scripts/): tile your stills with scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, then assert the encode with scripts/probe-mp4.sh out.mp4 [WxH] [fps]. See scripts/README.md.

Remotion is frame-deterministic — every bar height, counter value, and rank is a pure function of useCurrentFrame(), so you can render any exact frame headlessly with no seek harness. The deliverable is an MP4 carrying exact numbers, so still-inspection is non-negotiable here: a one-pixel layout bug is forgivable, a wrong digit is not.

Output contract:

  • A Remotion project with the chart registered (<Composition> + zod schema + defaultProps), all motion frame-driven (no timers / Date.now() / Math.random(), no Chart.js/D3 .transition() clocks).
  • Deliverable = the rendered out/*.mp4 (plus the project, so the user can re-render with new datasets).
  • Data-dependent duration (N rows × frames/row)? compute it in calculateMetadata, not by hand.

Verify loop — render stills → inspect → encode. Render single frames first (cheap, no video encode), then encode only once the numbers and layout are right.

# 1. Frame-exact stills at start / mid / end (PNG, headless, fast) — with the SHIPPED props
npx remotion still Race out/f-start.png --frame=0   --props='{"data":[...]}'
npx remotion still Race out/f-mid.png   --frame=90  --props='{"data":[...]}'
npx remotion still Race out/f-end.png   --frame=149 --props='{"data":[...]}'  # last = durationInFrames - 1

# 2. Inspect each PNG — FIDELITY (axis labels, value labels, year/date, ranks, units all EXACT —
#    round + tabular-nums working, no 1287.4013) AND artifacts (bar overflow, off-canvas label,
#    clipped safe-area, missing font, wrong data binding / wrong column mapped).

# 3. Only after the stills check out, encode:
npx remotion render Race out/race.mp4 --props='{"data":[...]}'
  • Use npx remotion compositions to read each chart's durationInFrames/fps and pick the end frame.
  • Data-driven / batch (the headline case): verify ONE representative dataset via stills before batch-rendering all N files — a height-scale or column-mapping bug caught once beats finding it in 50 MP4s.
  • README demo GIF for free: npx remotion render Race out/demo.gif --codec=gif.

Before you finish:

  1. npx remotion still renders cleanly at frame 0, mid, and last — no errors, no missing fonts/assets.
  2. Every number is exact (rounded, Intl.NumberFormat, tabular-nums) and inside the safe area at each frame.
  3. Frame-driven only — no Date.now() / Math.random() / library timers (determinism holds in CI).
  4. The shipped dataset props render correctly (not just defaultProps) — right column mapped, right scale.
  5. Full MP4 encoded and plays; (optional) GIF rendered for the README.

Reference files

  • references/bar-chart-race.md — a complete runnable Remotion bar-chart-race component: sparse-row keyframe interpolation, per-frame ranking, gliding y-positions, animated value labels, and a D3-scale axis. Plus a vanilla canvas variant.
  • references/data-pipeline.md — CSV/JSON → typed props parsing and validation, the theme object pattern, animated counters/line-reveal recipes, and the template×data batch render script for N videos.

Embed badges

Add these to your README to show the skill's verification status.

SkillSafe verified badge
Verified badge
[![SkillSafe verified badge](https://api.skillsafe.ai/v1/badge/@iart-ai/chart-animation/verified)](https://skillsafe.ai/skill/@iart-ai/chart-animation/)
Installs badge
Installs badge
[![Installs badge](https://api.skillsafe.ai/v1/badge/@iart-ai/chart-animation/installs)](https://skillsafe.ai/skill/@iart-ai/chart-animation/)
Scan badge
Scan badge
[![Scan badge](https://api.skillsafe.ai/v1/badge/@iart-ai/chart-animation/scan)](https://skillsafe.ai/skill/@iart-ai/chart-animation/)
Eval pass rate badge
Eval pass rate
[![Eval pass rate badge](https://api.skillsafe.ai/v1/badge/@iart-ai/chart-animation/eval)](https://skillsafe.ai/skill/@iart-ai/chart-animation/)