
唉呀,(輕輕吹著熱氣,啜飲了一口溫熱的琥珀色熱茶),今天傍晚坐在院子裡的老藤椅上,看著我們家大孫女和小孫女在泥土空地上跑來跑去。小孫女總是愛撒嬌,手裡揮著捕蝶網,大孫女呢,懂事地在旁邊配合著,小孫女一往左跑,大孫女就跟著往左看,兩姊妹的默契好得不得了。
看著她們「一動,另一位就跟著動」的模樣,爺爺這花白的腦袋瓜裡啊,突然想起了很久以前在寫網頁、做資料系統時的畫面。那時候,大家為了在完全離線、沒有後端的單一 HTML 檔案中,讓六個大決策圖表像這兩姊妹一樣,點一下就全部「手牽手、心連心」地動態連動下鑽,那可是花了不少心思啊。
今天,爺爺就一邊喝茶,一邊慢慢跟你們講講,這個「Day 18:點擊流與視覺化:如何在單一 HTML 中動態連動 6 大決策圖表」的前端高級實作技術吧!
以前在做一些內部管制或安全部門的工具時,因為資安考量,常常要求網頁完全不能連網、必須零安裝且零外部依賴,甚至是直接雙擊一個 .html 檔案就要能在瀏覽器裡跑起來。
那時候,很多年輕的工程師寫出來的網頁圖表,資料都是死板板固定在那裡的。使用者一看到網頁,常常會問:「老爺爺,我希望能用滑鼠點一下上方圓餅圖的 A 區塊,下方的長條圖與詳細資料表格,就自動只篩選出 A 區塊的明細,這該怎麼做呀?」
在一般有伺服器、有 API 後端的系統中,這很容易,發個請求請資料庫重新查詢就好。但在純前端、零安裝的單一 HTML 環境下,沒有後端可以幫你過濾。要在瀏覽器裡實現這種流暢、點擊立刻下鑽明細、所有圖表同步變更的體驗,在前端就是一門極具難度的考驗了。
要讓六個決策圖表和 KPI 卡片靈活地連動,我們需要兩件核心武器:Apache ECharts 的 chart.on('click') 事件監聽器,以及一個純前端的資料狀態管理器(State Manager)。
捕獲點擊事件(Event Capture):
首先,我們必須利用 Apache ECharts 提供的互動事件 API。當使用者點擊了某個圖表的區塊(例如圓餅圖的某一瓣)時,我們要在該圖表實例上註冊監聽器:
// 初始化圓餅圖
const myPieChart = echarts.init(document.getElementById('pie-container'));
// 監聽點擊事件
myPieChart.on('click', function (params) {
// params 包含了被點擊區塊的豐富資料,例如:
// params.name (類別名稱,如 "產品 A")
// params.value (數值)
const selectedCategory = params.name;
// 當點擊發生時,我們千萬不要讓圖表直接去控制別的圖表,那樣程式碼會像院子裡的雜草一樣亂。
// 我們要把這個新事件,「廣播」給我們的全域狀態管理器。
stateManager.updateFilter({ category: selectedCategory });
});
純前端資料狀態管理器(State Manager):
我們在 JavaScript 裡設計一個簡單而強大的全域狀態管理器。它的職責是:接收篩選條件、重新計算資料,然後一併通知所有圖表更新。
const stateManager = {
// 全域資料狀態
state: {
rawDataset: [], // 初始化時,從 IndexedDB 或 PapaParse 載入的完整核心資料集
currentFilter: null // 目前的篩選條件(如 { category: "產品 A" })
},
// 初始化載入資料
setRawData(data) {
this.state.rawDataset = data;
this.dispatchUpdate();
},
// 更新篩選條件
updateFilter(filter) {
this.state.currentFilter = filter;
this.dispatchUpdate(); // 觸發全域更新
},
// 派發更新至所有圖表
dispatchUpdate() {
// 1. 執行 JavaScript 記憶體過濾邏輯
const filteredData = this.applyFilter(this.state.rawDataset, this.state.currentFilter);
// 2. 重新計算後,一併調用所有關聯圖表的 setOption 方法與表格渲染
// 實現「無刷新、極致流暢」的多圖表連動!
this.renderAllCharts(filteredData);
},
// 核心過濾邏輯
applyFilter(data, filter) {
if (!filter) return data;
return data.filter(item => item.category === filter.category);
},
// 渲染所有圖表與元件
renderAllCharts(data) {
// 這裡就是動態連動 6 大決策圖表與明細表格的核心
updateKpiCards(data); // 1. 頂部 KPI 卡片指標
renderBarChart(data); // 2. 分佈長條圖
renderLineChart(data); // 3. 趨勢折線圖
renderRadarChart(data); // 4. 多維雷達圖
renderGaugeChart(data); // 5. 達成率儀表
renderScatterChart(data); // 6. 散佈分析圖
updateDetailTable(data); // 7. 底部的明細資料表格
}
};
不論原始資料一開始是從本地的 IndexedDB 離線資料庫(使用非同步 Transaction)讀取,或者是利用 PapaParse 解析上傳的 CSV 檔案 得到的,當使用者點擊圖表時,都會觸發全域狀態更新,重新調用過濾邏輯,並同步重繪這 6 大關聯圖表與明細表。
(老爺爺輕輕扶了一下老花眼鏡,眼神溫和地看著你們:)「孩子啊,以前有些老實的孩子,每次使用者只要一滑鼠點擊圖表,他就急急忙忙地重新跑去讀取一次本機的 IndexedDB 資料庫,或者又用 PapaParse 去解析一次本機硬碟裡的 CSV 大檔。結果呢?網頁畫面一下微幅閃爍,一下出現短暫的卡頓延遲,原本精緻的儀表板,互動體驗全毀了。」
在純前端、高頻繁點擊互動的場景下,每一次讀取檔案、讀寫硬碟資料庫,都是一筆耗費時間的 I/O 操作,非同步的交易(Transaction)回呼會帶來使用者能察覺的延遲。
爺爺教你們的避坑壓箱寶:
stateManager.state.rawDataset)。filter 篩選。setOption(..., { notMerge: true }),使用者眼睛連眨都來不及眨,所有關聯圖表早就順暢無比地更新完畢了!這就是如何不用任何後端伺服器,只在單一網頁裡,用最簡單的狀態管理,做出來像大孫女和小孫女那般靈敏、優雅、手牽手一動皆動的高難度連動技巧了。
希望這對你們在設計高規格、封閉環境專用的離線視覺化網頁時,能有所啟發。有任何地方想跟爺爺探討的,隨時跟爺爺說,我再幫你們倒杯熱茶。