「一張好圖表,勝過千言萬語。」
昨天我們透過 TanStack Query 成功拉取了用戶的交易紀錄。今天,我們要將這些冰冷的數字轉化為直觀、動態且視覺效果優良的圖表!我們將使用 React 生態圈中最熱門的圖表庫之一 —— Recharts,搭配 AI 來快速實現「收支圓餅圖」與「每月消費趨勢折線圖」。
在我們的 「AI 智慧個人財務追蹤器 (Smart Finance Tracker)」 中:
將 Day 15 抓到的扁平化交易資料(Transaction[]),轉換為圖表所需的統計結構(例如:依類別加總、依月份加總)。
使用 Recharts 建立響應式(Responsive)的類別圓餅圖(PieChart)與收支折線圖(LineChart)。
掌握在 AI 輔助開發下,如何克服 Recharts 在 SSR(Next.js App Router)及 RWD 容器中的常見繪圖陷阱。
圖表庫組件通常有大量嵌套標籤(如 , , , ),手寫不僅繁瑣還容易漏掉屬性。但只要提供清楚的 Data Schema,AI 能極速生成 UI!
我們正在 Next.js (App Router) + Tailwind CSS 專案中開發圖表元件。
請幫我建立一個 ExpenseCategoryChart 元件(路徑:src/components/charts/ExpenseCategoryChart.tsx)。
要求:
transactions: Transaction[]。type === 'EXPENSE' 的交易按 category 分組並計算總金額。AI 很快給出了一份完整的程式碼,表面上看起來要素齊全:
// src/components/charts/ExpenseCategoryChart.tsx
import React from 'react';
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Transaction } from '@/hooks/useTransactions';
const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6'];
export const ExpenseCategoryChart = ({ transactions }: { transactions: Transaction[] }) => {
// 資料聚合 logic
const categoryData = transactions
.filter((t) => t.amount < 0) // AI 假設負數是支出
.reduce((acc, t) => {
const existing = acc.find((item) => item.name === t.category);
if (existing) {
existing.value += Math.abs(t.amount);
} else {
acc.push({ name: t.category, value: Math.abs(t.amount) });
}
return acc;
}, [] as { name: string; value: number }[]);
return (
<div className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={categoryData}
cx="50%"
cy="50%"
outerRadius={80}
fill="#8884d8"
dataKey="value"
label
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
);
};
這段看似精美的圖表程式碼放到頁面上後,直接迎來了兩個典型的「前端圖表災難」:
Hydration Failed & DOM Height 塌陷 (0px 災難):
在 Next.js 伺服器端渲染時,ResponsiveContainer 無法預先知道瀏覽器的 DOM 寬高,導致頁面載入瞬間出現 Hydration failed 警告。
更糟的是,父容器若沒有明確的相對定位或高寬,Recharts 會算不出尺寸,導致圖表高度變成 0px 甚至整張消失!
商業邏輯判斷錯誤:
AI 擅自假設 amount < 0 代表支出,但我們在 Day 6 建立資料庫 Schema 時,設計了明確的 type: 'INCOME' | 'EXPENSE' 欄位,amount 皆為正數。這導致 categoryData 永遠是空的,圓餅圖直接開寂寞。
微調指令,補齊客戶端渲染機制與資料格式校正:
修正 Prompt:
「1. 交易類別過濾請改用 t.type === 'EXPENSE',金額直接累加 t.amount。
2. 請在檔案頂部加上 'use client' 宣告,並加上 mounted 狀態判斷,確保 Recharts 只在客戶端掛載(Mounted)後才渲染 ResponsiveContainer,避免 Next.js SSR 水合錯誤與 0px 渲染問題。」
最終修正版本 (Refactored Code)
'use client';
import React, { useState, useEffect } from 'react';
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Transaction } from '@/hooks/useTransactions';
const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6', '#06b6d4'];
interface Props {
transactions: Transaction[];
}
export const ExpenseCategoryChart = ({ transactions }: Props) => {
const [isMounted, setIsMounted] = useState(false);
// 確保只在 Client 端渲染 Recharts
useEffect(() => {
setIsMounted(true);
}, []);
// 正確按 EXPENSE 類型聚合金額
const categoryData = React.useMemo(() => {
const expenseMap: Record<string, number> = {};
transactions
.filter((t) => t.type === 'EXPENSE')
.forEach((t) => {
expenseMap[t.category] = (expenseMap[t.category] || 0) + t.amount;
});
return Object.keys(expenseMap).map((cat) => ({
name: cat,
value: expenseMap[cat],
}));
}, [transactions]);
if (!isMounted) {
return <div className="w-full h-[300px] flex items-center justify-center text-slate-400">圖表載入中...</div>;
}
if (categoryData.length === 0) {
return <div className="w-full h-[300px] flex items-center justify-center text-slate-400">尚無支出資料</div>;
}
return (
<div className="w-full h-[300px] min-h-[300px] relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={categoryData}
cx="50%"
cy="50%"
innerRadius={60} // 改為 DonutChart 風格,視覺更現代
outerRadius={90}
paddingAngle={5}
dataKey="value"
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
formatter={(value: number) => `$${value.toLocaleString()}`}
contentStyle={{ backgroundColor: '#1e293b', borderRadius: '8px', color: '#fff', border: 'none' }}
/>
<Legend verticalAlign="bottom" height={36} />
</PieChart>
</ResponsiveContainer>
</div>
);
};
資料轉換 (Data Transformation) 是圖表的靈魂:
圖表庫通常只吃特定格式的 Array(如 { name, value })。學會使用 useMemo 搭配 reduce 或 forEach 整理 API 原始資料,是前端開發的核心功力,也能避免每次 Render 都重新運算大筆資料。
SSR 框架中的 Canvas / SVG 繪圖陷阱:
大部分第三方圖表庫(Recharts、Chart.js)極度依賴 DOM API(getBoundingClientRect)。在 React Server Components 或 SSR 模式下,務必確認該元件僅在 Client 掛載完畢後才計算尺寸,否則畫面「長不出來」會是極常見的 Bug。