Remotion LabRemotion Lab
核心動畫動態長條圖:spring 驅動的高度成長
data-vizbar-chartspringstaggerintermediate

動態長條圖:spring 驅動的高度成長

六個月營收長條圖依序「長出來」。用 Math.max 自動算最大值、spring 控制每根的高度成長、stagger 讓視線從左掃到右——數據視覺化最經典的動畫一篇學會。

成品預覽

6 秒、六根長條依序往上長,每根頂端的營收數字也跟著計數。最大值用 Math.max(...values) 自動算出來,所以換資料完全不用調比例。視覺上看起來像「儀表板正在被資料填滿」——這是月報、季報、年報影片裡幾乎一定會用到的元件。


這篇教什麼

長條圖看起來簡單,但要做得「有節奏感」有三個關鍵:

  1. 比例自動化——別寫死最大值,用 Math.max(...values) 自動算
  2. spring 控制成長——linear 看起來像進度條,spring 才有「彈性的長出來」感
  3. Stagger 做掃視效果——六根同時長出來會分散注意力,依序長出來會引導觀眾的視線從左到右

這三點做好就能套到任何「N 個資料點 + 高度比較」的場景。不只是月報,銷售排名、流量對比、選舉開票…只要是「比大小」都吃這套。


前置知識

  • T6:KPI 計數卡片 — 上一篇講 ease-out 計數,這篇會繼續用
  • 會看基本的 JavaScript 陣列 .map()

Step 1:資料結構

// src/data/q1-2026.ts
export const MONTHLY = [
  {month: "10 月", value: 3.8},
  {month: "11 月", value: 4.1},
  {month: "12 月", value: 4.58},
  {month: "1 月",  value: 3.92},
  {month: "2 月",  value: 4.2},
  {month: "3 月",  value: 4.36},
];

數字單位是百萬(M)。不要先把它換算成像素高度——那是畫面層的工作,資料層只放原始值。


Step 2:自動計算最大值 + 高度映射

const MONTHLY = [...]; // 上面的資料
const CHART_HEIGHT = 320;
const maxValue = Math.max(...MONTHLY.map((d) => d.value));
// → 4.58
 
const barPixelHeight = (value: number) =>
  (value / maxValue) * CHART_HEIGHT;

Math.max(...arr.map(...)) 是一個你會用到無數次的 pattern——展開陣列丟給 Math.max核心觀念是「比例」而不是「絕對值」:最高的那根永遠占滿 320px,其他根按比例縮短。換新資料只要 push 一筆進陣列,所有 bar 自動重新分配比例。

如果 maxValue 隨時間變動會很怪(畫面會「跳尺度」),所以這個值只算一次、放在元件外或 useMemo 裡。


Step 3:每根用 spring 長出來

import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
 
const Bar: React.FC<{
  data: {month: string; value: number};
  delay: number;
  maxValue: number;
}> = ({data, delay, maxValue}) => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();
 
  // 0 → 1 的彈性進度
  const grow = spring({
    frame: frame - delay,
    fps,
    config: {damping: 18, stiffness: 80},
    durationInFrames: 30,
  });
 
  const targetHeight = (data.value / maxValue) * 320;
  const currentHeight = targetHeight * grow;
  const currentValue = (data.value * grow).toFixed(2);
 
  return (
    <div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>
      {/* 頂端的數字 */}
      <div style={{
        fontSize: 28,
        fontWeight: 700,
        color: "#facc15",
        marginBottom: 8,
        fontVariantNumeric: "tabular-nums",
      }}>
        {currentValue}M
      </div>
      {/* 長條本身 */}
      <div style={{
        width: 120,
        height: currentHeight,
        background: "linear-gradient(180deg, #60a5fa, #3b82f6)",
        borderTopLeftRadius: 12,
        borderTopRightRadius: 12,
        boxShadow: "0 8px 30px rgba(59,130,246,0.4)",
      }} />
      {/* 月份標籤 */}
      <div style={{fontSize: 28, color: "#94a3b8", marginTop: 12}}>
        {data.month}
      </div>
    </div>
  );
};

