iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

30 天手把手學會 Chart.js v4:從圖表基礎到互動式資料視覺化實戰系列 第 30

Day 30 - 30 天手把手學會 Chart.js|專案優化與總結

  • 分享至 

  • xImage
  •  

這是 Chart.js 30 天學習計畫的最後一天。Day 28 我們把「個人財務儀表板」從零想清楚,Day 29 把藍圖真正做成一個能互動的網頁——四張圖表、三種篩選器、CSV 匯出,全部串接完成。今天要做兩件事:第一,回頭檢視這個專案,討論當交易筆數從 70 筆成長到幾千筆時可能遇到的效能瓶頸,學習 Chart.js 內建的效能優化技巧與 decimation(資料抽稀)外掛,並針對 main.js 略顯集中的渲染邏輯做進一步的重構與模組化;第二,把這 30 天學過的所有知識點做一次系統性的總複習,並整理延伸學習資源,作為這趟學習旅程的收尾與下一步的起點。

一、效能優化的第一步:先量測,別用猜的

在動手改任何程式碼之前,必須先建立一個重要觀念:效能優化的第一步永遠是「量測」,不是「猜測」。沒有數據支持的優化,很容易花時間改善一個其實不是瓶頸的地方,卻忽略真正拖慢畫面的原因。

1.1 用 console.time 做最簡單的量測

最快能上手的工具就是瀏覽器內建的 console.time() / console.timeEnd()

console.time('renderTransactionsTable');
renderTransactionsTable();
console.timeEnd('renderTransactionsTable'); // 主控台會印出:renderTransactionsTable: 12.3ms

把這組計時包在懷疑有效能問題的函式外層,就能得到具體的毫秒數,而不是「感覺變慢了」這種模糊的印象。

1.2 用 Chrome DevTools 的 Performance 面板

想看到更完整的畫面,可以打開 Chrome 開發者工具的 Performance 面板,按下錄製、操作一次篩選器切換,再停止錄製,就能看到:

  • Scripting(JavaScript 執行時間,例如 aggregate.jsreducefilter 運算)
  • Rendering(版面計算、重排 reflow)
  • Painting(畫面實際繪製,包含 <canvas> 內容)

三者的時間分佈,就能判斷「慢」的原因究竟是資料運算、DOM 更新,還是圖表繪製本身。

1.3 動手重現問題:把資料量放大

Day 29 動手練習第 2 題已經提示過:把 server/data/generate-mock-data.js 裡的 months 陣列從 6 個月延長成 12 個月,甚至更多年份,重新執行 node generate-mock-data.js 產生更大量的交易資料,就能真實重現「資料變多之後畫面會不會變慢」的情境,而不是憑空想像。建議在正式優化前,先花五分鐘做這個實驗,並用上面兩種方式量測「篩選前」與「篩選後」的耗時,作為本篇後續優化的對照基準(Baseline)。

二、資料量變大時,儀表板的效能瓶頸會出現在哪裡?

把 Day 29 的儀表板放大資料量之後,實務上常見的效能瓶頸可以歸納成下表:

現象 常見原因 對應策略
折線圖資料點暴增後,繪製、互動變得卡頓 每次繪製都要處理所有原始資料點 decimation 外掛、parsing: false、必要時關閉動畫
交易明細表格筆數變多後,切換篩選器有明顯延遲 innerHTML 整包字串重建、大量 DOM 節點同時插入 分頁(Pagination),縮小單次重繪範圍
篩選器切換時圖表閃爍、記憶體持續上升 誤用 chart.destroy() + new Chart() 重建圖表 延續 Day 13 的原則:只呼叫 chart.update()(Day 29 已經這樣做,本篇會再次確認)
專案越改越難維護,main.js 越寫越肥 狀態、DOM 參照、渲染邏輯、事件綁定全部混在同一支檔案 模組化拆分,落實單一職責

