一、前言
1.1、D3.js 是什麼?
D3.js 是一個基於 JavaScript
開發的庫,主要是用於在瀏覽器中操作 SVG
、HTML
、CSS
,通常我們可以利用它來進行一些圖表繪製的工作。
1.2、為什麼要學 D3.js
在我們學某種技術之前,最好是能夠帶有一些比較明確的目的,那麼在學習的過程中就不容易喪失目標,最後只學到皮毛。
在我看來,學 D3
主要是有這幾個方面:
- 感興趣,希望在業餘時間能夠學習自己感興趣的一些技術;同時
D3
也具有一定的複雜度,也可以拓寬自己的技術廣度 D3
可以很方便的繪製一些互動式的圖表,同時相比業界現成的元件庫更加的定製化,可以靈活的根據自己的需求來實現一些功能- 工作中的一部分內容涉及到了圖表互動,苦於一些元件庫的定製化程度不高,或者是文件太過於繁雜,難以滿足一些需求
因此,在這些動機的促使下我決定學習 D3
這個庫是怎麼使用的。
二、基礎知識
2.1、常用 API
2.1.1、選擇節點
API | 功能描述 |
---|---|
d3.select | 用於選中某個 dom 元素,類似於 document.querySelector |
d3.selectAll | 用於選中多個 dom 元素,類似於 document.querySelectorAll |
2.1.2、修改節點
API | 功能描述 |
---|---|
selection.text("content") | 獲取或者返回當前選中節點的文字內容 |
selection.append("element name") | 新增節點到當前已選中節點的末尾處 |
selection.insert("element name") | 插入節點到當前已選中節點 |
selection.remove | 移除指定節點 |
selection.html("content") | 獲取或返回當前已選中節點的 html |
selection.attr("name", value) | 獲取或設定當前已選中節點的屬性 |
selection.property("name", value) | 同上 |
selection.style("name", value) | 獲取或設定當前已選中節點的樣式 |
selection.classed("css class", bool) | 獲取、刪除或者新增類名到當前已選中的節點 |
2.1.3、增刪節點
API | 功能描述 |
---|---|
selection.data | 將 data 繫結到節點上 |
selection.join | 基於繫結的 data 可實現 enter、update、exit 三種函式,更加精細的控制實際的效果 |
selection.enter | 獲得進入的 selection |
selection.exit | 獲得退出的 selection |
selection.datum | 獲取/設定節點資料 |
更多 API
請參考 官方文件
2.2、鏈式呼叫
為了方便使用,D3
支援了鏈式呼叫。如上面的 API
所示,D3
裡面分成這兩類方法:
d3.select
/d3.selectionAll
d3.selection.function
- 其他(以後再講)
因此,我們必須要有一個 selection
物件才能夠使用其他操作方法。如:
d3.select('div')
.append('svg')
.attr('width', 100)
.attr('height', 100)
2.3、座標系
需要注意的是,在 D3.js
繪製的 svg 中,座標原點是在左上角的。
三、實戰
3.1、第一步 —— 建立 svg
建立一個寬高為 400 x 33 的 svg
const width = 400
const height = 33
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 -20 ${width} 33`);
3.2、第二步 —— 生成隨機字元
定義一個生成隨機字元的函式。如下所示,我們會生成隨機的字元,最多為 26 個大寫的字母。程式碼如下:
function randomLetters() {
return d3
.shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''))
.slice(0, Math.ceil(Math.random() * 26))
.sort()
}
目前到這裡還算比較簡單。
3.3、第三步 —— 更新字串
根據生成的隨機字元,把資料繫結到對應的節點上。其中,我們把新增、更新、刪除的節點顏色分別設定為:
綠色、紅色、金色
const t = svg.transition().duration(750);
svg.selectAll("text")
// 繫結資料
.data(randomLetters(), d => d)
.join(
// 設定新增的字元
enter => enter.append("text")
// 設定顏色
.attr("fill", "green")
// 設定位置
.attr("x", (d, i) => i * 16)
.attr("y", -30)
// 設定字元到節點上
.text(d => d)
.call(enter => enter.transition(t)
.attr("y", 0)),
// 設定更新的字元
update => update
.attr("fill", "red")
.attr("y", 0)
.call(update => update.transition(t)
.attr("x", (d, i) => i * 16)),
// 設定刪除的字元
exit => exit
.attr("fill", "gold")
.call(exit => exit.transition(t)
.attr("y", 30)
.remove())
);
當然,這裡的程式碼比較長並且理解成本也比較高,我們可以形成幾個問題並嘗試透過解決問題來理解他。
3.3.1、為什麼 text
節點還沒有就能夠呼叫 selectAll
關於這點一開始我在學習 D3.js
也曾經想過,後來我去翻了下對應的原始碼感覺就能夠 get 到了。
如下所示,我們可以看到呼叫 selectAll
的時候,實際上會返回一個 Selection
物件。而傳入的 selector
則會被作為第一個引數接受,第二個引數是預設的 document.documentElement
。
import array from "./array.js";
import {Selection, root} from "./selection/index.js";
export default function(selector) {
return typeof selector === "string"
? new Selection([document.querySelectorAll(selector)], [document.documentElement])
: new Selection([array(selector)], root);
}
然後物件本身會儲存著兩個引數到 _groups
和 _parents
這兩個變數中,如下所示:
export function Selection(groups, parents) {
this._groups = groups;
this._parents = parents;
}
所以這一步本身只是生成了 Selection
的例項並且把 selector
及其父節點繫結到這個例項上。
那麼問題就來了:
- 這一步可以用
select
函式來替代嗎? 答:不行,因為select
和selectAll
函式的區別在於:前者呼叫的是document.querySelector
,結果返回的是物件;後者則呼叫的是document.querySelectorAll
,結果返回的是“陣列”。那麼最後的資料結構一個是物件,另外一個是二維陣列。最直接的表現是,替換之後程式馬上就會報錯~ - 可以用其他函式來代替
selectAll
麼? 答:不行,雖然本質上只需要拿到一個Selecton
物件並且繫結資料就可以了,但是目前D3.js
並沒有把這個功能直接暴露出來。
綜上,我們只能使用 selectAll
來獲取 Selection
物件,即使沒有預先生成節點而呼叫而導致看起來很彆扭?。
3.3.2、data
函式是怎麼實現資料繫結的?
提到這個問題那麼就必須看一下 data
函式的內部實現了:
data
會根據傳入的key
來進行繫結,呼叫bindKey
函式;否則則呼叫bindIndex
函式bindIndex
按照下標分別將節點存入update
、enter
、exit
陣列中bindKey
傳入的key
是一個函式,利用生成的keyValue
來將節點存入update
、enter
、exit
陣列中
值得注意的是,如果需要針對 update
階段做處理(比如說動畫),那麼傳 key
是非常重要的。因為預設傳 index
的話,即使是節點更新了,那麼也有可能觸發不了 update
函式。
- 返回一個新的
Selection
物件
export default function(value, key) {
if (!arguments.length) return Array.from(this, datum);
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;
if (typeof value !== "function") value = constant(value);
for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = arraylike(value.call(parent, parent && parent.__data__, j, parents)),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// Now connect the enter nodes to their following update node, such that
// appendChild can insert the materialized enter node before this node,
// rather than at the end of the parent node.
for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) i1 = i0 + 1;
while (!(next = updateGroup[i1]) && ++i1 < dataLength);
previous._next = next || null;
}
}
}
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
}
3.4、第四步 —— 收尾
Okay, 如果上面這些程式碼都看懂了,最後一步就很簡單了 —— 新增迴圈。
;(async () => {
// ...
while (true) {
// ...
await new Promise(resolve => setTimeout(resolve, 3000));
document.body.appendChild(svg.node());
}
那麼經過以上這些步驟,我們就實現了一個簡單的 demo。透過這個 demo,可以將前面提到的一些知識點融會貫通。
四、總結
這篇文章我們花了一些篇幅來了解 D3.js
中常見的 API
及其用法。目前涉及到的部分比較少,主要是為了為下一篇帶大家繪製柱狀圖做一下鋪墊。