幾個值得記下來的細節:

  • 高度從 0 開始長——currentHeight = targetHeight * grow,spring 從 0 走到 1 時 bar 從 0 走到目標高度
  • 頂端的數字跟著 spring 成長——這樣觀眾看到 bar 長到一半時,數字也是一半,數字跟畫面完全同步
  • borderTopLeftRadius 而不是 borderRadius——只圓上半部,下半部是直角,視覺上「站在 X 軸上」的感覺才對
  • boxShadow 加藍色發光——讓 bar 看起來「發光」而不是貼在背景上,這是 dashboard 美感的關鍵

damping: 18, stiffness: 80 是比較「軟」的設定——bar 會在頂端微微回彈一下。如果你想要「硬一點、不要彈」的感覺,把 damping 拉到 30 就會變成幾乎不彈。


Step 4:Stagger 控制節奏

const STAGGER = 8; // frames between bars
 
<div style={{display: "flex", gap: 60, alignItems: "flex-end"}}>
  {MONTHLY.map((d, i) => (
    <Bar key={d.month} data={d} delay={i * STAGGER} maxValue={maxValue} />
  ))}
</div>

每根 bar 比前一根晚 8 frames 進場。8 frames ≈ 0.27 秒,剛好夠觀眾的眼睛跟著從左到右掃過去。

怎麼選 stagger 數字?

  • 太小(< 4 frames):所有 bar 看起來幾乎同時出現,stagger 效果消失
  • 太大(> 15 frames):節奏拖沓,6 根 bar 全部長完要 2 秒以上
  • 8~12 frames 是甜蜜點,給人「咻咻咻」的快節奏感

alignItems: "flex-end" 是讓所有 bar 從底部對齊往上長——這是最重要的一個 CSS 屬性,沒有這行所有 bar 會從容器中央開始長,視覺會崩。


Step 5:渲染

npx remotion render AnimatedBarChart out/bar-chart.mp4 --codec=h264

常見問題

Q:為什麼不直接用 Chart.js / Recharts?

可以,但有兩個問題:

  1. 這類圖表庫假設 window.requestAnimationFrame 會被呼叫,Remotion 渲染時沒有這種 loop(時間是由 frame 決定的,不是 wall clock)
  2. 它們的動畫用 CSS transition,Remotion 渲染時會被凍結(你會得到一個靜態的最終狀態)

自己用 <div> + 高度才能真的對到每一 frame。如果非用不可,可以看 /docs/third-party 了解怎麼整合。

Q:資料有 100 筆,長條圖畫不下?

超過 10 個資料點的圖表要考慮捲動或分頁。Remotion 很適合做「捲動畫面」——用 translateX 讓畫面往左滑,每個資料點停留 30 frames。或者用 T32:D3 長條圖賽跑 的做法,讓 bar 動態重新排序。

Q:bar 的數字格式想要不同單位(千、萬、億)?

寫一個 formatter:

const formatValue = (v: number) => {
  if (v >= 1e8) return (v / 1e8).toFixed(1) + "億";
  if (v >= 1e4) return (v / 1e4).toFixed(0) + "萬";
  return v.toLocaleString();
};

中文區的數字單位是「萬、億」(10^4、10^8),跟英文的「K、M、B」不一樣。混用會讓觀眾分心。

Q:可以做水平長條圖(往右長)嗎?

可以,把 height 改成 widthflexDirection: column 改成 rowalignItems: "flex-end" 改成 flex-start。其他邏輯完全一樣。水平長條更適合「項目名稱很長」的情況(比如國家名、產品名)。


本篇涵蓋的官方文件


下一步

T9:圓餅圖:SVG arc + per-slice 動畫 — 長條圖比的是「絕對大小」,圓餅圖比的是「占比」。下一篇用 SVG 的 <path> arc 命令手刻一個會動的甜甜圈圖,學會 polarToCartesianlargeArc flag——這是所有「圓周類圖表」的基礎。