iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

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

Day 26 - 30 天手把手學會 Chart.js|與 Vue 整合

  • 分享至 

  • xImage
  •  

昨天(Day 25)我們用 react-chartjs-2 把 Chart.js 帶進了 React 的世界,體驗了 useStateuseEffect 如何驅動圖表更新。今天我們換一個舞台:Vue。Vue 3 的 Composition API 搭配官方生態圈推薦的 vue-chartjs 套件,會讓「資料驅動圖表」這件事變得更直覺——因為 Vue 的 Reactivity(響應式)系統本來就是為了「資料變、畫面自動變」而設計的。今天會示範如何用 Vite 建立 Vue 3 + TypeScript 專案、如何用 refreactivecomputed 管理圖表資料,並再次搭配 Node.js 的 Express 架一支簡單的後端 API,體驗「前後端分離」下即時更新圖表的完整流程。

本日範例程式碼:
前端:example01-vue-dashboard
後端 API Server:example02-api-server

一、為什麼不要在 Vue 裡直接用 new Chart()

延續 Day1 ~ Day24 的寫法,我們一直都是這樣建立圖表的:

const ctx = document.getElementById('myChart');
const chart = new Chart(ctx, { type: 'bar', data, options });

如果把這段程式碼直接搬進 Vue 元件的 onMounted 裡,畫面確實「能動」,但很快就會遇到跟 React 類似、卻又帶有 Vue 特色的麻煩:

  • 響應式資料與 Chart.js 內部狀態不同步:Vue 3 的 reactive() 會把資料包成 Proxy,如果直接把這個 Proxy 物件丟給 new Chart() 當作 data,Chart.js 內部某些運算(例如深拷貝、比較)可能會出現非預期的行為,因為 Chart.js 原本並不是為了跟 Proxy 物件打交道而設計的。
  • 元件卸載時忘記 destroy():Vue 元件被銷毀(例如切換路由)時,如果沒有在 onUnmounted 呼叫 chart.destroy() 釋放資源,就會造成記憶體洩漏(Memory Leak),這一點與 React 的 useEffect cleanup 概念相通,只是 Vue 用的是 onUnmounted 這個生命週期鉤子(Lifecycle Hook)。
  • 資料更新要手動呼叫 API:即使 Vue 的響應式系統偵測到資料變了,Chart.js 本身並不知道,還是得手動呼叫 chart.update() 才會重繪,這件事很容易忘記寫、或寫在錯誤的時機點。
  • 重複的樣板程式碼(Boilerplate):每個用到圖表的元件都要重複寫「建立 => 用 watch 監聽資料變化 => update() => onUnmounteddestroy()」這一整套邏輯。

vue-chartjs 就是為了解決上述問題而生的官方推薦套件:它內部已經用 Composition API 的 watch(..., { deep: true }) 監聽了 dataoptions 這兩個 props,資料一變就會自動局部更新(Patch)並呼叫 chart.update(),同時也用 onMounted 建立圖表、onUnmounted 銷毀圖表,把整套生命週期邏輯都封裝好了。我們只需要用 Vue 熟悉的方式傳入 dataoptions,其餘的事情都交給套件處理。

vue-chartjs 內部仍然是呼叫 Chart.js 的原生 API,並沒有重新實作一套繪圖引擎,所以我們前面學過的 dataoptions、Scale、Plugin 等觀念完全可以沿用,只是「建立與更新圖表」的方式换成了 Vue 元件與 Composition API。

二、環境建置:用 Vite 建立 Vue 3 + TypeScript 專案

2.1 建立前端專案

我們一樣使用 Vite 這套現代前端建構工具來初始化專案(Vite 本身就是 Vue 團隊主導開發的,對 Vue 專案的支援非常完整):

npm create vite@latest chartjs-vue-demo -- --template vue-ts
cd chartjs-vue-demo
npm install

安裝完成後,再加裝 Chart.js 與 vue-chartjs