值得說明的是:目前儀表板的四張圖表(趨勢、分類、預算、資產配置)資料點都不多——趨勢圖最多 6~12 個月份、分類與預算圖最多 8 個類別、資產配置圖最多 4 個帳戶,這些圖表本身其實還沒有真正的「大量資料點」問題。真正會隨資料量線性成長的,反而是交易明細表格(第六章)。不過為了完整示範 Chart.js 處理大量資料點折線圖的技巧,第五章會延伸一個「假設情境」:如果日後儀表板新增一張「每日資產淨值走勢」圖,資料點可能上看數百甚至數千筆,這時候就是 decimation 外掛真正派上用場的地方。

三、Chart.js 內建的效能技巧總整理

Chart.js 官方文件的 Performance 章節整理了幾項不需要額外外掛、單靠設定就能生效的效能技巧,這裡搭配專案情境逐一說明。

3.1 parsing: false + 提供正規化資料

Chart.js 預設會把你傳入的 data(不論是純數字陣列還是 {x, y} 物件陣列)解析、正規化成內部格式,這個「解析」的過程本身就有運算成本。如果你能自己先把資料整理成 Chart.js 內部期待的格式(依 x 軸遞增排序的 {x, y} 物件),就可以設定 parsing: false 跳過這個步驟:

options: {
  parsing: false, // 資料已經是排序好的 { x, y } 格式,不需要 Chart.js 再解析一次
  normalized: true // 進一步告知 Chart.js:索引唯一、已排序,可以套用更快速的內部演算法
}

aggregate.js 裡的彙整函式本來就會用 Array.prototype.sort 或依照月份順序組資料,這種「資料先處理乾淨、圖表只管畫」的分工,正好是套用 parsing: false 的先決條件。

3.2 資料量大時,考慮關閉動畫

options: {
  animation: false // 大量資料時,關閉動畫可以省下多次重繪的成本,只需要畫一次最終結果
}

動畫預設會讓 Chart.js 在一段時間內(預設約 1 秒)反覆重繪畫面,資料點越多,每一次重繪的成本就越高。如果圖表資料量本來就大,關閉動畫能讓 CPU 的負擔從「畫很多次」降為「只畫一次」。不過要注意:儀表板目前的四張圖表資料點都不多,動畫帶來的視覺回饋(例如切換篩選器時長條圖的漸變效果)對使用者體驗是加分的,不需要為了「预防性优化」而关闭——這一點會在第九章的常見誤區再次強調。

3.3 spanGaps:省去線段分段的判斷

options: {
  spanGaps: true // 資料中間即使有 null/缺漏值,也直接連成一條線,不用另外判斷斷點分段
}

如果資料集本身有缺漏值(null),Chart.js 預設會把線段從缺漏處斷開,形成好幾條獨立線段;開啟 spanGaps 可以省去這個分段判斷,用一條完整的線直接跨過缺漏點。這對資料量大、又允許「用直線帶過缺漏」的圖表(例如趨勢觀察用途)能帶來效能提升,但要注意 null 代表「沒有預算資料」不該被畫出來是不同情境,必須依照圖表想傳達的意義決定要不要開啟

3.4 減少繪製內容:showLine: falsepointRadius: 0

datasets: [{
  showLine: false, // 只畫點,不畫連接線(適合純粹想呈現「分佈」而非「趨勢」的情境)
  // 或者
  pointRadius: 0    // 只畫線,不畫資料點圓圈(資料點很密集時,圓圈幾乎重疊,畫了也看不出來)
}]

資料點數量很多時,同時畫「線」又畫「一堆密密麻麻的圓點」是浪費運算資源的——使用者根本看不清楚每一個點,不如二選一。

3.5 ticks.sampleSize:加速刻度標籤的尺寸計算

scales: {
  x: {
    ticks: {
      sampleSize: 20 // 只抽樣 20 個標籤來估算文字寬度,而不是量測全部標籤
    }
  }
}

Chart.js 在決定「這個座標軸要顯示幾條刻度線、要不要旋轉文字」之前,需要先量測標籤文字的寬度。標籤數量很多時,量測全部標籤本身就是一筆開銷;sampleSize 讓 Chart.js 只抽樣一部分標籤估算,適合標籤長度差異不大的情境。

3.6 固定 scalesmin / max:省去每次重新計算範圍

scales: {
  y: {
    min: 0,
    max: 200000 // 明確指定範圍,Chart.js 就不需要每次都重新掃描全部資料算最大最小值
  }
}

