【UI/UX 技術實作】使用 ApexCharts 打造動態數據儀表板
1. Overview
在現代數據驅動的應用程式中,一個直觀、互動且響應迅速的數據儀表板對於呈現複雜資訊至關重要。它不僅能幫助使用者快速理解數據趨勢,還能透過互動功能進行深入分析,從而做出更明智的決策。ApexCharts 是一個現代化的開源 JavaScript 圖表庫,以其豐富的圖表類型、流暢的動畫效果和高度客製化的能力,成為打造動態數據儀表板的理想選擇 [1]。
本文件將深入探討如何利用 ApexCharts,結合現代前端框架(如 React/Next.js),從零開始構建一個高效能的動態數據儀表板。我們將涵蓋從組件架構設計、數據流管理、動態更新機制到 UI/UX 最佳實踐等關鍵環節,旨在幫助開發者打造出既美觀又實用的數據視覺化解決方案。
2. Architecture / Design
構建動態數據儀表板的關鍵在於其模組化設計、高效的數據管理以及流暢的圖表渲染。以下將闡述其組件架構與數據流邏輯。
2.1 組件架構 (Component Architecture)
儀表板的組件架構應具備良好的可擴展性和可維護性,通常會區分為以下幾種類型:
2.1.1 DashboardLayout (佈局組件)
這是儀表板的頂層容器,負責定義整體佈局和響應式行為。它可以使用 CSS Grid 或 Flexbox 來創建靈活的網格系統,以適應不同螢幕尺寸。對於更複雜的需求,可以整合第三方庫,如 react-grid-layout,以支援使用者拖拽、調整圖表大小和重新排列的功能,從而提供高度客製化的儀表板體驗。
2.1.2 ChartContainer (數據容器組件)
ChartContainer 扮演著數據與圖表之間的橋樑。它的主要職責包括:
- 數據獲取邏輯:整合數據獲取庫(如
@tanstack/react-query或 SWR),負責從後端 API 獲取數據,並處理 Loading、Error 和 Success 狀態。這確保了數據的快取、去重和自動重新獲取,提升了應用程式的響應速度和穩定性。 - 數據轉換與處理:將從 API 獲取的原始數據轉換為 ApexCharts 所需的
series和options格式。這可能涉及數據聚合、過濾或計算。 - 圖表配置管理:管理圖表的靜態和動態配置選項。靜態選項(如顏色、字體)可以在初始化時設定,而動態選項(如數據範圍、圖表類型)則會根據使用者互動或數據變化進行調整。
- 響應式調整:監聽窗口大小變化,並通知其子圖表組件進行尺寸調整,以確保圖表在不同設備上都能正確顯示。
2.1.3 DynamicChart (基礎圖表組件)
DynamicChart 是對 react-apexcharts 庫的封裝,負責渲染單個 ApexCharts 圖表。它的設計應考慮到效能和靈活性:
- 配置優化:利用 React 的
useMemo或useCallbackHook 來優化圖表配置項的生成,確保只有在配置真正改變時才重新計算,避免不必要的渲染。 - 實例方法調用:透過
ref機制獲取 ApexCharts 實例,並呼叫其提供的公共方法,如updateSeries、updateOptions或toggleSeries,以實現圖表的動態更新和互動功能。ApexCharts 內建的動態更新機制能夠提供平滑的動畫效果 [2]。
2.2 資料流邏輯 (Data Flow Logic)
動態數據儀表板的數據流是一個持續的循環過程,旨在保持數據的即時性和圖表的互動性:
- 初始載入 (Initial Load):當儀表板首次載入時,
ChartContainer會觸發數據獲取請求,從後端 API 獲取初始數據集。此時,儀表板會顯示載入狀態(Loading State)。 - 數據更新機制 (Data Update Mechanism):
- 輪詢 (Polling):對於需要定期更新的數據,
@tanstack/react-query的refetchInterval選項可以設定一個時間間隔,自動定期重新獲取數據。這適用於數據變化頻率較高的場景。 - 串流 (Streaming):對於需要極高即時性的數據(如即時監控),可以整合 WebSocket 或 Server-Sent Events (SSE) 等技術,讓後端主動推送數據到前端。前端接收到新數據後,會觸發圖表的更新。
- 輪詢 (Polling):對於需要定期更新的數據,
- 部分更新 (Partial Update):當新數據到達時,
ChartContainer會更新DynamicChart組件的series屬性。ApexCharts 會自動偵測數據變化,並以平滑的動畫效果更新圖表,而不是重新渲染整個圖表,這極大地提升了使用者體驗 [3]。 - 狀態同步 (State Synchronization):在多圖表儀表板中,可能需要實現圖表之間的狀態同步。例如,當使用者在一個圖表上縮放或懸停時,其他相關圖表也能同步顯示對應的數據範圍或提示資訊。這可以透過共享狀態管理(如 Redux 或 Zustand)或 ApexCharts 提供的事件監聽機制來實現。
3. Prerequisites
在開始使用 ApexCharts 構建動態數據儀表板之前,請確保您具備以下知識和工具:
- 前端開發基礎:熟悉 HTML, CSS, JavaScript (ES6+)。
- React/Next.js 經驗:理解 React 組件生命週期、Hooks (useState, useEffect, useMemo, useCallback) 和 Next.js 的基本概念。
- TypeScript 基礎:推薦使用 TypeScript 提升程式碼的可維護性和類型安全。
- 數據獲取庫:熟悉
@tanstack/react-query或 SWR 等數據獲取和狀態管理庫。 - Node.js & npm/yarn:用於專案的依賴管理和運行。
4. Implementation / Code Example
以下將提供一個簡化的程式碼範例,展示如何在 Next.js 應用程式中使用 ApexCharts 創建一個動態更新的折線圖。
4.1 安裝依賴
首先,安裝必要的套件:
npm install apexcharts react-apexcharts @tanstack/react-query
# 或者使用 yarn
yarn add apexcharts react-apexcharts @tanstack/react-query
4.2 ChartDataService.ts (模擬數據服務)
// src/services/ChartDataService.ts
export interface TimeSeriesDataPoint {
x: number; // Timestamp
y: number; // Value
}
export const fetchTimeSeriesData = async (): Promise<TimeSeriesDataPoint[]> => {
// 模擬 API 請求延遲
await new Promise((resolve) => setTimeout(resolve, 1000));
// 模擬生成隨機數據
const data: TimeSeriesDataPoint[] = [];
const now = Date.now();
for (let i = 0; i < 30; i++) {
data.push({
x: now - (29 - i) * 1000 * 60 * 60, // 過去 30 小時的數據
y: Math.floor(Math.random() * 100) + 20, // 20-120 之間的隨機值
});
}
return data;
};
export const fetchNewDataPoint = async (): Promise<TimeSeriesDataPoint> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
x: Date.now(),
y: Math.floor(Math.random() * 100) + 20,
};
};
4.3 DynamicLineChart.tsx (動態折線圖組件)
// src/components/DynamicLineChart.tsx
import React, { useState, useEffect, useMemo, useRef } from 'react';
import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import { fetchTimeSeriesData, fetchNewDataPoint, TimeSeriesDataPoint } from '../services/ChartDataService';
import { ApexOptions } from 'apexcharts';
// 由於 ApexCharts 需要在客戶端渲染,使用 next/dynamic 進行動態導入
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
interface DynamicLineChartProps {
title: string;
}
const DynamicLineChart: React.FC<DynamicLineChartProps> = ({ title }) => {
const [seriesData, setSeriesData] = useState<TimeSeriesDataPoint[]>([]);
const chartRef = useRef<any>(null); // 用於獲取 ApexCharts 實例
// 使用 React Query 獲取初始數據並定期更新
const { data, isLoading, isError, error } = useQuery<TimeSeriesDataPoint[], Error>(
['timeSeriesData'],
fetchTimeSeriesData,
{
staleTime: 1000 * 60, // 數據在 1 分鐘內被視為新鮮
refetchInterval: 1000 * 5, // 每 5 秒自動重新獲取數據
onSuccess: (newData) => {
// 首次載入或數據完全刷新時,直接設定數據
if (seriesData.length === 0 || newData.length > seriesData.length) {
setSeriesData(newData);
} else {
// 模擬增量更新,將新數據點添加到現有數據中
// 在實際應用中,您可能需要更複雜的邏輯來處理數據合併或替換
fetchNewDataPoint().then(newPoint => {
setSeriesData(prevData => {
const updatedData = [...prevData, newPoint];
// 保持數據點數量,例如只顯示最新的 30 個點
return updatedData.slice(-30);
});
});
}
},
}
);
// 當 seriesData 變化時,更新 ApexCharts 實例
useEffect(() => {
if (chartRef.current && seriesData.length > 0) {
chartRef.current.chart.updateSeries([{ data: seriesData }], true);
}
}, [seriesData]);
const chartOptions: ApexOptions = useMemo(() => ({
chart: {
id: 'realtime-line-chart',
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
enabled: true,
speed: 1000,
},
},
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
},
dataLabels: {
enabled: false,
},
stroke: {
curve: 'smooth',
},
title: {
text: title,
align: 'left',
},
markers: {
size: 0,
},
xaxis: {
type: 'datetime',
range: 30 * 1000 * 60 * 60, // 顯示最近 30 小時的數據
labels: {
formatter: (value) => {
const date = new Date(value);
return date.toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit' });
},
},
},
yaxis: {
max: 150,
min: 0,
title: {
text: '數值',
},
},
tooltip: {
x: {
format: 'yyyy/MM/dd HH:mm:ss',
},
},
grid: {
row: {
colors: ['#f3f3f3', 'transparent'], // takes an array which will be repeated on columns
opacity: 0.5,
},
},
}), [title]);
if (isLoading) {
return <div className="text-center py-8">載入數據中...</div>;
}
if (isError) {
return <div className="text-center py-8 text-red-500">錯誤: {error?.message || '無法載入數據'}</div>;
}
return (
<div className="p-4 bg-white rounded-lg shadow-md">
<Chart
options={chartOptions}
series={[{ name: '數據', data: seriesData }]}
type="line"
height={350}
ref={chartRef}
/>
</div>
);
};
export default DynamicLineChart;
4.4 pages/index.tsx (Next.js 頁面)
// pages/index.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import DynamicLineChart from '../components/DynamicLineChart';
const queryClient = new QueryClient();
export default function Home() {
return (
<QueryClientProvider client={queryClient}>
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold text-center mb-8">動態數據儀表板範例</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DynamicLineChart title="即時數據趨勢" />
{/* 可以添加更多圖表 */}
</div>
</div>
</QueryClientProvider>
);
}
5. Parameters / API Reference
此處主要列出 ApexCharts 的核心配置選項和 react-apexcharts 組件的屬性。
5.1 ApexOptions (ApexCharts 配置物件)
| 屬性路徑 | 類型 | 描述 |
|---|---|---|
chart.type | string | 圖表類型,例如 'line', 'bar', 'area', 'pie'。 |
chart.animations.enabled | boolean | 是否啟用圖表動畫。 |
chart.animations.dynamicAnimation.enabled | boolean | 是否啟用數據更新時的動態動畫。 |
series | Array<Object> | 圖表數據系列,每個物件包含 name 和 data (數據點陣列)。 |
xaxis.type | string | X 軸類型,例如 'category', 'datetime', 'numeric'。 |
xaxis.labels.formatter | Function | X 軸標籤的格式化函數。 |
yaxis.title.text | string | Y 軸標題。 |
tooltip | Object | 提示框配置,可客製化顯示內容。 |
dataLabels.enabled | boolean | 是否顯示數據標籤。 |
stroke.curve | string | 線條圖的曲線類型,例如 'smooth', 'straight'。 |
5.2 react-apexcharts Component Props
| 屬性 | 類型 | 描述 |
|---|---|---|
options | ApexOptions | ApexCharts 的配置物件。 |
series | Array<Object> | 圖表數據系列。 |
type | string | 圖表類型,與 options.chart.type 相同。 |
height | string / number | 圖表高度。 |
width | string / number | 圖表寬度。 |
ref | RefObject | 用於獲取 ApexCharts 實例,以便調用其公共方法。 |
6. Notes & Best Practices
- 效能優化:
- 數據量管理:對於大量數據,考慮使用數據聚合(Data Aggregation)或數據抽樣(Data Sampling)來減少傳輸和渲染的數據點數量。ApexCharts 也能處理相當大的數據量,但過多的數據點仍會影響性能。
- 避免不必要的渲染:在 React 中,確保
options和series物件只有在實際內容改變時才重新創建。使用useMemo和useCallback可以有效避免不必要的組件重新渲染。 - 動態導入 (Dynamic Import):對於圖表庫這類體積較大的第三方庫,在 Next.js 中使用
next/dynamic進行動態導入(ssr: false),可以減少初始載入的 JavaScript 體積,提升頁面載入速度。
- UI/UX 考量:
- 響應式設計:確保圖表在不同螢幕尺寸下都能良好顯示。ApexCharts 內建響應式配置,可以根據斷點調整圖表選項。
- 清晰的視覺層次:使用適當的顏色、字體和間距,確保數據易於閱讀和理解。避免過度裝飾,讓數據本身成為焦點。
- 互動性:充分利用 ApexCharts 提供的互動功能,如 Tooltip、縮放、平移、數據點點擊事件等,讓使用者能夠更深入地探索數據。
- 載入與錯誤狀態:為數據載入中和載入失敗的情況提供清晰的視覺反饋(例如 Skeleton Screen 或錯誤訊息),提升使用者體驗。
- 數據管理:
- 數據格式:確保從後端獲取的數據格式與 ApexCharts 所需的格式相符。必要時進行數據轉換。
- 即時性:根據業務需求選擇合適的數據更新策略(輪詢或 WebSocket),並考慮數據更新頻率對後端負載的影響。
- 客製化與主題:
- ApexCharts 提供了豐富的客製化選項,可以調整圖表的顏色、字體、網格線、Tooltips 等。利用這些選項來匹配應用程式的整體設計風格。
- 考慮實現深色模式(Dark Mode)支援,以提供更舒適的夜間使用體驗。
7. 為什麼選擇這種方式?
選擇 ApexCharts 結合現代前端框架來構建動態數據儀表板,是基於其在功能、效能和開發體驗上的綜合優勢:
- 豐富的圖表類型與互動性:ApexCharts 提供了多種圖表類型,從基本的折線圖、柱狀圖到複雜的熱力圖、甘特圖等應有盡有。其內建的縮放、平移、數據點選取、Tooltips 和動畫效果,使得數據儀表板具備高度的互動性,能夠滿足使用者對數據探索的深度需求 [4]。
- 優異的動態更新能力:ApexCharts 在數據更新時能夠提供流暢的動畫效果,無需重新渲染整個圖表,這對於需要即時更新的動態儀表板至關重要。結合
react-apexcharts,可以輕鬆地透過更新series屬性來實現數據的增量更新 [2]。 - 良好的開發者體驗:ApexCharts 的 API 設計直觀易用,文件完善,且與 React、Vue、Angular 等主流前端框架都有良好的整合。這使得開發者能夠快速上手,高效地構建複雜的數據視覺化組件。
- 輕量與高效能:相較於一些功能更為龐大的圖表庫,ApexCharts 在保持豐富功能的同時,也注重性能優化。它能夠處理相當大的數據量,並透過優化的渲染機制確保儀表板的響應速度。
- 客製化與主題:ApexCharts 提供了極高的客製化彈性,開發者可以根據品牌指南或 UI/UX 設計稿,精確調整圖表的每一個視覺元素,從顏色、字體到網格線和 Tooltips,打造獨一無二的儀表板風格。
實作心得
ApexCharts 的動態更新看起來簡單,但在 React 裡做實時資料儀表板時,記憶體洩漏是個需要認真處理的問題。用 setInterval 每 5 秒更新一次資料,如果元件被卸載時沒有 clearInterval,就會持續有多個 interval 在背景跑,瀏覽器開久了記憶體會一直爬升。在 useEffect 的 cleanup function 裡加 return 清除 interval 是基本,但每次更新大量數據點時,也要注意只更新變動的部分,而不是替換整個 series 陣列,否則圖表會重新渲染整個動畫,視覺上會有明顯的閃爍感。
時間序列圖的 x 軸格式是另一個踩坑點。把 Unix timestamp(毫秒)傳給 ApexCharts 時,預設的時間格式顯示不一定符合在地化需求,台灣使用者看到的是 UTC 時間而不是 UTC+8。需要在 xaxis.labels.formatter 裡手動加上時區偏移,或者在資料來源就處理好時區問題。
Tooltip 的客製化功能非常強大,但文件有時候給的範例不完整。最省力的方式是直接在 tooltip.custom 裡用函數返回 HTML 字串,比用 tooltip.y.formatter 更彈性,可以完整控制整個 Tooltip 的排版和內容。
參考資料
- [1] ApexCharts. (n.d.). ApexCharts.js - Open Source JavaScript Charts for your website. Retrieved from https://apexcharts.com/
- [2] ApexCharts. (n.d.). Updating your React chart data is simple. Retrieved from https://apexcharts.com/docs/react-charts/
- [3] ApexCharts. (n.d.). Update Charts from JSON API & AJAX. Retrieved from https://apexcharts.com/docs/update-charts-from-json-api-ajax/
- [4] LogRocket Blog. (2023, January 24). Comparing the most popular JavaScript charting libraries. Retrieved from https://blog.logrocket.com/comparing-most-popular-javascript-charting-libraries/