iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 16 篇

# Day 16 - 動態資料視覺化:生成 Recharts / Chart.js 圖表組件

  • 分享至 

  • xImage
  •  

「一張好圖表,勝過千言萬語。」
昨天我們透過 TanStack Query 成功拉取了用戶的交易紀錄。今天,我們要將這些冰冷的數字轉化為直觀、動態且視覺效果優良的圖表!我們將使用 React 生態圈中最熱門的圖表庫之一 —— Recharts,搭配 AI 來快速實現「收支圓餅圖」與「每月消費趨勢折線圖」。

今日目標

在我們的 「AI 智慧個人財務追蹤器 (Smart Finance Tracker)」 中:

將 Day 15 抓到的扁平化交易資料(Transaction[]),轉換為圖表所需的統計結構(例如:依類別加總、依月份加總)。

使用 Recharts 建立響應式(Responsive)的類別圓餅圖(PieChart)與收支折線圖(LineChart)。

掌握在 AI 輔助開發下,如何克服 Recharts 在 SSR(Next.js App Router)及 RWD 容器中的常見繪圖陷阱。

Human vs. AI:提示詞與協同開發

圖表庫組件通常有大量嵌套標籤(如 , , , ),手寫不僅繁瑣還容易漏掉屬性。但只要提供清楚的 Data Schema,AI 能極速生成 UI!

給 AI 的指令(Prompt)

我們正在 Next.js (App Router) + Tailwind CSS 專案中開發圖表元件。
請幫我建立一個 ExpenseCategoryChart 元件(路徑:src/components/charts/ExpenseCategoryChart.tsx)。

要求:

  1. 輸入 props 為交易列表 transactions: Transaction[]。
  2. 先在元件內部整理資料:將 type === 'EXPENSE' 的交易按 category 分組並計算總金額。
  3. 使用 Recharts 繪製圓餅圖(PieChart),並使用 ResponsiveContainer 讓圖表自動適應父容器大小。
  4. 加上漂亮的 Tooltip 與 Legend,顏色請搭配 Tailwind 現代科技風格(如 slate、indigo、emerald、amber、rose)。
  5. 必須相容於 Next.js SSR,避免 Hydration 錯誤。

AI 產出的結果

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>
  );
};

翻車與除錯過程 (Debug Experience)

這段看似精美的圖表程式碼放到頁面上後,直接迎來了兩個典型的「前端圖表災難」:

AI 哪裡搞砸了?

Hydration Failed & DOM Height 塌陷 (0px 災難):

在 Next.js 伺服器端渲染時,ResponsiveContainer 無法預先知道瀏覽器的 DOM 寬高,導致頁面載入瞬間出現 Hydration failed 警告。

更糟的是,父容器若沒有明確的相對定位或高寬,Recharts 會算不出尺寸,導致圖表高度變成 0px 甚至整張消失!

商業邏輯判斷錯誤:

AI 擅自假設 amount < 0 代表支出,但我們在 Day 6 建立資料庫 Schema 時,設計了明確的 type: 'INCOME' | 'EXPENSE' 欄位,amount 皆為正數。這導致 categoryData 永遠是空的,圓餅圖直接開寂寞。

修正與引導(Human Decision)

微調指令,補齊客戶端渲染機制與資料格式校正:

修正 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>
  );
};

今日 JavaScript / 前端筆记

資料轉換 (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。


上一篇
# Day 15 - 前後端 API 串接:讓 AI 寫 Client-side 資料獲取(TanStack Query / SWR)
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言