Chart.js 預設會依照目前的資料自動算出座標軸的最大最小值,資料量大或更新頻繁時,這個「重新掃描」的過程會反覆發生。如果你的業務情境本來就有明確的合理範圍(例如財務儀表板的月支出很少超過某個上限),直接寫死 min / max,能省下這筆重複運算。

3.7 折線圖的「自動抽稀」:不需要設定就內建生效

Chart.js 的線段元素(Line Element)有一項容易被忽略的內建優化:只要 tensionsteppedborderDash 都維持預設值(分別是 false0[]),繪製時就會自動略過畫面上看不見的線段,藉此加速渲染。這意味著:如果你為了美觀把 tension 設成 0.3(曲線平滑,Day 5 教過的技巧),就會失去這項自動優化——這是「視覺效果」與「效能」之間常見的取捨,資料量小的時候(例如目前的趨勢折線圖)完全不需要在意,但資料量真的很大時就要納入考量。

四、decimation 外掛實戰:處理上千個資料點的折線圖

4.1 什麼時候該用 decimation?

decimation(資料抽稀)外掛的用途,是在折線圖繪製之前,先把過多的資料點依照演算法縮減成「肉眼看起來幾乎一樣、但運算量小很多」的資料集。官方文件很直白地點出了它的核心理念:

一張只有幾百像素寬的圖表,畫上好幾萬個資料點是沒有意義的——使用者根本分辨不出來,不如先抽稀再畫。

decimation 是 Chart.js 核心內建的外掛之一(跟 LegendTooltipTitle 同一個等級),並不需要另外安裝套件;Day 2 教過的 Chart.register(...registerables) 已經把它一併註冊好了,只需要在 options.plugins.decimation 打開 enabled: true 即可使用。

4.2 使用前必須符合的限制條件

decimation 外掛並不是所有折線圖都能套用,官方文件列出六項明確的先決條件:

  1. 資料集的 indexAxis 必須是 'x'(也就是預設的直立折線圖,不是橫向的)。
  2. 資料集的類型必須是 line
  3. x 軸的類型必須是 'linear''time'——'category' 軸不支援
  4. 資料不能需要額外解析,也就是必須搭配 parsing: false
  5. dataset 物件必須是可變動的:外掛會把原始資料存成 dataset._data,再另外定義一個新的 data 屬性存放抽稀後的資料。
  6. 圖表上的資料點數量必須超過 threshold(門檻值)才會真的觸發抽稀,否則資料量不夠多,維持原樣即可。