npm install chart.js@4.5.1 vue-chartjs@5.3.4

chart.jsvue-chartjspeer dependency(同儕依賴),也就是說 vue-chartjs 本身不包含 Chart.js 的原始碼,兩者需要一起安裝,而且版本要互相搭配(vue-chartjs v5 對應 chart.js v4,同時支援 Vue 3 與 Vue 2.7)。

2.2 註冊需要的元件(Tree Shaking)

跟 React 篇一樣,Chart.js 4.x 為了減少打包後的檔案大小,把所有的圖表類型、Scale、Plugin 都拆成獨立的模組,需要我們手動 register() 註冊實際會用到的部分。建議把註冊的程式碼獨立成一個檔案,在應用程式進入點只 import 一次:

// src/chartSetup.ts
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  ArcElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';

ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  ArcElement,
  Title,
  Tooltip,
  Legend,
);
// src/main.ts
import { createApp } from 'vue';
import './chartSetup'; // 確保進入點就完成註冊
import './style.css';
import App from './App.vue';

createApp(App).mount('#app');

一個 vue-chartjs 特有的小細節:如果你是從 vue-chartjs import 具名的圖表元件(例如 import { Bar } from 'vue-chartjs'),套件內部其實已經自動幫你 ChartJS.register(BarController) 註冊了「長條圖的 Controller」,這是 createTypedChart() 這個工廠函式的內建行為。但是 CategoryScaleLinearScaleBarElementTooltipLegend 這些「座標軸」「圖形元素」「外掛」仍然需要自己手動 register(),因為套件無法預測你會不會用到時間軸、多 Y 軸或其他外掛。所以最保險的做法,還是像上面一樣在 chartSetup.ts 統一註冊常用元件,不要依賴「元件會不會自動註冊」這件事。

2.3 啟動開發伺服器

npm run dev

Vite 預設會在 http://localhost:5173 啟動開發伺服器,並支援 HMR(Hot Module Replacement),修改 .vue 檔案後畫面會立即更新,不需要手動整頁重整,這一點與 React 篇的體驗完全一致。

三、元件化圖表設計:使用 vue-chartjs 的具名圖表元件

3.1 最基本的用法

vue-chartjs 針對每種圖表類型都提供了對應的具名元件,例如 BarLinePieDoughnutRadarPolarAreaBubbleScatter,用法跟平常寫 Chart.js 的設定幾乎一樣,只是 dataoptions 變成了 props。這裡使用 Vue 3 推薦的 <script setup> 語法:

<!-- src/components/MonthlySalesChart.vue -->
<script setup lang="ts">
import { Bar } from 'vue-chartjs';
import type { ChartData, ChartOptions } from 'chart.js';

const data: ChartData<'bar'> = {
  labels: ['一月', '二月', '三月', '四月', '五月', '六月'],
  datasets: [
    {
      label: '銷售額(萬元)',
      data: [65, 59, 80, 81, 56, 72],
      backgroundColor: 'rgba(54, 162, 235, 0.6)',
    },
  ],
};

const options: ChartOptions<'bar'> = {
  responsive: true,
  maintainAspectRatio: false,
  plugins: {
    title: { display: true, text: '每月銷售額' },
  },
};
</script>

<template>
  <div style="height: 360px">
    <Bar :data="data" :options="options" />
  </div>
</template>

maintainAspectRatio: false 這行很重要:跟 React 篇提到的一樣,Chart.js 預設會依照固定的長寬比例縮放圖表,如果外層容器(例如上面的 <div>)用 CSS 指定了高度,卻沒有關閉 maintainAspectRatio,圖表可能不會乖乖填滿容器。養成「外層容器給高度 + maintainAspectRatio: false」的習慣,可以省下很多除錯時間。

3.2 用 TypeScript 定義 Props,讓圖表元件可重複使用

