chart-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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).
  • 条形图竞赛/随时间变化的排名(核心使用场景)。
  • 动画折线/面积图展示、动画计数器与数字滚动器。
  • 基于同一图表设计,为多数据集渲染视频(模板 × CSV → N个视频)。

The one rule that prevents 90% of bugs

避免90%错误的黄金准则

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.
js
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.
**所有数值均由当前帧驱动——绝不要依赖 wall-clock time(实时时钟)或库的内部动画循环。**视频帧是确定性渲染的;如果条形图高度来自CSS过渡或D3/GSAP/Chart.js动画,会因渲染器与动画时钟不一致而出现闪烁或不同步。需将显示值计算为帧的纯函数。
js
import { useCurrentFrame, interpolate } from "remotion";
const frame = useCurrentFrame();
const value = interpolate(frame, [0, 60], [0, 1287], { extrapolateRight: "clamp" });
在Remotion中,禁用所有第三方图表动画(Chart.js中设置
animation: false
,不使用D3的
.transition()
),让
useCurrentFrame()
成为唯一的时钟。D3在比例尺和图形绘制(
scaleLinear
scaleBand
line()
)方面仍然表现出色——只是不要使用它的计时器。

Map data → pixels

数据→像素映射

A chart is two functions: a value scale and a frame interpolator. Keep them separate.
LayerToolJob
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
js
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" }));
图表由两个函数构成:数值比例尺和帧插值器。请将二者分开处理。
层级工具作用
数值比例尺
scaleLinear
/
scaleBand
数据单位 → 像素(高度、X轴位置)
帧插值器
interpolate
/
spring
帧 → 缓动进度 0→1
显示值
round
+
Intl.NumberFormat
进度 → 人类可读的数字
js
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.
js
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.
对数值的缓动处理方式应与运动缓动一致。对于数字递增或条形图增长效果,
easeOutCubic
会给人一种"稳定在某个数值"的感觉。对于排名变化,优先使用
spring
,这样超越其他条形图的元素会带有一定的重量感。
js
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;
除非是稳定的时间轴(如时钟、年份计数器),否则绝不要对观众读取的数值进行线性插值——标题数字的线性运动会显得机械生硬。

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.
js
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.
对底层数值进行插值,然后在渲染时格式化。必须做到两点:格式化前先取整(避免出现
1287.4013
这类数值),并且固定数字框,防止数字变化时布局跳动。
js
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;  → 避免宽度抖动,防止内容被截断
对于货币/百分比,使用
Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
。等宽数字会让每个数字保持相同宽度,避免数字出现晃动。

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.
js
// 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.
这是标志性的效果。真实数据集通常是稀疏的(按年/月划分的行);流畅的竞赛效果需要数据行之间的插值关键帧,以及条形图滑动到新排名的动画。Y轴位置来自插值后的排名,这样超越动画会是平滑滑动而非跳跃。
js
// frame → 稀疏行之间的连续时间,然后根据插值后的数值对每个项目进行排名
const t = frame / fps;                  // 秒
const i = Math.min(Math.floor(t), rows.length - 2);
const f = easeInOut(t - i);             // 当前行对之间的0→1
const vals = items.map((it) => ({
  it,
  v: rows[i][it] + (rows[i + 1][it] - rows[i][it]) * f, // 插值后的数值
}));
vals.sort((a, b) => b.v - a.v);         // 当前帧的排名
vals.forEach((d, rank) => { d.y = rank * (barH + gap); }); // 来自插值排名的Y轴位置
为每个项目使用稳定的key(让React在重排时跟踪每个条形图),基于每帧数值对
y
width
进行动画,并显示移动的年份/日期标签。完整可运行的Remotion组件请参考
references/bar-chart-race.md

Pacing data over time

数据展示节奏把控

Animation that's too fast is unreadable. Budget time per data event, not per second of polish.
BeatBudgetWhy
Intro / first framehold 1–1.5slet the viewer read axes + units before motion
Per data row (race)0.3–0.6sfast enough to feel alive, slow enough to track the leader
A highlighted moment+0.5–1s slow-downdwell on the crossover / record / inflection
Final statehold 2–3sthe 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.
动画过快会导致无法读取。应为每个数据事件分配时间,而非按秒来打磨。
环节时间预算原因
开场/第一帧停留1–1.5秒让观众在动画开始前读取坐标轴和单位
每条数据行(竞赛)0.3–0.6秒足够生动,同时让观众能够跟踪领先者
高亮时刻额外放慢0.5–1秒在交叉点/记录点/拐点处停留
最终状态停留2–3秒核心信息需要清晰传达,且方便截图
逐步展示——先展示一个系列,再展示下一个——而非一次性展示所有数据;观众一次只能跟踪一个运动元素。

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.
jsx
// Remotion: data is a prop; same composition, different CSV → different video
export const Race = ({ data }) => { /* …reads `data`, hardcodes nothing… */ };
bash
undefined
代码驱动图表的优势:一套设计,适配多数据集。将数据集设为输入属性,而非硬编码常量,然后为每个文件渲染一次。
jsx
// Remotion: data是属性;相同的合成,不同的CSV → 不同的视频
export const Race = ({ data }) => { /* …读取`data`,无硬编码内容… */ };
bash
undefined

