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外部依赖
| Kind | Dependency / Endpoint |
|---|---|
| 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管控建议与判定依据
- scripts/: 执行本地脚本或命令: skill 目录内无自带脚本;SKILL.md/reference 指示 agent 执行 `node scripts/build-props.mjs
- 网络端点: 声明外部网络访问: 执行期无业务 API 调用;数据集由用户以 CSV/JSON 提供。
- 本地工作目录: 文件读写与工件输出: 在用户工程内写 `props/<name>.json`、`out/<name>.mp4`、`out/f-start.png`/`out/f-mid.p
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…