上面的例子把 data 寫死在元件內部,沒有真正做到「元件化」。實務上我們會用 definePropsdata 需要的資料抽出來當作 props,讓同一顆圖表元件可以顯示不同的資料:

<!-- src/components/BarChart.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { Bar } from 'vue-chartjs';
import type { ChartData, ChartOptions } from 'chart.js';

const props = withDefaults(
  defineProps<{
    title: string;
    labels: string[];
    values: number[];
    color?: string;
  }>(),
  {
    color: 'rgba(54, 162, 235, 0.6)',
  },
);

// 用 computed 包裝,確保只有 props 真正改變時才重新建立 data 物件
const data = computed<ChartData<'bar'>>(() => ({
  labels: props.labels,
  datasets: [
    {
      label: props.title,
      data: props.values,
      backgroundColor: props.color,
    },
  ],
}));

const options = computed<ChartOptions<'bar'>>(() => ({
  responsive: true,
  maintainAspectRatio: false,
  plugins: {
    title: { display: true, text: props.title },
    legend: { display: false },
  },
}));
</script>

<template>
  <div style="height: 320px">
    <Bar :data="data" :options="options" />
  </div>
</template>

使用時,只要傳入不同的 props,就能重複利用同一顆元件畫出不同的圖表:

<!-- src/App.vue -->
<script setup lang="ts">
import BarChart from './components/BarChart.vue';
</script>

<template>
  <div style="display: grid; gap: 24px; padding: 24px">
    <BarChart
      title="每月銷售額(萬元)"
      :labels="['一月', '二月', '三月', '四月']"
      :values="[65, 59, 80, 81]"
    />
    <BarChart
      title="每月退貨數量"
      :labels="['一月', '二月', '三月', '四月']"
      :values="[3, 5, 2, 4]"
      color="rgba(255, 99, 132, 0.6)"
    />
  </div>
</template>

這就是「元件化圖表設計」的核心精神:圖表元件只負責「怎麼畫」,資料則由外部透過 props 決定「畫什麼」,兩者職責分離,元件才能真正被重複使用在不同的頁面或情境。這跟昨天 React 篇的設計理念完全一致,只是把 props 的傳遞方式换成了 Vue 的模板語法。

3.3 陷阱:多個 dataset 更新時互相「合併」的問題

vue-chartjs 內部有一個 datasetIdKey 的 props(預設值是 'label'),用來辨識「這是原本的第幾筆 dataset」。當 data 更新時,vue-chartjs 會依照 datasetIdKey 去比對新舊 dataset,找得到對應的舊資料就直接 Object.assign 進去做局部更新(保留動畫過渡效果),找不到就當作全新的 dataset 加入。

這代表:如果你的多個 dataset 沒有唯一的 label(或你指定的識別欄位),vue-chartjs 可能會誤判成同一筆資料,導致更新時發生資料錯亂或動畫跳動的狀況。解決方法跟 React 篇一樣:

  1. 讓每個 dataset 都有唯一的 labelvue-chartjs 預設就是用 label 當作識別 key)。
  2. 如果 label 不方便當作唯一識別(例如 label 是給圖例顯示用的中文說明,可能會重複),可以額外給每個 dataset 一個 id 欄位,並透過 datasetIdKey prop 告訴元件要用哪個欄位當 key:
<Line
  dataset-id-key="id"
  :data="{
    labels: ['六月', '七月', '八月'],
    datasets: [
      { id: 1, label: '今年', data: [5, 6, 7] },
      { id: 2, label: '去年', data: [3, 2, 1] },
    ],
  }"
/>

記住這個原則:只要圖表有兩個以上的 dataset,就要主動確認每個 dataset 有沒有唯一的識別值,否則等到資料動態更新時才發現圖表跑掉,會比較難排查問題根源。這一點無論是 react-chartjs-2 還是 vue-chartjs 都是共通的設計,因為兩者都是圍繞著 Chart.js 原生 API 打造的 Wrapper。