render the same template for every dataset in /data

为/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.
for f in data/*.json; do name=$(basename "$f" .json) npx remotion render Race "out/$name.mp4" --props="$f" done

将颜色/字体/布局放在单个主题对象中,确保50个视频保持品牌一致性,仅数值变化。CSV→属性的解析、验证以及完整的批量脚本请参考`references/data-pipeline.md`。

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.
  • 所有动画数值均为
    useCurrentFrame()
    的纯函数;无库计时器。
  • 数字已取整并格式化(
    Intl.NumberFormat
    ),数字使用
    tabular-nums
  • 竞赛条形图使用稳定的key;排名变化通过插值排名实现平滑滑动。
  • 坐标轴/单位标注一次;数值标签随数值动画。
  • 开场停留、每条数据行节奏可读、最终状态停留≥2秒。
  • 数据集为输入属性——一套模板可渲染文件夹中的所有CSV。

Deliver & verify (rendered stills → MP4)

交付与验证(渲染静帧→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.
bash
undefined
打包助手
scripts/
):使用
scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png
拼接静帧,然后使用
scripts/probe-mp4.sh out.mp4 [WxH] [fps]
验证编码。详情请见
scripts/README.md
Remotion是帧确定性的——每个条形图高度、计数器数值和排名均为
useCurrentFrame()
的纯函数,因此无需搜索工具即可无头渲染任意精确帧。交付物是包含精确数值的MP4,因此静帧检查必不可少:1像素的布局错误可以接受,但数字错误绝对不行。
输出约定:
  • 一个Remotion项目,其中已注册图表(
    <Composition>
    + zod
    schema
    +
    defaultProps
    ),所有运动均由帧驱动(无计时器/
    Date.now()
    /
    Math.random()
    ,无Chart.js/D3
    .transition()
    时钟)。
  • 交付物 = 渲染后的
    out/*.mp4
    (加上项目文件,方便用户使用新数据集重新渲染)。
  • 依赖数据的时长(N行 × 每行帧数)?在
    calculateMetadata
    中计算,而非手动设置。
**验证流程——渲染静帧→检查→编码。**先渲染单帧(成本低,无需视频编码),确认数值和布局正确后再进行编码。
bash
undefined

1. Frame-exact stills at start / mid / end (PNG, headless, fast) — with the SHIPPED props

1. 精确渲染开场/中间/结尾的静帧(PNG,无头模式,速度快)——使用最终交付的属性

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
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":[...]}' # 最后一帧 = durationInFrames - 1

2. Inspect each PNG — FIDELITY (axis labels, value labels, year/date, ranks, units all EXACT —

2. 检查每个PNG——准确性(坐标轴标签、数值标签、年份/日期、排名、单位均精确——

round + tabular-nums working, no 1287.4013) AND artifacts (bar overflow, off-canvas label,

取整和等宽数字生效,无1287.4013这类数值)以及瑕疵(条形图溢出、标签超出画布、

clipped safe-area, missing font, wrong data binding / wrong column mapped).

安全区域被截断、字体缺失、数据绑定错误/映射错误列)。

3. Only after the stills check out, encode:

3. 仅在静帧检查通过后进行编码:

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.
npx remotion render Race out/race.mp4 --props='{"data":[...]}'

- 使用`npx remotion compositions`查看每个图表的`durationInFrames`/`fps`,并选择最后一帧。
- **数据驱动/批量处理(核心场景)**:批量渲染所有N个文件前,先通过静帧验证一个代表性数据集——提前发现高度比例尺或列映射错误,好过在50个MP4中逐个查找。
- **免费生成README演示GIF**:`npx remotion render Race out/demo.gif --codec=gif`。

**完成前检查:**
1. `npx remotion still`可干净渲染第0帧、中间帧和最后一帧——无错误,无缺失字体/资源。
2. 所有数字均**精确**(已取整、使用`Intl.NumberFormat`、`tabular-nums`),且在每帧都位于安全区域内。
3. 仅由帧驱动——无`Date.now()`/`Math.random()`/库计时器(在CI中保持确定性)。
4. **最终交付**的数据集属性可正确渲染(不仅是`defaultProps`)——映射正确列、比例尺正确。
5. 完整MP4已编码并可播放;(可选)已为README渲染GIF。

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.
  • references/bar-chart-race.md
    ——完整可运行的Remotion条形图竞赛组件:稀疏行关键帧插值、每帧排名、平滑Y轴位置、动画数值标签,以及D3比例尺坐标轴。还包含原生canvas版本。
  • references/data-pipeline.md
    ——CSV/JSON→类型化属性的解析与验证、主题对象模式、动画计数器/折线展示方案,以及模板×数据批量渲染N个视频的脚本。