⚠️ 對照這份清單就會發現:Day 29 的收支趨勢折線圖(trendChart.js目前並不符合條件——它的 labels['2025-10', '2025-11', ...] 這種月份字串陣列,形成的是 'category' 類型座標軸,而不是 'linear''time'。再加上它最多也只有 12 個資料點,遠遠稱不上「大量資料」,硬套用 decimation 不會有任何效果,也沒有必要。真正適合示範 decimation 的情境,是下一節延伸出來的「每日資產淨值走勢」圖。

4.3 延伸情境:新增一張「近一年每日資產淨值走勢」圖

假設這個記帳 App 未來串接了銀行的 Open Banking API,開始累積「每天」的資產淨值快照——一年就有 365 筆資料,三年就破千筆。這正是 decimation 大展身手的場景。以下示範如何新增這張圖表模組 charts/netWorthChart.js

// charts/netWorthChart.js
// 延伸範例:近一年每日資產淨值走勢,資料點數量遠多於現有四張圖表,
// 用來示範 Day 6 學過的 time 軸 + decimation 外掛的完整搭配。
import { Chart } from '../chartSetup.js';
import 'https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/+esm';

function buildConfig(dailyNetWorth) {
  // dailyNetWorth 是已經依日期由舊到新排序好的 [{ x: '2026-01-01', y: 812000 }, ...]
  return {
    type: 'line',
    data: {
      datasets: [
        {
          label: '每日資產淨值',
          data: dailyNetWorth,
          borderColor: '#3b82f6',
          borderWidth: 1.5,
          pointRadius: 0, // 資料點太密集,圓圈幾乎重疊,只畫線即可
          tension: 0       // 保留自動抽稀優化,不使用曲線平滑
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      parsing: false,   // 資料已經是排序好的 {x, y},符合 decimation 的先決條件
      normalized: true,
      animation: false, // 資料點多,關閉動畫換取流暢的縮放/平移體驗
      interaction: { mode: 'nearest', axis: 'x', intersect: false },
      plugins: {
        decimation: {
          enabled: true,
          algorithm: 'min-max', // 保留波峰波谷(例如某天有大額支出造成的淨值驟降)
          samples: 500           // 若改用 'lttb' 演算法,這裡可指定輸出的取樣點數
        }
      },
      scales: {
        x: {
          type: 'time',
          time: { unit: 'month' },
          ticks: { source: 'auto', maxRotation: 0, autoSkip: true } // 減少刻度計算成本
        },
        y: { ticks: { callback: (value) => `${(value / 10000).toLocaleString()}萬` } }
      }
    }
  };
}

export function createNetWorthChart(canvas, dailyNetWorth) {
  return new Chart(canvas, buildConfig(dailyNetWorth));
}

export function updateNetWorthChart(chart, dailyNetWorth) {
  chart.data.datasets[0].data = dailyNetWorth;
  chart.update();
}

這段程式碼一次整合了本篇前面幾節的技巧:type: 'time' 搭配 chartjs-adapter-date-fns(Day 6 學過)滿足 decimation 的座標軸限制、parsing: false 搭配已排序資料、animation: falsepointRadius: 0 減少繪製成本、最後才是 plugins.decimation 本身的設定。

4.4 兩種抽稀演算法怎麼選?

演算法 特性 適合情境
'min-max' 每個像素最多保留 4 個點(該區間的最大值、最小值等),能完整保留資料的波峰與波谷 想觀察「異常尖峰」的情境,例如本例的每日淨值——如果某天有一筆大額支出造成淨值驟降,min-max 能確保這個驟降依然會被畫出來,不會被抽稀抹平
'lttb'(Largest Triangle Three Buckets) 大幅減少資料點數量,用固定的 samples 數量呈現資料的整體形狀與趨勢 只想呈現長期走勢、不特別在意單點細節的情境,例如「近三年資產成長趨勢」這種宏觀視角的圖表

threshold 選項則是決定「資料點數量超過多少才觸發抽稀」,預設是畫布寬度的 4 倍——換句話說,資料點數量沒有明顯超過畫布可以顯示的像素數量時,decimation 不會有任何動作,這也呼應了「沒必要抽稀就不抽稀」的設計理念。

五、表格效能:從「整包重繪」到「分頁處理」

5.1 問題根源:innerHTML 整包字串重建

回顧 Day 29 的 renderTransactionsTable()

transactionsTableBody.innerHTML = filtered
  .slice()
  .sort((a, b) => (a.date < b.date ? 1 : -1))
  .map((tx) => `<tr>...</tr>`)
  .join('');

每次篩選條件改變,這段程式碼都會把符合條件的全部交易轉成一大串 HTML 字串,整包塞進 innerHTML。交易筆數是 70 筆時完全感覺不出差異,但當資料量成長到幾千筆,瀏覽器需要一次性解析、建立、插入數千個 <tr> DOM 節點,畫面明顯會卡頓一下。

5.2 解法:分頁(Pagination)

最直接、CP 值最高的解法是分頁:畫面上一次只渲染「一頁」的資料量(例如 20 筆),而不是把符合條件的全部資料一次塞進表格。

// aggregate.js 新增:純函式,只負責「切出某一頁的資料」
export function paginate(items, page, pageSize) {
  const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
  const safePage = Math.min(Math.max(1, page), totalPages);
  const start = (safePage - 1) * pageSize;
  return {
    pageItems: items.slice(start, start + pageSize),
    currentPage: safePage,
    totalPages,
    totalCount: items.length
  };
}
// main.js:renderTransactionsTable 改為只渲染「目前頁次」的資料
const PAGE_SIZE = 20;

function renderTransactionsTable() {
  const filtered = getFilteredTransactions().slice().sort((a, b) => (a.date < b.date ? 1 : -1));
  const { pageItems, currentPage, totalPages, totalCount } = paginate(filtered, state.filters.page, PAGE_SIZE);

  transactionCountEl.textContent = String(totalCount);
  transactionsTableBody.innerHTML = pageItems.map(renderTransactionRow).join('');
  renderPaginationControls(currentPage, totalPages);
}

💡 別忘了一個很容易漏掉的細節:每次篩選條件(時間區間、類別、關鍵字)改變時,都必須把 state.filters.page 重設回 1。否則使用者原本停留在第 5 頁,篩選之後總筆數變少、只剩 2 頁,畫面就會顯示一個空表格——這是分頁功能最常見的邊界情況錯誤,第九章會再次提醒。

5.3 更進階的方案:只維持提示,不強求實作

資料量如果成長到數萬筆以上,分頁可能還是不夠,這時業界常見的做法是 虛擬捲動(Virtual Scrolling):畫面上永遠只實際渲染「使用者看得到的那幾十列」DOM 節點,捲動時動態抽換內容,其餘資料完全不建立 DOM。這已經超出 Chart.js 的範疇、進入表格元件庫的領域(例如 TanStack Virtualreact-window 等),這裡先點出概念,讓大家知道「分頁不是唯一解法,資料量夠大時還有更進階的技巧」,有興趣可以自行深入研究。

六、程式碼重構與模組化:main.js 瘦身計畫

6.1 現況問題:一支檔案,四種責任

Day 29 的 main.js 雖然已經把「彙整邏輯」(aggregate.js)與「畫圖邏輯」(charts/*.js)拆出去,但檔案本身依然同時扛了四種不同性質的責任:

  1. 全域狀態statecharts 物件)
  2. DOM 參照(一長串 document.getElementById(...)
  3. 渲染邏輯renderAllrenderSummaryCardsrenderTrendChart 等)
  4. 事件綁定rangeSelect.addEventListener(...) 等)

檔案不算長(不到 200 行)的時候還讀得下去,但可以想像:如果之後要加上淨值走勢圖、分頁控制、更多篩選器,這支檔案只會越來越肥。這正是**單一職責原則(Single Responsibility Principle)**要解決的問題——一個模組應該只有一個「改變的理由」。

6.2 拆分方向:把四種責任拆成四個模組

public/js/
├── store.js       # 全域狀態 + 簡單的訂閱/通知機制(Observer Pattern)
├── dom.js         # 集中管理所有 DOM 節點參照
├── renderers.js   # 純粹的「讀狀態 → 算資料 → 更新畫面」渲染函式
├── events.js       # 統一綁定使用者互動事件,呼叫 store 的方法更新狀態
├── aggregate.js   # 不變:彙整邏輯(Day 29 已經拆好)
├── chartSetup.js  # 不變:Chart.js 註冊與全域預設值(Day 29 已經拆好)
├── charts/        # 不變:四張圖表模組
└── main.js        # 瘦身後:只負責「串起以上模組、呼叫 init()」

6.3 store.js:狀態集中管理 + 訂閱通知

// store.js
// 集中管理儀表板的全域狀態,並提供簡單的訂閱/通知機制(Observer Pattern):
// 任何模組只要呼叫 subscribe() 註冊回呼函式,state 一旦透過 setFilters() 改變,
// 就會自動通知所有訂閱者重新渲染,不需要在每個事件監聽器裡手動列出「這次要更新哪些畫面」。
const state = {
  categories: [],
  accounts: [],
  transactions: [],
  budgets: [],
  filters: { range: 'month', categoryId: 'all', keyword: '', page: 1 }
};

const listeners = new Set();

export function getState() {
  return state;
}

export function setData(data) {
  Object.assign(state, data);
  notify();
}

export function setFilters(patch) {
  // 除了 page 之外的任何篩選條件改變,一律把頁碼重設回第 1 頁
  const resetPage = 'page' in patch ? {} : { page: 1 };
  Object.assign(state.filters, patch, resetPage);
  notify();
}

export function subscribe(listener) {
  listeners.add(listener);
  return () => listeners.delete(listener); // 回傳一個「取消訂閱」函式,方便日後擴充
}

function notify() {
  listeners.forEach((listener) => listener(state));
}

6.4 renderers.js:只管「畫」,不管「怎麼被觸發」

// renderers.js
import { getState } from './store.js';
import { filterByRange, aggregateCategoryBreakdown, /* ... */ } from './aggregate.js';
import { createCategoryChart, updateCategoryChart } from './charts/categoryChart.js';
import { categoryCanvas } from './dom.js';

const charts = { category: null /* ...其餘圖表 */ };

export function renderCategoryChart(onClickCategory) {
  const state = getState();
  const rangeFiltered = filterByRange(state.transactions, state.filters.range);
  const breakdown = aggregateCategoryBreakdown(rangeFiltered, state.categories);

  if (!charts.category) {
    charts.category = createCategoryChart(categoryCanvas, breakdown, onClickCategory);
  } else {
    updateCategoryChart(charts.category, breakdown, state.filters.categoryId);
  }
}

6.5 main.js:瘦身後只剩「組裝」的責任

// main.js(重構後)
import { fetchDashboardData } from './api.js';
import { setData, subscribe } from './store.js';
import { renderAll } from './renderers.js';
import { bindEvents } from './events.js';
import { statusText } from './dom.js';

async function init() {
  try {
    const data = await fetchDashboardData();
    setData(data);
    statusText.textContent = `資料載入完成,共 ${data.transactions.length} 筆交易紀錄。`;
  } catch (error) {
    statusText.textContent = `資料載入失敗:${error.message}`;
  }
}

subscribe(renderAll); // 狀態一改變,自動觸發重新渲染,事件綁定不需要知道「該重新畫哪些圖」
bindEvents();
init();

6.6 這樣拆分帶來什麼好處?

  • 事件綁定不需要知道「該重新渲染哪些畫面」:Day 29 的 rangeSelect.addEventListener 內要手動列出「時間區間改變會影響卡片、分類圖、預算圖、表格」,重構後 events.js 只需要呼叫 setFilters({ range: ... }),剩下的交給 subscribe(renderAll) 統一處理,新增一張圖表時也不必回頭修改每一個事件監聽器。
  • 狀態變更有唯一入口:所有修改 state.filters 的地方都必須經過 setFilters(),好處是可以在同一個地方統一處理「重設頁碼」這種跨欄位的邊界情況,不用擔心某個事件監聽器忘記重設。
  • 各模組職責單純、容易單獨測試aggregate.js 本來就是純函式,現在 store.js 的狀態邏輯也不依賴 DOM,可以脫離瀏覽器環境單獨測試。

七、總複習:Chart.js 30 天知識地圖

這趟旅程從一個 <canvas> 元素開始,一路走到跨框架整合與效能優化。以下依照週次,把 30 天的學習重點濃縮成一張知識地圖,方便日後快速查找複習:

第一週:Chart.js 基礎入門(Day 1~7)

Day 主題 一句話重點
1 認識 Chart.js Chart.js 的定位、三種安裝方式(CDN/npm/ESM),畫出第一張圖
2 基本結構 new Chart(ctx, config) 三大設定 typedataoptionsregisterables 與 Tree-shaking 註冊機制
3 長條圖 單/多資料集、indexAxis 水平長條圖、顏色與圓角設定
4 圓餅/環狀圖 Pie 與 Doughnut 的差異、cutout 屬性、圖例顯示
5 折線圖進階 多線比較、fill 區域圖、tension 曲線平滑
6 座標軸與時間序列 linearcategorytime 軸、雙 Y 軸、chartjs-adapter-date-fnschartjs-plugin-zoom
7 第一週總複習 綜合實作「天氣溫度折線圖」與「成績長條圖」

第二週:常用圖表類型深入(Day 8~14)

Day 主題 一句話重點
8 雷達圖 多維度能力值比較,angleLinespointLabels 設定
9 極座標圖 與 Pie 的差異(角度相等、半徑代表數值),適合「比大小」而非「比佔比」
10 氣泡圖/散佈圖 三維資料(x, y, r)呈現,適合相關性與分布觀察
11 混合圖表 同畫布結合 Bar + Line,type 可設在 dataset 層級
12 圖例與 Tooltip 自訂圖例位置/點擊事件、tooltip.callbacks 客製化內容
13 動畫效果 animationanimationstransitions 設定、逐步顯示、update() 動態更新
14 第二週總複習 綜合實作「業績儀表板」混合圖表

第三週:資料處理與互動功能(Day 15~21)

Day 主題 一句話重點
15 串接靜態 JSON fetch 讀本地 JSON,轉換成 Chart.js 資料格式
16 串接後端 API RESTful 串接、async/await、loading 狀態處理
17 串接 CSV PapaParse 解析、資料清洗(filtermapreduce
18 動態資料更新 setInterval + update()、滑動視窗(Sliding Window)、update('none')
19 互動事件處理 onClickonHovergetElementsAtEventForMode、圖表連動
20 響應式設計 responsivemaintainAspectRatio、專屬容器規則、min-width: 0 陷阱
21 第三週總複習 串接公開 API 製作即時互動圖表

第四週:外掛開發與框架整合(Day 22~27)

Day 主題 一句話重點
22 外掛系統基礎 Plugin 架構、生命週期 hook(beforeDrawafterDraw 等)、chartjs-plugin-datalabels
23 自訂外掛開發 撰寫 beforeDrawafterDraw 外掛,實作浮水印與中央標籤
24 主題與樣式客製化 Chart.defaults 全域設定、深色模式、Scriptable Options、漸層背景
25 與 React 整合 react-chartjs-2、元件化、useStateuseEffect
26 與 Vue 整合 vue-chartjs、Vue 3 Composition API、響應式資料綁定
27 與 Angular 整合 ng2-chartsprovideCharts(withDefaultRegisterables())<canvas baseChart>ChangeDetectorRef

收尾週:綜合實戰專案(Day 28~30)

Day 主題 一句話重點
28 專案規劃與資料設計 User Story、MoSCoW、Wireframe、Entity 設計、依目的挑圖表
29 專案實作 整合四張圖表、CSS Grid/Flexbox 響應式版面、三種互動功能、CSV 匯出
30 專案優化與總結 效能量測、decimation 外掛、表格分頁、模組化重構、全課程總複習

看著這張地圖回顧,可以發現一條清楚的學習脈絡:先學會「畫出一張圖」(第一週)→ 認識「不同資料形狀該用什麼圖表」(第二週)=> 讓圖表「動起來、connect 真實資料」(第三週)=> 讓圖表「融入真實專案與團隊技術棧」(第四週)=> 最後用一個完整專案把所有技能串起來(收尾週)。這也是為什麼 Day 28~30 刻意選擇「個人財務儀表板」這種綜合性題目——它幾乎用到了前 27 天教過的每一項技巧。

八、結語:30 天的終點,也是下一個起點

從 Day 1 那張最簡單的折線圖,到今天這個能篩選、能連動、能匯出、還懂得照顧效能與程式碼品質的個人財務儀表板,這趟旅程走過了 Chart.js 幾乎所有的核心能力:圖表類型的選擇、資料的清洗與彙整、互動與動畫的設計、外掛系統的擴充,以及與 React/Vue/Angular 三大框架的整合方式。

但更重要的,其實不是記住每一個 API 怎麼寫——那些永遠可以查官方文件;真正帶得走的,是面對一份資料時,知道該怎麼問對問題、選對圖表、想清楚每個篩選條件該套用在哪裡、以及在專案還小的時候就養成模組化與效能意識的習慣。這些思考方式,換到任何一個圖表庫、任何一個前端框架,都依然適用。

30 天的學習在這裡告一段落,但資料可視化這條路才剛開始——下一步,找一份你真正關心的資料(可能是工作上的報表、興趣相關的統計、或是任何讓你好奇的數字),把這 30 天學到的東西實際用上去,做出屬於你自己的作品。祝你在資料可視化的路上,走得又快又穩。


上一篇
Day 29 - 30 天手把手學會 Chart.js|專案實作
系列文
30 天手把手學會 Chart.js v4:從圖表基礎到互動式資料視覺化實戰30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言