1实现原理 · 为什么它能做到
防 90% bug 的铁律:每个显示值由当前帧推导,禁止挂钟时间与库内部动画循环。
**Drive every value from the current frame — never from wall-clock time or a library's internal animation loop.**
把图表拆成三层可分离函数:值域映射、帧插值、显示值格式化。
| Value scale | `scaleLinear` / `scaleBand` | data units → px (height, x-position) |
bar chart race 的两个难点:稀疏数据要插值、名次要滑动(y 由插值名次推出)。
Real datasets are sparse (yearly/monthly rows); a smooth race needs **interpolated keyframes between data rows** plus bars that slide to new ranks.
数据必须先校验再渲染:静默 NaN 会变成高度 0 的柱子,三分钟后才发现。
Treat the dataset as the single input. Parse, validate, and *fail loud* before rendering — a silent `NaN` becomes a bar of height 0 in the final MP4, which you only notice after a 3-minute render.
清洗规则清单化:数值强转校验、缺口显式补、按时间升序、条数上限约 12。
- **Cap item count.** Beyond ~12 bars a race is unreadable; keep top-N per frame and drop the rest, or pre-aggregate the long tail into "Other".
计数器精度:插值数值 → 取整 → format,并用 tabular-nums 固定字宽。
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.
节奏预算按「每个数据事件」算:每行 0.3–0.6s、关键点减速、终帧静置 2–3s。
| Per data row (race) | 0.3–0.6s | fast enough to feel alive, slow enough to track the leader |
标注只在关键时刻出现(交叉点、峰值),用时间窗淡入淡出。
const eventFrame = fps * 6.2; // when the crossover happens
验证不可省:交付物承载精确数字,一像素布局 bug 可原谅、错一位数字不可。
a one-pixel layout bug is forgivable, a wrong digit is not.
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | remotion(frame/interpolate/useVideoConfig) |
| package | d3-scale / d3-shape(值域映射与路径生成,仅取其数学,不用其计时器) |
| package | csv-parse(CSV → records,配 skip_empty_lines/trim) |
| cli | ffmpeg / ffprobe(仓库共享 contact-sheet.sh、probe-mp4.sh) |
| network | iart.ai(reference 尾部推广链接,具 utm 参数;仅读者点击才请求) |
4风险提醒 风险提醒:橙色 · 评估后使用
- 依赖面比 SKILL.md 声明更宽 — csv-parse、d3-shape 只在 reference 出现;只读 SKILL.md 的使用者不会预期这些包。
- 摘抄 SKILL.md 片段会踩坑 — race 伪代码缺 clamp、示例帧号与 reference 默认节奏不一致,照抄者可能末帧过冲或取错 end frame。
- 数据真伪不被审 — skill 保证数字被精确呈现,但不保证数据本身可信;错误但不违法的数据集会被渲染成权威感很强的画面。
- 远程包与本地代码执行 — npx/npm 取回并执行 remotion/d3/csv-parse;Remotion 会 bundle 运行项目代码(框架固有属性)。
- 商业引流位 — reference 尾部固定 iart.ai 推广段与 utm 链接。
5第二遍独立确认
- [discrepancy] SKILL.md 是否声明了 CSV 解析依赖 — SKILL.md 通篇未提 `csv-parse`(只把 CSV→props 指向 data-pipeline.md),而 reference 直接 `import { parse } from "csv-parse/sync"`;依赖清单在 SKILL.md 层面不完整。
- [discrepancy] race 伪代码是否与 reference 的 clamp 要求一致 — SKILL.md 片段 `const f = easeInOut(t - i);` 未把 f 夹到 [0,1],而 bar-chart-race.md 用 `Math.min(Math.max(tRow - i, 0), 1)` 并把它列入 pitfall 表(Last frame overshoots → Clamp f to [0,1]);直接照抄 SKILL.md 片段会末帧过冲。
- [discrepancy] 示例帧号与 reference 默认节奏是否对齐 — SKILL.md 验证示例给 `--frame=149 # last = durationInFrames - 1`(即 150 帧=5s 总长),而 reference 默认 SECONDS_PER_ROW=1.5 且示例 4 行 → 195 帧(末帧 194)。公式正确但示例数字与 reference 算例不一致。
- [ok] 数字格式化口径是否全文一致 — SKILL.md 与 data-pipeline/bar-chart-race 三处都用 Math.round() + Intl.NumberFormat(en-US) + font-variant-numeric: tabular-nums;percent 变体单独处理分数。
- [ok] 「D3 可用 scale 不可用 transition」的边界是否一致 — SKILL.md 明说 D3 仍适合 scales and shapes,只是不用它的 timers;bar-chart-race.md 实际只 import scaleLinear、无任何 .transition() 调用。
- [ok] 时长推导与 calculateMetadata 声明 — SKILL.md 要求数据相关时长在 calculateMetadata 计算,reference 用 (rows.length - 1) * SECONDS_PER_ROW * fps + hold * fps 推导总帧数,思路一致。
6结论
a97e1c50769273d4…8ce2709c39