四、Vue 響應式系統下的資料綁定與自動更新

vue-chartjs 內部用 watch([() => props.options, () => props.data], ..., { deep: true }) 監聽 dataoptions 這兩個 props 的深層變化,只要偵測到內容真的不一樣,就會自動呼叫 Chart.js 的 update()。這代表:在 Vue 裡,我們完全不需要手動呼叫任何「更新圖表」的方法,只要讓資料本身是響應式的(refreactive),並讓它的值真正改變,圖表就會自動重繪——這正是 Vue Reactivity 系統與 Chart.js 整合得最漂亮的地方。

4.1 情境一:使用者互動觸發更新(ref + computed

例如做一個「切換時間區間」的下拉選單,選擇不同區間時圖表資料跟著換掉:

<!-- src/components/RangeSalesChart.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Line } from 'vue-chartjs';
import type { ChartData, ChartOptions } from 'chart.js';

type Range = 'week' | 'month' | 'year';

const DATASET_BY_RANGE: Record<Range, { labels: string[]; values: number[] }> = {
  week: { labels: ['一', '二', '三', '四', '五', '六', '日'], values: [12, 19, 8, 15, 22, 30, 25] },
  month: { labels: ['第1週', '第2週', '第3週', '第4週'], values: [80, 95, 70, 110] },
  year: { labels: ['Q1', 'Q2', 'Q3', 'Q4'], values: [300, 420, 380, 460] },
};

const range = ref<Range>('week'); // 響應式狀態:目前選擇的時間區間

// 只有 range 改變時才會重新計算,computed 會自動快取結果
const data = computed<ChartData<'line'>>(() => {
  const source = DATASET_BY_RANGE[range.value];
  return {
    labels: source.labels,
    datasets: [
      {
        label: '銷售趨勢',
        data: source.values,
        borderColor: 'rgb(75, 192, 192)',
        backgroundColor: 'rgba(75, 192, 192, 0.3)',
        tension: 0.3,
      },
    ],
  };
});

const options: ChartOptions<'line'> = {
  responsive: true,
  maintainAspectRatio: false,
};
</script>

<template>
  <div>
    <select v-model="range">
      <option value="week">本週</option>
      <option value="month">本月</option>
      <option value="year">今年</option>
    </select>
    <div style="height: 320px; margin-top: 12px">
      <Line :data="data" :options="options" />
    </div>
  </div>
</template>

重點在於:只要 range 這個 ref 的值改變,computed 就會重新計算出新的 datavue-chartjs 偵測到 data 的內容變了,就會自動更新圖表,開發者完全不需要自己手動操作 Chart.js 的 API。這跟 React 篇用 useState + useMemo 的思路幾乎一模一樣,只是 Vue 的 v-model 讓「表單元素綁定狀態」這件事變得更簡潔——不需要自己寫 onChange 事件處理函式。

4.2 情境二:onMounted + setInterval 定時向後端拉取即時資料

接下來示範跟後端 API 整合、定時更新圖表的情境(後端 Express API 的部分會在下一節說明):

<!-- src/components/LiveStatsChart.vue -->
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { Line } from 'vue-chartjs';
import type { ChartData, ChartOptions } from 'chart.js';

const MAX_POINTS = 10; // 圖表上最多保留幾個資料點(Sliding Window)
const API_URL = 'http://localhost:4000/api/stats';
const POLL_INTERVAL_MS = 3000;

interface StatsPayload {
  time: string;
  onlineUsers: number;
  orders: number;
}

const labels = ref<string[]>([]);
const onlineUsers = ref<number[]>([]);
const errorMessage = ref<string | null>(null);
let intervalId: number | undefined;

