比特之外

Remotion 教程 11:做数据可视化视频时,只让 D3 负责计算

D3 很强,但在 Remotion 里不能照搬网页图表写法。最重要的边界是:

D3 负责 scale、shape、数据映射
Remotion 负责 frame 动画

为什么不用 D3 transition

网页 D3 示例里经常会看到:

selection.transition().duration(800)

这类写法适合网页运行时动画,但不适合作为 Remotion 视频主动画。它的问题和 CSS transition、Motion runtime 动画类似:动画过程由运行时推进,而不是由 frame 直接推导。

在 Remotion 里更稳的写法是:

数据 -> D3 scale -> Remotion frame -> style / svg 属性

D3 负责 scale

例如柱状图需要两个映射:

const x = scaleBand()
  .domain(chartData.map((item) => item.label))
  .range([0, 980])
  .padding(0.24);
 
const y = scaleLinear().domain([0, 100]).range([0, 420]);

scaleBand() 负责计算每个柱子的横向宽度。
scaleLinear() 负责把数据值转换成像素高度。

这就是 D3 在 Remotion 里的推荐角色:负责数学映射。

Remotion 负责动画

每根柱子出现时,使用 Remotion 的 interpolate()

const progress = interpolate(
  frame,
  [36 + index * 8, 72 + index * 8],
  [0, 1],
  {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  },
);

这段代码让每根柱子错开出现:

第 1 根:36 -> 72 帧
第 2 根:44 -> 80 帧
第 3 根:52 -> 88 帧

然后把进度乘到柱子高度上:

const height = y(item.value) * progress;

这样就能得到稳定的逐帧动画。

完整最小示例

下面是一个 D3 计算柱宽和高度映射、Remotion 控制动画的柱状图。

import React from "react";
import { scaleBand, scaleLinear } from "d3";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
 
const chartData = [
  { label: "脚本", value: 72 },
  { label: "字幕", value: 88 },
  { label: "素材", value: 64 },
  { label: "渲染", value: 96 },
];
 
export const DataVideo: React.FC = () => {
  const frame = useCurrentFrame();
 
  const x = scaleBand()
    .domain(chartData.map((item) => item.label))
    .range([0, 980])
    .padding(0.24);
 
  const y = scaleLinear().domain([0, 100]).range([0, 420]);
 
  return (
    <AbsoluteFill
      style={{
        backgroundColor: "#f8fafc",
        padding: 96,
        fontFamily: "Inter, sans-serif",
      }}
    >
      <h1 style={{ margin: 0, fontSize: 72 }}>
        D3 计算图形,Remotion 控制动画
      </h1>
 
      <div
        style={{
          marginTop: 120,
          height: 520,
          display: "flex",
          alignItems: "flex-end",
          gap: 24,
        }}
      >
        {chartData.map((item, index) => {
          const progress = interpolate(
            frame,
            [36 + index * 8, 72 + index * 8],
            [0, 1],
            {
              extrapolateLeft: "clamp",
              extrapolateRight: "clamp",
            },
          );
 
          const height = y(item.value) * progress;
 
          return (
            <div
              key={item.label}
              style={{
                width: x.bandwidth(),
                display: "flex",
                flexDirection: "column",
                alignItems: "center",
              }}
            >
              <div
                style={{
                  width: "100%",
                  height,
                  borderRadius: 8,
                  backgroundColor: "#16a34a",
                }}
              />
              <div style={{ marginTop: 18, fontSize: 30, fontWeight: 800 }}>
                {item.label}
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

D3 适合什么视频

优先考虑 D3 的场景:

  • 柱状图、折线图、面积图。
  • 排名变化视频。
  • 数据报告自动生成。
  • 地图、路径、弧线、力导向等图形计算。
  • 需要 scale、axis、shape、interpolate 的图表。

如果只是几个固定数字卡片,不需要 D3。直接用 interpolate() 和普通布局就够了。

和 zod、Renderer 的关系

数据视频通常会进入批量生成流程。建议把输入数据做成明确结构,并用 zod 校验:

type ChartItem = {
  label: string;
  value: number;
};

真实项目里可以把 chartData 替换成接口返回的数据,再用 @remotion/renderer 批量生成日报、周报、投放报告或业务汇总视频。

示例项目位置

这篇文章对应示例项目里的:

Composition: LessonDataVideo
文件: src/lessons/DataVideo.tsx
数据: src/course/mockData.ts

运行示例项目后,可以尝试:

  1. 修改 chartData 数值。
  2. [36 + index * 8, 72 + index * 8] 改成 [20 + index * 6, 64 + index * 6]
  3. 观察柱子是否更早出现、节奏是否更紧。

下一篇回到完整工作流:把脚本、音频、字幕、素材和模板串成 AI 口播短视频。