資料視覺化 (I) – Chart.js 基礎:讓數據說話
在上一堂課中,我們學會了如何撰寫 HTML 架構並透過 DOM 與網頁互動。對於資料科學家來說,處理完龐大的數據後,最重要的步驟就是「展現結果」。
這堂課我們將進入 資料視覺化(Data Visualization 的領域。我們將使用 Chart.js,這是一個強大且靈活的 JavaScript 函式庫,能幫助我們將枯燥的 JSON 數據瞬間轉化為精美的長條圖、折線圖或圓餅圖。
1. 基礎
1.1 為什麼選擇 Chart.js?
在網頁上繪圖有很多選擇(如 D3.js, Google Charts),但 Chart.js 特別適合作為資料科學初學者的首選,原因如下:
- 基於 HTML5 Canvas:渲染效能好,且能自適應各種螢幕尺寸(RWD)。
- 語法直觀:透過簡單的 JavaScript 物件(Object)配置即可生成圖表,不需要像 D3.js 那樣從零刻畫幾何圖形。
- 豐富的內建樣式:預設的配色和動畫就已經相當美觀,適合快速展示分析結果。
1.2 準備工作:在 HTML 中引入 Chart.js
還記得我們上週寫的 HTML 樣板嗎?我們將繼續使用它。要使用 Chart.js,最簡單的方法是透過 CDN (Content Delivery Network) 直接引入。
請建立一個新的檔案 index.html,並貼上以下內容:
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的資料視覺化儀表板</title>
</head>
<body>
<div style="width: 800px; margin: 50px auto;">
<canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 我們稍後會在這裡寫程式
</script>
</body>
</html>關鍵點解析:
<canvas>標籤:這是圖表的畫布。Chart.js 會抓取這個元素,並在上面繪製圖形。- CDN 連結:
<script src="...">這行程式碼會從網路下載 Chart.js 的所有功能,讓我們可以直接使用Chart這個全域物件。
1.3 實作:繪製你的第一張長條圖
假設我們有一組簡單的資料:某班級 5 位學生在「程式設計導論」的期中考成績。
- 學生:小明, 小華, 小美, 阿強, 這裡
- 分數:85, 72, 90, 65, 88
讓我們把這組數據變成圖表。請將以下程式碼填入 index.html 中的 <script> 區塊內:
// 取得 Canvas 元素
const ctx = document.getElementById('myChart');
// 建立一個新的 Chart 物件
new Chart(ctx, {
type: 'bar', // 圖表類型:長條圖
data: {
labels: ['小明', '小華', '小美', '阿強', '杰倫'], // X 軸標籤
datasets: [{
label: '期中考成績', // 圖例名稱
data: [85, 72, 90, 65, 88], // Y 軸數值 (資料本體)
borderWidth: 1,
backgroundColor: 'rgba(54, 162, 235, 0.5)', // 長條圖顏色
borderColor: 'rgba(54, 162, 235, 1)' // 邊框顏色
}]
},
options: {
scales: {
y: {
beginAtZero: true // Y 軸從 0 開始
}
}
}
});存檔後用瀏覽器打開,你應該會看到一張互動式的藍色長常久。條圖。試著將滑鼠移到長條上,你會發現它內建了 Tooltip(提示框)功能!
1.4 解構 Chart.js 的核心設定
剛剛的程式碼雖然短,但包含了一個 Chart.js 圖表最重要的三個靈魂:Type, Data, Options。
A. Type (圖表類型)
決定資料呈現的形式。
'bar': 長條圖 (適合比較各類別的大小)'line': 折線圖 (適合呈現隨時間變化的趨勢)'pie': 圓餅圖 (適合呈現佔比)
B. Data (資料結構)
這是資料科學家最常操作的部分。
labels: 這是 X 軸的標籤陣列。這對應到我們之前學過的陣列(Array)概念。datasets: 這是一個物件陣列。為什麼是陣列?因為一張圖表可以包含多組數據(例如:同時顯示「期中考」與「期末考」兩組長條)。data: 實際的數值陣列,長度通常要跟labels一樣。backgroundColor: 設定顏色。
C. Options (詳細配置)
用來微調圖表的外觀和行為。
scales: 控制 X 軸與 Y 軸的刻度。例如beginAtZero: true確保 Y 軸從 0 開始,避免誤導讀者以為分數差異巨大(如果從 60 開始,65 跟 85 看起來會差三倍)。plugins: 控制標題、圖例(Legend)等附加元件(我們會在下一小時深入介紹)。
1.5 小練習:切換視角
現在,試著修改你的程式碼,觀察圖表的變化:
- 將
type: 'bar'改為type: 'line'。- 觀察:資料沒變,但呈現方式變成了趨勢圖。這適合用在成績嗎?可能不適合,但如果是「每週氣溫」就很適合。
- 在
datasets裡新增一組資料:{ label: '期末考成績', data: [92, 88, 95, 70, 90], backgroundColor: 'rgba(255, 99, 132, 0.5)', borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 1 }- 觀察:Chart.js 會自動幫你把兩組長條並排顯示,方便比較。
2. 動態或取資料
在上一堂課,我們成功畫出了第一張長條圖,但數據是「寫死」在程式碼裡的。這堂課我們要模擬真實開發情境:如何處理從後端傳來的 JSON 資料(Object Array),並將其動態渲染到圖表上。
此外,我們也會介紹資料科學中最常用的另一種圖表:散佈圖 (Scatter Plot),用來觀察變數之間的相關性。
2.1 資料轉換:從 JSON 到 Arrays
Chart.js 的 data 屬性通常需要兩個獨立的陣列:一個給 labels (X軸),一個給 datasets.data (Y軸)。然而,我們在 Node.js 中處理的資料通常是「物件陣列 (Array of Objects)」。
核心技巧:Array.map()
這是資料科學家在 JavaScript 中最常用的函式之一。
假設後端 API 回傳了以下格式的銷售資料:
const apiResponse = [
{ month: "一月", revenue: 15000, cost: 5000 },
{ month: "二月", revenue: 23000, cost: 8000 },
{ month: "三月", revenue: 18000, cost: 6000 },
// ... 更多資料
];我們不能直接把 apiResponse 丟給 Chart.js。我們需要把它「拆解」:
// 提取月份作為 X 軸標籤
const labels = apiResponse.map(item => item.month);
// 結果: ["一月", "二月", "三月"]
// 提取營收作為 Y 軸數據
const revenueData = apiResponse.map(item => item.revenue);
// 結果: [15000, 23000, 18000]2.2 實作:模擬非同步資料載入與圖表更新
讓我們修改 index.html。這次我們不一開始就畫圖,而是模擬一個「載入資料」的過程。
請將原本的 <script> 內容替換為以下程式碼:
const ctx = document.getElementById('myChart');
let myChart = null; // 先宣告一個變數來存圖表實例
// 1. 定義一個初始化圖表的函式
function initChart(labels, dataPoints) {
// 如果圖表已經存在,先銷毀它 (這是重繪圖表的重要步驟)
if (myChart) {
myChart.destroy();
}
myChart = new Chart(ctx, {
type: 'line', // 這次我們用折線圖
data: {
labels: labels,
datasets: [{
label: '月營收趨勢',
data: dataPoints,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1, // 線條平滑度,0 為直線
fill: false
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '2025 年度營收報表' // 圖表標題
}
}
}
});
}
// 2. 模擬從伺服器抓取資料 (Fetch Data)
function fetchData() {
console.log("正在從伺服器載入資料...");
// 模擬網路延遲 1.5 秒
setTimeout(() => {
// 假設這是從後端撈回來的 JSON
const rawData = [
{ month: "Jan", amount: 120 },
{ month: "Feb", amount: 190 },
{ month: "Mar", amount: 30 },
{ month: "Apr", amount: 50 },
{ month: "May", amount: 20 },
{ month: "Jun", amount: 300 },
];
// 資料清洗 (Data Wrangling)
const x_labels = rawData.map(d => d.month);
const y_data = rawData.map(d => d.amount);
// 呼叫繪圖函式
initChart(x_labels, y_data);
console.log("資料載入完成!");
}, 1500);
}
// 3. 執行資料載入
fetchData();關鍵邏輯解析
myChart.destroy(): Chart.js 是一個基於 Canvas 的繪圖庫。如果你想在同一個 Canvas 上畫新圖,必須先「銷毀」舊的實例,否則滑鼠互動事件會重疊錯亂(這是在 Single Page Application 開發中常見的 Bug)。- 分離「數據層」與「表現層」:
fetchData負責處理資料邏輯,initChart只負責畫圖。這是良好的程式架構。
2.3 進階圖表:散佈圖 (Scatter Plot)
在資料科學(特別是迴歸分析)中,我們常需要看兩個變數之間的關係(例如:廣告費用 vs 銷售額)。這時候 labels + data 的結構就不適用了,我們需要 x 與 y 的座標點。
Chart.js 的散佈圖資料格式如下:
data: [
{ x: 10, y: 20 },
{ x: 15, y: 10 },
// ...
]常久。實作練習
請在剛剛的程式碼下方,嘗試建立一個新的 canvas (給它 id="scatterChart"),並嘗試繪製散佈圖:
// 在 HTML 增加 <canvas id="scatterChart"></canvas>
const ctxScatter = document.getElementById('scatterChart');
new Chart(ctxScatter, {
type: 'scatter',
data: {
datasets: [{
label: '廣告投入 vs 轉換率',
data: [
{ x: 100, y: 5 },
{ x: 200, y: 12 },
{ x: 300, y: 15 },
{ x: 400, y: 25 },
{ x: 150, y: 8 },
],
backgroundColor: 'rgb(255, 99, 132)'
}]
},
options: {
scales: {
x: {
type: 'linear', // 散佈圖的 X 軸必須是線性數值,不能是文字標籤
position: 'bottom',
title: {
display: true,
text: '廣告費用 (USD)'
}
},
y: {
title: {
display: true,
text: '轉換率 (%)'
}
}
}
}
});2.4 讓圖表更專業:Plugins 配置
Chart.js 的功能大都模組化在 plugins 下。在上面的散佈圖範例中,我們其實已經用到了。
常見的設定包含:
plugins.legend: 控制圖例。position: 'bottom'(放到底部)display: false(隱藏圖例)
plugins.tooltip: 當滑鼠移過去時顯示的資訊框。我們可以自定義回傳的字串(Callback),這在顯示複雜數據時很有用。plugins.title: 圖表的大標題。
options: {
plugins: {
legend: {
position: 'right', // 圖例改到右邊
labels: {
font: {
size: 14 // 調整字體大小
}
}
},
title: {
display: true,
text: '行銷成效分析'
}
}
}3. 進階儀表板:複合圖表與互動偵測
在真實的商業分析中,單一維度的圖表往往不足以解釋複雜的現象。
想像你是電商平台的資料分析師,你需要在一張圖上同時展示:
- 每月銷售總額(數值很大,例如 100,000 ~ 500,000)
- 毛利率(數值很小,例如 0.1 ~ 0.5,即 10% ~ 50%)
如果你把它們畫在同一個 Y 軸上,毛利率的那條線會因為數值太小貼在底部變成一條直線,完全看不出波動。這時候我們需要雙軸複合圖表 (Dual Axis Mixed Chart)。
3.1 實作:長條圖與折線圖
Chart.js 允許在同一個 canvas 上混合不同的圖表類型。我們只需要在個別的 dataset 中指定 type 即可。
請建立或清空你的 <script> 區塊,貼上以下程式碼:
const ctx = document.getElementById('myChart');
const mixedChart = new Chart(ctx, {
data: {
labels: ['一月', '二月', '三月', '四月', '五月'],
datasets: [
{
type: 'bar', // 第一組數據:長條圖
label: '銷售總額 (元)',
data: [150000, 230000, 180000, 320000, 290000],
backgroundColor: 'rgba(54, 162, 235, 0.6)',
yAxisID: 'y', // 對應左側 Y 軸
order: 2 // 圖層順序:數字越大越在下層
},
{
type: 'line', // 第二組數據:折線圖
label: '毛利率 (%)',
data: [25, 30, 28, 15, 20], // 注意這裡數值很小
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 3,
tension: 0.4, // 讓線條平滑一點
yAxisID: 'y1', // 關鍵:對應右側 Y 軸
order: 1 // 數字小,會蓋在長條圖上面
}
]
},
options: {
responsive: true,
scales: {
y: { // 左側 Y 軸設定
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: '金額 (TWD)' }
},
y1: { // 右側 Y 軸設定
type: 'linear',
display: true,
position: 'right',
grid: {
drawOnChartArea: false // 移除右軸的格線,避免畫面太亂
},
title: { display: true, text: '百分比 (%)' },
ticks: {
callback: function(value) {
return value + '%'; // 幫刻度加上 % 符號
}
}
}
},
plugins: {
title: {
display: true,
text: '2025 銷售額與毛利分析 (雙軸圖表)'
}
}
}
});關鍵技術解析
yAxisID: 這是最重要的設定。我們在datasets裡告訴 Chart.js 哪一組資料要對齊哪一個軸 ('y' 或 'y1')。scales配置: 在options中,我們分別定義了y(左) 和y1(右) 的位置與樣式。grid.drawOnChartArea: false: 這是一個美學細節。如果不關閉右軸的格線,圖表上會有兩套格線交錯,看起來非常凌亂。通常我們只保留主軸(左軸)的格線。
3.2 讓圖表活起來:點擊事件 (Event Handling)
資料科學的價值在於「探索」。使用者看到圖表異常(例如四月毛利大跌),通常會想「點下去」看發生了什麼事。
Chart.js 提供了 onClick 事件,讓我們可以捕捉使用者的點擊行為。
請修改上面的 options 物件,加入 onClick 事件處理器:
options: {
// ... (原本的 scales 設定保留) ...
onClick: (e, elements, chart) => {
// 1. 檢查是否有點擊到任何資料點
if (elements.length > 0) {
// 2. 取得被點擊的第一個元素資訊
const element = elements[0];
const datasetIndex = element.datasetIndex; // 點到哪一組資料 (0是長條, 1是折線)
const dataIndex = element.index; // 點到第幾個月份 (0~4)
// 3. 從 chart 物件中撈出實際數值
const datasetLabel = chart.data.datasets[datasetIndex].label;
const labelName = chart.data.labels[dataIndex];
const value = chart.data.datasets[datasetIndex].data[dataIndex];
// 4. 執行互動邏輯 (這裡簡單用 alert 示範,實務上通常是跳轉頁面或打開 Modal)
alert(`你點擊了:${labelName}\n${datasetLabel}: ${value}`);
// 進階應用:你可以在這裡呼叫 fetch() 去後端撈該月份的詳細交易清單
// getDetailData(labelName);
}
},
plugins: {
// ... (原本的 plugins 設定)
}
}試著點擊圖表上的長條或折線上的點,瀏覽器應該會彈出對應的數值視窗。
3. 視覺優化:漸層填色 (Gradient Fill)
作為課程的最後一個技巧,我們要讓圖表看起來更具「科技感」。純色填充雖然清楚,但漸層色能讓圖表更有質感。這需要用到 Canvas原本的 API。
我們可以在建立 Chart 之前,先建立一個漸層物件。
// 在 new Chart 之前
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(54, 162, 235, 0.8)'); // 上方顏色 (較深)
gradient.addColorStop(1, 'rgba(54, 162, 235, 0.1)'); // 下方顏色 (較淺)
// 修改 datasets 中的 backgroundColor
// datasets: [{
// type: 'bar',
// backgroundColor: gradient, // 將原本的顏色字串換成這個變數
// ...
// }]這個小技巧能讓你的長條圖看起來像是一個立體的數據柱,在展示給非技術背景的主管或客戶看時,這種視覺優化往往能增加報告的說服力。