前面 27 天,我們把 Chart.js 從「認識」到「精通」走過了一整輪:從基礎的折線圖、長條圖,到雷達圖、氣泡圖等各種圖表類型;從座標軸、圖例、動畫等細節設定,到資料串接(JSON/API/CSV)、動態更新與互動事件;再到外掛開發、樣式主題客製化,以及與 React、Vue、Angular 三大框架的整合。今天開始,我們要進入最後三天的綜合實戰專案(Capstone Project):Day 28 規劃專案與設計資料、Day 29 動手實作儀表板、Day 30 效能優化與總複習。跟前面 27 天「一天一個知識點」的節奏不同,今天完全不會出現新的 Chart.js API——我們要退後一步,練習一件比寫程式更容易被新手忽略、卻決定專案成敗的事:在動手寫 code 之前,先把「做什麼」「為誰做」「資料長怎樣」「用什麼圖表」想清楚。
新手做資料視覺化專案時,很常見的順序是:「先找一份資料 => 打開編輯器 => 想到什麼圖表就刻什麼圖表」。這樣做通常在專案進行到一半,會遇到以下幾種情況:
datasets 需要的是「已經彙整好」的陣列(例如「每個月的支出總和」),但原始資料庫拿到的往往是「每一筆交易紀錄」。如果沒有先想清楚資料要怎麼從「明細」轉換成「彙整後的圖表資料」,寫到一半才發現要大改資料處理邏輯,非常浪費時間。decimation(資料抽稀)這個效能優化外掛,但如果資料結構設計時完全沒考慮「未來資料筆數可能上千上萬筆」,重構起來會比一開始就設計好複雜很多。這就是為什麼專業的資料視覺化專案,往往會先畫「藍圖」再蓋房子:軟體開發中的規劃,就等同於建築的設計圖——把格局、管線、水電都想清楚,才不會蓋到一半才發現承重牆位置不對。今天的內容,就是帶你走過這張「藍圖」該長什麼樣子。
在動手之前,我們可以把「專案規劃」拆解成四個循序漸進的步驟,本篇文章的章節安排也是依照這個順序展開:
| 步驟 | 目的 |
|---|---|
| (1) 需求分析 | 釐清「這個功能是為誰而做、要解決什麼問題」,避免一開始就陷入「要用哪個圖表」的技術細節 |
| (2) 主題選定 | 從候選方案中,依照客觀標準挑出最適合的一個主題 |
| (3) 範疇與資訊架構規劃 | 決定「這次要做多少功能」(Scope)與「畫面要怎麼分區塊呈現」(資訊架構) |
| (4) 資料結構設計與圖表對照 | 決定「資料長什麼樣子」(Data Modeling)以及「該用什麼圖表呈現」(圖表類型選用對照表) |
完成這四步之後,我們還會多做一步「技術棧與資料夾規劃」,把 Day 29 動手實作前需要決定的環境設定都先確認好,讓明天可以直接進入寫程式的節奏,不用中途停下來做架構決策。
User Story(使用者故事) 是一種很常見、也很好上手的需求釐清技巧,格式很簡單:
身為 [某個角色],我想要 [做到某件事],以便 [達成某個目的]。
這個技巧的重點,是強迫自己先想「目的」,而不是先想「用什麼技術/圖表實現」。舉例來說,如果我們正在規劃「個人財務儀表板」,可以先寫出幾條 User Story:
寫完這些故事後,你會發現「圖表類型」其實是最後才決定的事——先有「一眼看到佔比」的需求,才會想到適合用環狀圖(Doughnut Chart);先有「近半年趨勢變化」的需求,才會想到適合用折線圖(Line Chart)。這個「先有目的、才選圖表」的順序,也是第七節「圖表類型選用對照表」的設計邏輯。
💡 同樣的技巧也適用於另外兩個候選主題。例如「銷售數據分析」可以寫成「身為業務主管,我想要比較各分店本季業績排名,以便決定資源分配優先順序」;「網站流量監控」可以寫成「身為網站管理者,我想要即時看到流量突然暴增的時間點,以便判斷是否遭受異常流量攻擊」。不管選哪個主題,第一步永遠是先寫幾條 User Story,把「為誰做、為什麼做」講清楚。
候選主題:個人財務儀表板、銷售數據分析、網站流量監控。
這三個主題都很適合拿來練習 Chart.js,但資料特性與難度略有不同,以下逐一分析:
| 比較面向 | 個人財務儀表板 | 銷售數據分析 | 網站流量監控 |
|---|---|---|---|
| 核心受眾 | 個人使用者,管理自己的收支 | 業務/營運主管,檢視業績表現 | 網站管理者/工程師,監控流量狀態 |
| 資料時間特性 | 以「日」為單位的歷史資料,偏靜態 | 以「月/季」為單位的歷史資料,偏靜態 | 以「分鐘/小時」為單位,偏即時、資料量大 |
| 典型資料量 | 中等(一般人一個月數十到數百筆交易) | 中等(依商品/分店數量而定) | 龐大(每分鐘都有新的請求紀錄) |
| 適合的圖表類型 | 折線圖、環狀圖、長條圖、極座標圖 | 長條圖、混合圖表、雷達圖(多維度比較) | 折線圖(含 zoom/decimation)、地理分佈長條圖 |
| 主要挑戰 | 資料分類(類別、帳戶)的彙整邏輯 | 多維度比較(產品 × 地區 × 時間)的資料切片 | 大量資料點的效能優化、即時更新頻率 |
| 難度 | 入門 ~ 中等 | 中等 | 中等~中高(牽涉效能與即時性) |
| 資料取得方式 | 自建 Mock Data 即可,貼近生活容易發想 | 需要設計較完整的商業情境資料 | 需要模擬高頻率的即時資料流 |
資料維度單純(時間、類別、帳戶、金額),但圖表類型變化豐富:收支趨勢、分類佔比、預算比較、資產配置都能各自對應到不同圖表。因為資料是「個人生活情境」,發想 Mock Data 也最直覺,很適合作為練習「資料建模」的入門題材。
資料通常包含「產品 × 地區 × 時間」多個維度,很適合練習「同一份原始資料,依照不同切角彙整出不同圖表」的能力(例如同一份訂單資料,可以彙整成「各產品業績排行」的長條圖,也可以彙整成「各分店月趨勢」的折線圖)。難度略高於財務儀表板,因為維度組合較複雜。
最貼近「即時系統」的情境,會用到 Day 18 學過的輪詢(Polling)更新,以及 之後即將學到的 decimation 資料抽稀。因為資料量最大、更新頻率最高,三者之中技術難度最高,也最考驗效能規劃。
接下來將以「個人財務儀表板」作為實作範例,原因是它的資料維度單純、圖表類型多樣,最能完整覆蓋前面 27 天學過的圖表類型與技巧,也最適合在三天內做出一個「有模有樣」的成品。如果你對「銷售數據分析」或「網站流量監控」更有興趣,完全可以套用接下來教的同一套規劃方法(User Story => Scope => 資料建模 => 圖表對照),把資料主題換成你自己想做的內容,本篇後續章節提到的技巧一樣適用。
MoSCoW 是一種常見的範疇管理技巧,把功能依照優先順序分成四類:Must have(一定要有)、Should have(應該要有)、Could have(可以有,非必要)、Won't have(這次不做)。這樣做的好處是:一旦時間不夠,可以很清楚地知道「先砍掉哪些功能」,而不會影響核心體驗。
| 分類 | 功能項目 |
|---|---|
| Must have | 本月收支總覽卡片(總收入/總支出/結餘)、收支趨勢折線圖、支出分類佔比環狀圖、交易明細列表 |
| Should have | 預算 vs 實際長條圖、時間區間切換(本月/近 3 個月/近半年) |
| Could have | 資產配置極座標圖、CSV 資料匯出、深色模式(Dark Mode) |
| Won't have(這次不做) | 多帳戶跨幣別換算、AI 消費建議、多人協作/權限管理 |
決定好範疇之後,接著要把畫面拆解成幾個獨立的區塊,這就是 IA(Information Architecture,資訊架構) 規劃:
| 區塊 | 呈現內容 | 對應圖表 | 資料來源 |
|---|---|---|---|
| (1) 總覽卡片區 | 本月總收入、總支出、結餘、與上月比較的百分比 | 數字卡片(非圖表) | Transaction 彙整 |
| (2) 收支趨勢區 | 近 N 個月的收入/支出走勢 | 折線圖(Line,含填色區域) | Transaction 依月彙整 |
| (3) 支出分類區 | 本月各類別支出佔比 | 環狀圖(Doughnut) | Transaction 依類別彙整 |
| (4) 預算比較區 | 各類別「預算」與「實際支出」對照 | 長條圖(Bar,兩個 dataset) | Transaction + Budget |
| (5) 資產配置區(Could have) | 各帳戶/投資項目的資產佔比 | 極座標圖(Polar Area) | Account 彙整 |
| (6) 交易明細區 | 可篩選的交易清單(日期、類別、關鍵字) | 表格(非圖表) | Transaction 原始明細 |
在真正寫 HTML/CSS 之前,先用簡單的文字方塊畫出版面草圖(Wireframe),確認每個區塊在畫面上的相對位置與大小關係,之後 Day 29 排版時(Grid/Flexbox)才有明確的依據:

