Back
PQCHub

資料視覺化 (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 小練習:切換視角

現在,試著修改你的程式碼,觀察圖表的變化:

  1. type: 'bar' 改為 type: 'line'
    • 觀察:資料沒變,但呈現方式變成了趨勢圖。這適合用在成績嗎?可能不適合,但如果是「每週氣溫」就很適合。
  2. 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();

關鍵邏輯解析

  1. myChart.destroy(): Chart.js 是一個基於 Canvas 的繪圖庫。如果你想在同一個 Canvas 上畫新圖,必須先「銷毀」舊的實例,否則滑鼠互動事件會重疊錯亂(這是在 Single Page Application 開發中常見的 Bug)。
  2. 分離「數據層」與「表現層」: fetchData 負責處理資料邏輯,initChart 只負責畫圖。這是良好的程式架構。

2.3 進階圖表:散佈圖 (Scatter Plot)

在資料科學(特別是迴歸分析)中,我們常需要看兩個變數之間的關係(例如:廣告費用 vs 銷售額)。這時候 labels + data 的結構就不適用了,我們需要 xy 的座標點。

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. 進階儀表板:複合圖表與互動偵測

在真實的商業分析中,單一維度的圖表往往不足以解釋複雜的現象。

想像你是電商平台的資料分析師,你需要在一張圖上同時展示:

  1. 每月銷售總額(數值很大,例如 100,000 ~ 500,000)
  2. 毛利率(數值很小,例如 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, // 將原本的顏色字串換成這個變數
//     ...
// }]

這個小技巧能讓你的長條圖看起來像是一個立體的數據柱,在展示給非技術背景的主管或客戶看時,這種視覺優化往往能增加報告的說服力。