chart-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseData 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 ( in Chart.js, no D3 ) and let be the only clock. D3 is still great for scales and shapes (, , ) — just not its timers.
animation: false.transition()useCurrentFrame()scaleLinearscaleBandline()**所有数值均由当前帧驱动——绝不要依赖 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中设置,不使用D3的),让成为唯一的时钟。D3在比例尺和图形绘制(、、)方面仍然表现出色——只是不要使用它的计时器。
animation: false.transition()useCurrentFrame()scaleLinearscaleBandline()Map data → pixels
数据→像素映射
A chart is two functions: a value scale and a frame interpolator. Keep them separate.
| Layer | Tool | Job |
|---|---|---|
| Value scale | | data units → px (height, x-position) |
| Frame interpolation | | frame → eased progress 0→1 |
| Display value | | 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" }));图表由两个函数构成:数值比例尺和帧插值器。请将二者分开处理。
| 层级 | 工具 | 作用 |
|---|---|---|
| 数值比例尺 | | 数据单位 → 像素(高度、X轴位置) |
| 帧插值器 | | 帧 → 缓动进度 0→1 |
| 显示值 | | 进度 → 人类可读的数字 |
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, reads as "settling on a number." For rank changes, prefer so a bar that overtakes another has a little weight.
easeOutCubicspringjs
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.
对数值的缓动处理方式应与运动缓动一致。对于数字递增或条形图增长效果,会给人一种"稳定在某个数值"的感觉。对于排名变化,优先使用,这样超越其他条形图的元素会带有一定的重量感。
easeOutCubicspringjs
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 ), and fix the digit box so the layout doesn't jump as digits change.
1287.4013js
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 clippingFor currency/percent use . Tabular figures keep each digit the same width so the number doesn't wiggle.
Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })对底层数值进行插值,然后在渲染时格式化。必须做到两点:格式化前先取整(避免出现这类数值),并且固定数字框,防止数字变化时布局跳动。
1287.4013js
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 rankRender with stable keys per item (so React tracks each bar across reorders), animate and off the per-frame value, and show a moving year/date label. See for a complete, runnable Remotion component.
ywidthreferences/bar-chart-race.md这是标志性的效果。真实数据集通常是稀疏的(按年/月划分的行);流畅的竞赛效果需要数据行之间的插值关键帧,以及条形图滑动到新排名的动画。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在重排时跟踪每个条形图),基于每帧数值对和进行动画,并显示移动的年份/日期标签。完整可运行的Remotion组件请参考。
ywidthreferences/bar-chart-race.mdPacing 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.
动画过快会导致无法读取。应为每个数据事件分配时间,而非按秒来打磨。
| 环节 | 时间预算 | 原因 |
|---|---|---|
| 开场/第一帧 | 停留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
undefinedrender 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 ; no library timers.
useCurrentFrame() - Numbers are rounded and formatted (), digits use
Intl.NumberFormat.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 (): tile your stills withscripts/, then assert the encode withscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png. Seescripts/probe-mp4.sh out.mp4 [WxH] [fps].scripts/README.md
Remotion is frame-deterministic — every bar height, counter value, and rank is a pure function of , 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.
useCurrentFrame()Output contract:
- A Remotion project with the chart registered (+ zod
<Composition>+schema), all motion frame-driven (no timers /defaultProps/Date.now(), no Chart.js/D3Math.random()clocks)..transition() - Deliverable = the rendered (plus the project, so the user can re-render with new datasets).
out/*.mp4 - Data-dependent duration (N rows × frames/row)? compute it in , not by hand.
calculateMetadata
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是帧确定性的——每个条形图高度、计数器数值和排名均为的纯函数,因此无需搜索工具即可无头渲染任意精确帧。交付物是包含精确数值的MP4,因此静帧检查必不可少:1像素的布局错误可以接受,但数字错误绝对不行。
useCurrentFrame()输出约定:
- 一个Remotion项目,其中已注册图表(+ zod
<Composition>+schema),所有运动均由帧驱动(无计时器/defaultProps/Date.now(),无Chart.js/D3Math.random()时钟)。.transition() - 交付物 = 渲染后的(加上项目文件,方便用户使用新数据集重新渲染)。
out/*.mp4 - 依赖数据的时长(N行 × 每行帧数)?在中计算,而非手动设置。
calculateMetadata
**验证流程——渲染静帧→检查→编码。**先渲染单帧(成本低,无需视频编码),确认数值和布局正确后再进行编码。
bash
undefined1. 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
参考文件
- — 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/bar-chart-race.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/data-pipeline.md
- ——完整可运行的Remotion条形图竞赛组件:稀疏行关键帧插值、每帧排名、平滑Y轴位置、动画数值标签,以及D3比例尺坐标轴。还包含原生canvas版本。
references/bar-chart-race.md - ——CSV/JSON→类型化属性的解析与验证、主题对象模式、动画计数器/折线展示方案,以及模板×数据批量渲染N个视频的脚本。
references/data-pipeline.md