JavaScript 互動圖表程式庫 - D3.js 長條圖練習
| | 0 | |
繪製圖表是常見的程式需求,過去我寫過不少相關文章:
- Python 練習:CSV 繪製樞鈕分析圖表
- 分析Log 計算平均、標準差、95 百分位數並繪製圖表- 從C# 到Python
- 讀書筆記 - 資料視覺化常用圖表整理
- C# 開源圖表程式庫 - ScottPlot
就語言生態系及程式庫成熟度來說,Python 應是首選,但在某些特殊情境,有些解決方案更能展現威力。今天就來介紹在網頁產生圖表的開源利器 - D3.js。
D3.js 的命名源自 Data-Driven Documents,它是專為網頁開發設計的 JavaScript 資料視覺化函式庫,能利用 SVG、HTML5 和 CSS 動態生成互動式圖表、地圖等資料視覺化元件。
D3.js 核心精神是將資料繫結到 SVG 元素,根據資料自動產生、更新或移除圖形。它的 API 用起來有點像 jQuery,用 select()/selectAll() 選元素,attr() 設屬性,並透過串接完成動作。例如:d3.select("body").append("p").text("Hello World!");
,對於有 jQuery 經驗的人應格外有親切感。
而除了提供各式現成圖表範例,D3.js 的 API 能充分支持開發特規圖表或加入高度客製的互動操作,換句話說,你的 HTML/CSS/JavaScript 技能水準決定 D3.js 的能力極限。
總之,如果想為網頁加上可互動操作的資料圖表,D3.js 這個 ISC 授權(相當於 BSD/MIT,可免費商用)的開源程式庫將是好選擇。既是前端攻城利器,焉有不學的道理?
於是,我試著用 D3.js 做了簡單的網頁長條圖當成練習,顯示由亂數產生的 5、10 或 20 筆資料;網頁並支援簡單互動操作,滑鼠滑過長條時右側表格的對應項目會變色;反之,當滑鼠滑過表格數字,對應的長條也會變色。
這個 D3.js 圖表練習成果如下:
必須說,D3.js 雖然看起來像 jQuery,但必須學會一些 D3.js 獨有的觀念才能運用自如。當然,如果你不想弄懂,打算全部丟給 AI 生,那就是選擇了另一條路線,也就不必繼續往下讀,我們下回再見! (揮手下降)
(揮手上升) 嘿,沒想到還有這麼多同學留下來。想必大家應該是有興趣看懂程式或自己動手寫的勇者,那在此簡單交待這張長條圖的做法。
D3.js 的基本教學多如牛毛,就算不想花時間讀,請 Copilot 給範例再要他解釋到飽,也能學到會。AI 造就了學寫程式的夢幻環境,也讓許多人慶幸未來再也不用寫程式,想寫程式跟不想寫程式的人都開心,也算皆大歡喜,噗。
以下是我本次學到的重點:(為學習 D3.js 運作原理,這裡不用現成長條圖套件,從頭自己刻一次)
- 長條圖的 X 軸需要一個 Band Scaler,將離散資料(如類別、索引)平均分配計算其對應的長條位置,考慮間距後決定寬度。例如:總寬度 400,間距 0.25,則每個區塊寬度為 (400 - 40 - 30) / 9 = 40。
- 長條圖的 Y 軸則是使用 Linear Scaler,提供極大值決定區間(有個 nice() 可將 97 調校為 100 等適合人類閱讀的刻度數字),以使將資料數值換算成 Y 座標。
- 縮製長條的方法是在
svg.selectAll('rect').data(data).join()
為每筆資料建立 rect 元素,接著.attr("x", (d, i) => x(i)).attr("y", d => y(d.c)).attr("height", d => y(0) - y(d.c)).attr("width", x.bandwidth())
決定 rect 的 X, Y 座標及寬、高。 - rect 是標準 HTML 元素,故用 :hover CSS 設定滑鼠移過時的樣式,也可掛上 mouseover/mouseout 事件加上互動邏輯。
- 產生 X 軸刻度的做法是產生一個 g 群組元素,
.attr("transform", `translate(0,${height - margin.bottom})`)
決定位置,.call(d3.axisBottom(x).tickFormat((i) => data[i]?.n ?? ""))
將 g 傳給 axisBottom(),用 X Band Scaler 設定 X 軸刻度位置。 - 產生 Y 軸刻度也是產生 g 元素,但改呼叫
.call(d3.axisLeft(y).ticks(5));
,將 g 傳給 axisLeft(),用 Y Linear Scaler 設定 Y 軸刻度,並指定分成五份,產生五個刻度。 - 產生隨機資料、用
<table>
列出資料的部分我用 Vue.js MVVM 寫,至於要跟 D3.js 元素互動,呼叫 d3.select() 再用類似 jQuery 的寫法即可d3.select(`rect[data-n='${n}']`).classed('active', true);
。
程式碼如下,全部不到 200 行搞定,我也放了線上版讓大家試玩:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3.js Bar Plot Example</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
.bar {
fill: steelblue;
}
.bar:hover, .bar.active {
fill: orange;
}
.axis-label {
font-size: 12px;
}
.op {
label {
cursor: pointer;
}
button {
margin-left: 8px;
}
}
.view {
display: flex;
flex-direction: row;
align-items: center;
.table {
display: flex;
flex-direction: row;
}
table {
margin-left: 10px;
border-collapse: collapse;
width: 100px;
td {
border: 1px solid #ccc;
text-align: center;
}
tr.focus {
background-color: yellow;
}
}
}
</style>
</head>
<body>
<div id="app">
<div class="op">
<label v-for="count in dataCounts" :key="count">
<input type="radio" v-model="dataCount" :value="count" />
{{ count }}
</label>
<button @click="genData(dataCount)">Generate</button>
<button @click="sortData()">Sort</button>
</div>
<div class="view">
<svg width="400" height="260"></svg>
<div class="table">
<table>
<tbody>
<tr v-for="(item, index) in data.slice(0, 10)" :class="{ 'focus': focusIdx === item.n }"
@mouseover="focusBar(item.n)" @mouseout="unfocusBar(item.n)">
<td>{{ item.n }}</td>
<td>{{ item.c }}</td>
</tr>
</tbody>
</table>
<table v-if="data.length > 10">
<tbody>
<tr v-for="(item, index) in data.slice(10)" :class="{ 'focus': focusIdx === item.n }"
@mouseover="focusBar(item.n)" @mouseout="unfocusBar(item.n)">
<td>{{ item.n }}</td>
<td>{{ item.c }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
function showData(data) {
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = { top: 20, right: 30, bottom: 30, left: 40 };
svg.selectAll("*").remove();
// 建立 X 軸 Band Scaler (帶狀比例尺)函數
// 將離散資料(如類別、索引)平均分配到一段連續像素範圍,並自動處理每個區塊的寬度與間距
// 例如:總寬度 400,間距 0.25,則每個區塊寬度為 (400 - 40 - 30) / 9 = 40
const x = d3.scaleBand()
// 使用元素索引作為 x 軸
.domain(data.map((d, i) => i))
.range([margin.left, width - margin.right])
// 設定間距
.padding(0.25);
// 建立 Y 軸 Linear Scaler (線性比例尺)函數
const y = d3.scaleLinear()
// .nice() 會自動調整成易讀刻度,例如: 97 調成 100
.domain([0, d3.max(data, d => d.c)]).nice()
.range([height - margin.bottom, margin.top]);
// 建立長條圖
svg.append("g") // 在 SVG 中新增一個群組元素 g
.attr("fill", "steelblue")
.selectAll("rect") // 選擇所有 rect 元素,一開始是空集合
.data(data) // 綁定資料,d3.js 決定產生多少個 rect
.join("rect") // 依據資料新增或移除對應數量的 rect
.attr("class", "bar")
.attr("data-n", (d, i) => d.n)
.attr("x", (d, i) => x(i)) // 用 Band Scaler 函數算出索引對應的 X 軸位置
.attr("y", d => y(d.c)) // 用 Linear Scaler 函數算出筆數對應的 Y 軸的位置
.attr("height", d => y(0) - y(d.c)) // 長條圖的高度
.attr("width", x.bandwidth()) // 長條寬度 (考量 padding)
// 互動事件
.on('mouseover', function (event, d) {
vm.focusIdx = d.n;
})
.on('mouseout', function (event, d) {
vm.focusIdx = -1;
});
// 加入 X 軸
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
// 將 g 傳給 axisBottom(),用 X Band Scaler 設定 X 軸刻度位置
.call(d3.axisBottom(x).tickFormat((i) => data[i]?.n ?? ""))
.selectAll("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-0)")
.style("text-anchor", "middle");
// 加入 Y 軸
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
// 將 g 傳給 axisLeft(),用 Y Linear Scaler 設定 Y 軸標記
// .ticks(n) 可指定 Y 軸分成 n 等份
.call(d3.axisLeft(y).ticks(5));
}
</script>
<script>
const app = Vue.createApp({
data() {
return {
data: [],
dataCount: 10,
dataCounts: [5, 10, 20],
focusIdx: -1,
}
},
methods: {
sortData() {
const sorted = this.data.slice().sort((a, b) => b.c - a.c);
showData(sorted);
},
genData(range) {
this.data = Array.from({ length: range }, (_, i) => ({
n: i + 1,
c: Math.floor(Math.random() * 512)
}));
if (this.sort) {
this.sortData();
}
showData(this.data);
},
focusBar(n) {
this.focusIdx = n;
d3.select(`rect[data-n='${n}']`).classed('active', true);
},
unfocusBar(n) {
this.focusIdx = -1;
d3.select(`rect[data-n='${n}']`).classed('active', false);
}
}
});
const vm = app.mount('#app');
vm.genData(vm.dataCount);
</script>
</body>
</html>
就醬,正式將 D3.js 收入我的前端工具箱。
This blog introduces D3.js for web-based interactive data visualization. It highlights essential concepts and provides a simple bar chart example with JavaScript and Vue.js integration.
Comments
Be the first to post a comment