async function fetchStats() {
  try {
    const res = await fetch(API_URL);
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    const stats: StatsPayload = await res.json();

    // 直接對 ref.value 陣列做賦值,Vue 的響應式系統會自動偵測到變化
    labels.value = [...labels.value, stats.time].slice(-MAX_POINTS);
    onlineUsers.value = [...onlineUsers.value, stats.onlineUsers].slice(-MAX_POINTS);
    errorMessage.value = null;
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    console.error('取得即時資料失敗', message);
    errorMessage.value = `無法連線到後端 API(${API_URL}):${message}`;
  }
}

onMounted(() => {
  fetchStats(); // 先立即抓一次,不用等第一個 interval
  intervalId = window.setInterval(fetchStats, POLL_INTERVAL_MS);
});

// 元件卸載時務必清除計時器,避免記憶體洩漏,觀念與 React 的 useEffect cleanup 相通
onUnmounted(() => {
  window.clearInterval(intervalId);
});

const data = computed<ChartData<'line'>>(() => ({
  labels: labels.value,
  datasets: [
    {
      label: '線上人數',
      data: onlineUsers.value,
      borderColor: 'rgb(255, 99, 132)',
      backgroundColor: 'rgba(255, 99, 132, 0.2)',
      fill: true,
      tension: 0.3,
    },
  ],
}));

const options: ChartOptions<'line'> = {
  responsive: true,
  maintainAspectRatio: false,
  animation: { duration: 300 }, // 縮短動畫時間,讓即時更新看起來更順暢
  scales: {
    y: { beginAtZero: true },
  },
};
</script>

<template>
  <div>
    <p v-if="errorMessage" style="color: #e03131; font-size: 0.85rem">
      {{ errorMessage }}(請確認 examples/example02-api-server 是否已啟動)
    </p>
    <div style="height: 320px">
      <Line :data="data" :options="options" />
    </div>
  </div>
</template>