這張草圖不需要畫得多精美(用 Excalidraw、Figma ... 工具或甚至紙筆都可以),重點是先確認資訊架構與資料流向沒有問題,而不是花時間在視覺設計上——視覺細節可以留到 Day 29 排版時再慢慢調整。
在設計資料結構之前,先弄懂三個資料分析領域的基礎概念,之後看任何一份資料,都可以用這三個角度去拆解:
以「個人財務儀表板」為例:Measure 是「金額(amount)」,Dimension 是「類別(category)」「帳戶(account)」「日期(date)」「收支類型(type)」,而不同圖表需要的 Granularity 也不同——收支趨勢折線圖要「每月」的粒度,交易明細表格則要「每筆」的粒度。
依照第 5.2 節拆解出來的區塊,可以歸納出四個核心的 Entity(資料實體):
Transaction(交易紀錄)
| 欄位 | 型別 | 說明 | 範例 |
|---|---|---|---|
id |
string | 交易唯一識別碼 | "t20260305-01" |
date |
string(ISO 8601) | 交易日期 | "2026-03-05" |
type |
"income" | "expense" |
收入或支出 | "expense" |
categoryId |
string | 對應 Category 的 id | "cat-food" |
accountId |
string | 對應 Account 的 id | "acc-checking" |
amount |
number | 金額(一律存正數,方向由 type 決定) |
320 |
note |
string | 備註 | "超市採買" |
Category(類別)
| 欄位 | 型別 | 說明 | 範例 |
|---|---|---|---|
id |
string | 類別識別碼 | "cat-food" |
name |
string | 類別名稱 | "餐飲" |
type |
"income" | "expense" |
屬於收入還是支出類別 | "expense" |
color |
string | 圖表配色(十六進位色碼) | "#f97316" |
Account(帳戶)
| 欄位 | 型別 | 說明 | 範例 |
|---|---|---|---|
id |
string | 帳戶識別碼 | "acc-checking" |
name |
string | 帳戶名稱 | "薪轉帳戶" |
type |
string | 帳戶類型(現金/銀行/信用卡/投資) | "bank" |
balance |
number | 目前餘額(用於資產配置圖) | 85000 |
Budget(預算)
| 欄位 | 型別 | 說明 | 範例 |
|---|---|---|---|
id |
string | 預算識別碼 | "b-2026-03-food" |
categoryId |
string | 對應 Category 的 id | "cat-food" |
month |
string(YYYY-MM) |
預算所屬月份 | "2026-03" |
limitAmount |
number | 該類別當月預算上限 | 8000 |
這四個 Entity 之間用
categoryId、accountId互相關聯,就像資料庫的**外來鍵(Foreign Key)**設計一樣——這也是為什麼 Mock Data 或未來接後端 API 時,通常會回傳「交易明細」+「類別對照表」+「帳戶對照表」這種拆分開來的結構,而不是把類別名稱、顏色都重複寫死在每一筆交易紀錄裡。
Chart.js 要的資料格式,跟資料庫或 API 回傳的「原始明細」通常不一樣。以「收支趨勢折線圖」為例,我們需要的是「每個月的收入總和、支出總和」,而不是一筆一筆的交易紀錄,這中間需要一段 Aggregation(資料彙整) 邏輯,通常會用 reduce() 搭配「依 key 分組(groupBy)」的技巧完成:
// 將交易明細依「月份 + 收支類型」彙整成 Chart.js 需要的格式
function aggregateMonthlyTrend(transactions) {
// 先用 reduce 依「月份」分組,累加 income/expense
const grouped = transactions.reduce((acc, tx) => {
const month = tx.date.slice(0, 7); // "2026-03-05" -> "2026-03"
if (!acc[month]) {
acc[month] = { income: 0, expense: 0 };
}
acc[month][tx.type] += tx.amount;
return acc;
}, {});
// 依月份排序,轉換成 Chart.js 的 labels + datasets 結構
const months = Object.keys(grouped).sort();
return {
labels: months,
datasets: [
{
label: '收入',
data: months.map((m) => grouped[m].income),
},
{
label: '支出',
data: months.map((m) => grouped[m].expense),
},
],
};
}
「支出分類佔比環狀圖」則需要換一種分組維度——改成依 categoryId 分組加總:
function aggregateCategoryBreakdown(transactions, categories) {
const totals = transactions
.filter((tx) => tx.type === 'expense')
.reduce((acc, tx) => {
acc[tx.categoryId] = (acc[tx.categoryId] || 0) + tx.amount;
return acc;
}, {});
const categoryIds = Object.keys(totals);
return {
labels: categoryIds.map((id) => categories.find((c) => c.id === id).name),
datasets: [
{
data: categoryIds.map((id) => totals[id]),
backgroundColor: categoryIds.map((id) => categories.find((c) => c.id === id).color),
},
],
};
}
這兩段程式碼示範了同一份 transactions 原始資料,可以依照「不同的 Dimension」(月份 vs 類別)彙整出完全不同的圖表資料——這正是資料建模時,把「明細資料」與「彙整邏輯」分開設計的價值:原始資料只需要存一份,彙整方式可以隨著圖表需求任意增加。
規劃階段先手動準備一份合理的假資料(Mock Data),讓 Day 29 一開始就有資料可以測試,不用等後端 API 做完才能動手刻前端畫面:
{
"categories": [
{ "id": "cat-salary", "name": "薪資", "type": "income", "color": "#22c55e" },
{ "id": "cat-food", "name": "餐飲", "type": "expense", "color": "#f97316" },
{ "id": "cat-transport", "name": "交通", "type": "expense", "color": "#3b82f6" },
{ "id": "cat-entertainment", "name": "娛樂", "type": "expense", "color": "#a855f7" }
],
"accounts": [
{ "id": "acc-checking", "name": "薪轉帳戶", "type": "bank", "balance": 85000 },
{ "id": "acc-cash", "name": "現金", "type": "cash", "balance": 3200 }
],
"transactions": [
{ "id": "t1", "date": "2026-03-03", "type": "income", "categoryId": "cat-salary", "accountId": "acc-checking", "amount": 52000, "note": "三月份薪資" },
{ "id": "t2", "date": "2026-03-05", "type": "expense", "categoryId": "cat-food", "accountId": "acc-cash", "amount": 320, "note": "超市採買" },
{ "id": "t3", "date": "2026-03-08", "type": "expense", "categoryId": "cat-transport", "accountId": "acc-checking", "amount": 1200, "note": "加油" },
{ "id": "t4", "date": "2026-03-15", "type": "expense", "categoryId": "cat-entertainment", "accountId": "acc-checking", "amount": 890, "note": "電影票" }
],
"budgets": [
{ "id": "b-2026-03-food", "categoryId": "cat-food", "month": "2026-03", "limitAmount": 8000 },
{ "id": "b-2026-03-transport", "categoryId": "cat-transport", "month": "2026-03", "limitAmount": 3000 }
]
}
準備 Mock Data 時,記得涵蓋一些「邊界情況(Edge Case)」:金額特別大的一筆交易、某個類別完全沒有預算、跨月份的資料等等。如果 Mock Data 準備得太理想化(例如每個類別金額都差不多、資料筆數剛好整除),Day 29 實作時很容易漏掉「資料不齊全時圖表該怎麼呈現」的處理邏輯。
原則很簡單:先確定「這張圖表要回答什麼問題」,再對照下表挑選圖表類型,而不是先選一個喜歡的圖表,再硬套資料進去。
| 想呈現的目的 | 建議圖表類型 | Chart.js type |
|---|---|---|
| 呈現數值隨時間的變化趨勢 | 折線圖(Line Chart) | 'line' |
| 比較不同類別之間的數值大小 | 長條圖(Bar Chart) | 'bar' |
| 呈現「部分佔整體」的比例關係(總和等於 100%) | 圓餅圖/環狀圖(Pie/Doughnut) | 'pie' / 'doughnut' |
| 強調單一類別的相對大小,但不要求嚴格加總為 100% | 極座標圖(Polar Area) | 'polarArea' |
| 同時比較多個維度/指標的綜合表現 | 雷達圖(Radar Chart) | 'radar' |
| 呈現兩個數值變數之間的相關性 | 散佈圖(Scatter Chart) | 'scatter' |
| 呈現兩個數值變數的相關性,並用「點的大小」表示第三個維度 | 氣泡圖(Bubble Chart) | 'bubble' |
| 同時呈現「絕對數值」與「趨勢走向」(例如業績金額 + 成長率) | 混合圖表(Mixed Chart:Bar + Line) | 混合設定 |
| 大量時間序列資料點,需要縮放、平移瀏覽細節 | 折線圖 + chartjs-plugin-zoom + decimation |
'line' |
把上面的通用對照表,套用到第 5.2 節拆解出來的每個區塊,就能得到今天規劃、明天直接照做的具體圖表清單:
| 區塊 | 想回答的問題 | 圖表類型 | 對應通用類別 |
|---|---|---|---|
| 收支趨勢區 | 「最近半年收入/支出怎麼變化?」 | 折線圖(Line,含 fill 區域) |
隨時間變化的趨勢 |
| 支出分類區 | 「這個月的錢主要花在哪裡?」 | 環狀圖(Doughnut) | 部分佔整體的比例 |
| 預算比較區 | 「哪些類別已經快要超支?」 | 長條圖(Bar,預算 vs 實際兩個 dataset) | 類別數值比較 |
| 資產配置區 | 「我的資產分散在哪些帳戶?」 | 極座標圖(Polar Area) | 強調相對大小 |
這張清單也回答了一個常見的新手疑問:「同樣是圓形的圖,環狀圖跟極座標圖到底該選哪個?」——環狀圖要求「每個扇形角度總和等於整個圓」,適合呈現嚴格的佔比關係(例如支出分類一定要加總等於 100% 的總支出);極座標圖的每個扇形角度是相等的,只有半徑長度隨數值變化,更適合單純比較「哪個類別數值比較大」,而不強調彼此加總的意義(例如各帳戶餘額,不見得需要被理解成「佔總資產的比例」)。
規劃到這裡,最後一步是把 Day 29 動手實作前需要決定的技術細節先確認好,避免明天寫程式寫到一半還要停下來做架構決策:
<script type="module"> 搭配 ESM CDN(https://cdn.jsdelivr.net/npm/chart.js/+esm)匯入 Chart.js,不需要額外安裝 Vite/Webpack 等打包工具,就能使用 import 語法做模組化拆分(這也是之後「程式碼重構與模組化」要用到的基礎)。fetch 取得資料,同時也能練習 Loading/錯誤處理的完整體驗。規劃出來的專案資料夾結構如下,今天先確定架構,Day 29 會依照這個結構逐一把檔案生出來:
day28-personal-finance-dashboard/
├── server/ # Mock API 後端(Day 29 動手做)
│ ├── package.json
│ ├── server.js
│ └── data/
│ ├── transactions.json # 對應 6.4 節的 Mock Data
│ ├── categories.json
│ ├── accounts.json
│ └── budgets.json
└── public/ # 前端頁面(Day 29 動手做)
├── index.html
├── css/
│ └── style.css # Grid/Flexbox 響應式版面
└── js/
├── main.js # 進入點:呼叫 API、初始化各張圖表
├── api.js # 封裝 fetch,統一處理 Loading/錯誤
├── aggregate.js # 對應 6.3 節的 groupBy/reduce 彙整邏輯
└── charts/
├── trendChart.js # 收支趨勢折線圖
├── categoryChart.js # 支出分類環狀圖
├── budgetChart.js # 預算 vs 實際長條圖
└── allocationChart.js # 資產配置極座標圖
把
charts/資料夾依「一個檔案負責一張圖表」拆分,是刻意為之後「程式碼重構與模組化」預留的設計——今天先把資料夾骨架定下來,之後不管是新增圖表、或是替某張圖表加上效能優化,都能在對應的檔案裡獨立修改,不會互相干擾。
明天(Day 29)我們會依照今天規劃出來的資料結構、圖表配置清單與資料夾架構,正式動手把「個人財務儀表板」實作出來:整合多種圖表類型於單一頁面、加入時間區間切換與篩選器等互動功能,並用 CSS Grid/Flexbox 完成響應式版面配置,把今天畫在紙上的 Wireframe 真正變成一個可以操作的網頁。