動態長條圖:spring 驅動的高度成長
六個月營收長條圖依序「長出來」。用 Math.max 自動算最大值、spring 控制每根的高度成長、stagger 讓視線從左掃到右——數據視覺化最經典的動畫一篇學會。
成品預覽
6 秒、六根長條依序往上長,每根頂端的營收數字也跟著計數。最大值用 Math.max(...values) 自動算出來,所以換資料完全不用調比例。視覺上看起來像「儀表板正在被資料填滿」——這是月報、季報、年報影片裡幾乎一定會用到的元件。
這篇教什麼
長條圖看起來簡單,但要做得「有節奏感」有三個關鍵:
- 比例自動化——別寫死最大值,用
Math.max(...values)自動算 - spring 控制成長——
linear看起來像進度條,spring才有「彈性的長出來」感 - 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?
可以,但有兩個問題:
- 這類圖表庫假設
window.requestAnimationFrame會被呼叫,Remotion 渲染時沒有這種 loop(時間是由 frame 決定的,不是 wall clock) - 它們的動畫用 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 改成 width、flexDirection: column 改成 row、alignItems: "flex-end" 改成 flex-start。其他邏輯完全一樣。水平長條更適合「項目名稱很長」的情況(比如國家名、產品名)。
本篇涵蓋的官方文件
- /docs/spring — spring 物理動畫
- /docs/animation-math — interpolate 與時間映射
下一步
T9:圓餅圖:SVG arc + per-slice 動畫 — 長條圖比的是「絕對大小」,圓餅圖比的是「占比」。下一篇用 SVG 的 <path> arc 命令手刻一個會動的甜甜圈圖,學會 polarToCartesian 與 largeArc flag——這是所有「圓周類圖表」的基礎。