這個範例展示了三個實務上很重要的細節,跟 React 篇的 useEffect 版本正好可以對照:

  1. onUnmounted 一定要記得清除 setInterval:如果沒有在 onUnmounted 呼叫 clearInterval,即使元件已經卸載,計時器還是會持續在背景執行、持續嘗試修改 ref.value,這是 Vue 應用程式常見的記憶體洩漏來源。這個角色對應到 React 篇 useEffect 回傳的 cleanup function。
  2. 直接對 ref.value 賦值就會觸發響應式更新:不像 React 的 useState 需要擔心「stale closure(過期閉包)」問題,Vue 的 ref 是透過 Proxy 追蹤依賴,setInterval 的 callback 裡直接讀取、修改 labels.value 都會拿到最新的值,不需要像 React 那樣特別寫成函式式更新 (prev) => ...。這是 Vue Reactivity 系統與 React 資料流哲學上一個很有趣的差異點。
  3. 限制資料點數量(slice(-MAX_POINTS):即時資料如果無限累加,陣列會越來越大,圖表也會越畫越擠,這裡一樣採用「移動視窗(Sliding Window)」的做法,只保留最近 N 筆資料。

五、進階:透過 ref 取得 Chart.js 原生實例

雖然大部分情境只需要 dataoptions 這兩個 props 就能滿足需求,但有時候還是需要直接呼叫 Chart.js 原生的 API(例如手動觸發 resize()、或是取得使用者點擊了圖表上的哪個元素)。vue-chartjs 的每個圖表元件都用 expose({ chart: chartRef }) 把底層的 Chart.js instance 暴露出來,我們可以透過 Vue 的 template ref 拿到它:

<!-- src/components/ClickableBarChart.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { Bar, getElementAtEvent } from 'vue-chartjs';
import type { ChartData, ChartOptions } from 'chart.js';

const props = defineProps<{ data: ChartData<'bar'> }>();

// template ref:型別對應到 vue-chartjs 具名元件的 instance
const chartRef = ref<InstanceType<typeof Bar> | null>(null);
const message = ref('點擊長條可以看到對應的資料點資訊');

const options: ChartOptions<'bar'> = {
  responsive: true,
  maintainAspectRatio: false,
};

function handleClick(event: MouseEvent) {
  if (!chartRef.value?.chart) return;
  const [element] = getElementAtEvent(chartRef.value.chart, event);
  if (element) {
    const { datasetIndex, index } = element;
    const dataset = props.data.datasets[datasetIndex];
    const label = props.data.labels?.[index];
    const value = dataset.data[index];
    message.value = `點擊到「${dataset.label}」第 ${index + 1} 筆(${label}):${value}`;
  }
}
</script>

<template>
  <div>
    <p style="font-size: 0.85rem; color: #666">{{ message }}</p>
    <div style="height: 320px">
      <Bar ref="chartRef" :data="data" :options="options" @click="handleClick" />
    </div>
  </div>
</template>

vue-chartjs 提供了三個常用的輔助函式,都是傳入「Chart.js instance」和「原生的滑鼠事件」,回傳使用者點擊到的資料,跟 react-chartjs-2 提供的輔助函式幾乎是同名同款(畢竟兩者都是圍繞著 Chart.js 原生 API 打造的):

輔助函式 說明
getDatasetAtEvent(chart, event) 回傳點擊位置對應的整個 dataset
getElementAtEvent(chart, event) 回傳點擊位置對應的單一資料點(element)
getElementsAtEvent(chart, event) 回傳點擊位置對應的所有重疊的資料點

chartRef.value?.chartchart 是怎麼來的? 打開 vue-chartjs 原始碼會看到 expose({ chart: chartRef }) 這一行,代表這個元件主動把內部的 chartRef(Chart.js instance 的 shallowRef)以 chart 這個名字暴露給外部使用。這是 Vue 3 <script setup> 元件想要對外公開內部狀態時的標準做法(defineExposesetup() 裡的 expose()),如果元件沒有主動 expose,外部即使拿到 template ref 也存取不到內部變數。

六、實作:整合前後端的銷售儀表板

現在把前面學到的技巧整合起來,做一個完整的小型儀表板(Dashboard):分別顯示「固定資料的長條圖」「使用者互動切換的折線圖」「每 3 秒更新一次的即時折線圖」「可點擊的長條圖」,資料來源涵蓋「寫死的靜態資料」與「Express API」兩種情境。

6.1 後端:用 Express 建立簡單的 API

先建立一個獨立的後端資料夾(與前端專案分開):

mkdir chartjs-api-server
cd chartjs-api-server
npm init -y
npm install express cors

撰寫一支最簡單的 API 伺服器(跟 Day 25 React 篇的寫法完全一樣,因為後端 API 本來就跟前端框架無關——這正是「前後端分離」架構的優勢,同一支 API 可以同時服務 React 版與 Vue 版的前端):

// server.js
const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors()); // 開發階段允許前端(不同 port)跨來源請求

// 模擬即時的線上人數/訂單數資料
app.get('/api/stats', (req, res) => {
  const now = new Date();
  const label = now.toLocaleTimeString('zh-TW', { hour12: false });
  const onlineUsers = Math.floor(50 + Math.random() * 50);
  const orders = Math.floor(Math.random() * 20);

  res.json({ time: label, onlineUsers, orders });
});

const PORT = 4000;
app.listen(PORT, () => {
  console.log(`API server running at http://localhost:${PORT}`);
});
node server.js

6.2 前端:組合成儀表板

<!-- src/App.vue -->
<script setup lang="ts">
import BarChart from './components/BarChart.vue';
import RangeSalesChart from './components/RangeSalesChart.vue';
import LiveStatsChart from './components/LiveStatsChart.vue';
import ClickableBarChart from './components/ClickableBarChart.vue';

const clickableData = {
  labels: ['北區', '中區', '南區', '東區'],
  datasets: [
    {
      label: '本月訂單數',
      data: [42, 35, 58, 20],
      backgroundColor: 'rgba(76, 110, 245, 0.6)',
    },
  ],
};
</script>

<template>
  <div style="padding: 24px">
    <h1>Day 26|Chart.js 與 Vue 整合:銷售儀表板</h1>
    <div
      style="
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
        gap: 32px;
      "
    >
      <section>
        <h2>本季銷售額(元件化 + props)</h2>
        <BarChart title="銷售額(萬元)" :labels="['一月', '二月', '三月']" :values="[65, 59, 80]" />
      </section>

      <section>
        <h2>銷售趨勢(ref + computed 互動切換)</h2>
        <RangeSalesChart />
      </section>

      <section>
        <h2>即時線上人數(onMounted + Express API)</h2>
        <LiveStatsChart />
      </section>

      <section>
        <h2>各區訂單數(template ref + 點擊事件)</h2>
        <ClickableBarChart :data="clickableData" />
      </section>
    </div>
  </div>
</template>

開發時分別在兩個終端機視窗執行:

# 終端機 1:啟動後端 API
cd chartjs-api-server
node server.js

# 終端機 2:啟動前端開發伺服器
cd chartjs-vue-demo
npm run dev

打開瀏覽器就可以看到四個區塊分別呈現不同的資料流動方式,完整體驗了「Vue 元件化 + Composition API 響應式更新 + 後端 API」的整合流程。

七、常見誤區與注意事項

  1. 忘記 register() 對應的元件:Chart.js 4.x 需要手動註冊 Scale、Element、Plugin,這件事和是否使用 Vue 無關,即使 import 了 vue-chartjs 的具名元件(會自動註冊 Controller),Scale/Element/Plugin 還是要自己補齊。
  2. 把整個 dataoptions 包進 reactive(),卻沒注意深層監聽的效能成本vue-chartjsdataoptions{ deep: true } 監聽,如果圖表資料量很大(例如上千筆資料點),每次任何巢狀屬性改變都會觸發整個物件的深層比對,可能造成效能瓶頸。資料量大時,建議改成「整批替換陣列」(如同第 5.2 節的 labels.value = [...])而不是逐一 push 觸發多次巢狀變化。
  3. 多 dataset 沒有設定 dataset-id-key 或唯一 label:如同第四章提到的,會造成資料更新時圖表資料錯亂或動畫跳動的問題。
  4. 外層容器沒有明確高度又忘記關閉 maintainAspectRatio:圖表可能顯示異常(太小、太大、或是不斷長高),這一點與 React 篇完全相同。
  5. onMounted 裡的 setInterval 忘記在 onUnmounted 清除:這是 Vue 應用程式最常見的記憶體洩漏來源之一,尤其在即時資料圖表、或是使用 <KeepAlive>/路由切換的情境中特別容易忽略。
  6. 直接把 Chart.js instance 存進 reactive():Chart.js instance 是一個複雜的物件,被 Vue 的 Proxy 包裹後容易出現非預期的行為(vue-chartjs 內部特地用 shallowRef 而非 ref 保存 instance 就是這個原因),如果需要保存 instance 的參照,應該用 shallowRef 或直接透過 template ref 存取。
  7. 元件沒有 expose 卻期待外部能拿到內部狀態:如果你自己寫的 Vue 元件想像 vue-chartjs 一樣,讓外部透過 template ref 存取內部的某個變數,記得要用 defineExpose({ ... })<script setup> 語法)或 setup() 裡的 expose() 明確公開,否則預設是拿不到的。

明天(Day 27)我們會完成框架整合的最後一站:Angular。學習使用 Angular 官方推薦的 ng2-charts 套件,認識 provideCharts(withDefaultRegisterables()) 的模組設定方式、<canvas baseChart> 指令用法,並搭配 @Input()ChangeDetectorRef 與 RxJS Observable 資料流,體驗第三種前端框架整合圖表的方式,與這兩天的 React/Vue 版本做個完整的三方對照。

參考資源


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

尚未有邦友留言

立即登入留言