Chapter 2. The Enter Selection(輸入選擇)
示例1:創建地鐵線路狀態公告欄
閒言少敘,來看本書的第一個坑,還是大坑:重要概念 enter selection(輸入選擇)。
示例的場景很簡單:根據清洗好的 JSON
數據源,羅列出紐約各個地鐵線的狀態信息,比如是否正常運行、是否計劃運營等等;然後對不同的狀態設置相應的 CSS 樣式,效果圖如下:
代碼也挺簡單的,順帶提到了一個 D3 慣用的【方法鏈式】編碼風格(小瀑布式寫法?(cascade)):
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<script src="js/d3.js"></script>
<script>
function draw(data) {
"use strict";
d3.select('body')
.append('ul')
.selectAll('li')
.data(data)
.enter()
.append('li')
// populate content
.text(d => `${d.name}: ${d.status}`)
// different styles
.style('font-weight', d =>
d.status == 'GOOD SERVICE' ? 'normal' : 'bold')
}
// v5.15.1:
d3.json("data/service_status.json").then(draw);
</script>
</body>
</html>
真正的坑,出現在作者對這段代碼的講解上。原諒我只憑書中的描述,難以領會作者的深意。後來才發現,必須訪問書中提示欄(如下圖所示)給出的 鏈接文章,並且再結合那篇文章末尾的補遺位置給出的 示例鏈接,才能完全搞明白 Mike 大神說的啥。
感興趣的親們可以去看看原文。要徹底搞懂本章標題中的 Enter Selection 爲何物,必須結合一個重要配圖:
鏈接文章說,D3 在繪製頁面元素時,只需要告知其數據元素(狀態信息)與作圖元素(li
)間的對應關係即可,這在 D3 中稱爲【數據連接】(data join)。結合第 L12-15 行代碼:
.selectAll('li')
.data(data)
.enter()
.append('li')
接下來是真正的關鍵信息:
.selectAll('li')
返回一個【空選中】(empty selection),這個【空選中】會與一個新的數據數組 連接 在一起,得到三個新 選中,對應上圖那三個狀態:enter(輸入)、update(更新)、exit(退出)。鑑於剛纔的【空選中】沒有任何頁面元素,update 和 exit 選中也是一個【空選中】;而左邊那個 enter 選中,則包含了一組 佔位符,每個 佔位符 與後續即將渲染的各個數據元素一一對應。根據小冊子中的解釋,此時這些佔位符都是空的,沒有綁定任何數據;雖然是空的,但可以通過【空選中】的.data()
方法接收數據,即後面的第二行代碼;.data(data)
返回上圖中的 update 選中,此時 enter 與 exit 選中阻斷了 update 選中的後續操作;.enter()
是 update 選中的方法,返回一個 enter 狀態的選中(即本章標題中的 輸入選擇)。此時,各佔位上已經有了帶渲染的數據;.append('li')
是在 enter 選中上的方法,作用是將所有待渲染的作圖元素li
,按數據源中的元素順序,逐一追加渲染到頁面的ul
容器元素內。
再配合後面兩句設置,纔有了最終的頁面效果。
數據連接的設計初衷
代碼倒是解釋完了,新的問題又出來了:
爲什麼要繞這麼大一圈呢?直接在
.data()
方法中搞定一切、或者直接給一個封裝好的方法不好嗎?
這篇由 D3 創始人親自執筆的文章給出的解釋是:
**The beauty of the data join is that it generalizes. **
翻譯過來就是:數據連接的終極奧義,在於它是泛化的、通用的、普適的。(說了當沒說)
結合後面的幾個例子,才知道這樣的設計不僅適合類似示例這樣只考慮 enter 選中的小兒科型應用場景,也適合動態的、實時的數據可視化展示——
例1:動態增刪元素
var circle = svg.selectAll("circle")
.data(data);
circle.exit().remove();
circle.enter().append("circle")
.attr("r", 2.5)
.merge(circle)
.attr("cx", d => d.x)
.attr("cy", d => d.y);
這段代碼會重新選中頁面 svg
元素內的所有 circle
元素。當新的數據集少於當前數據集,多出的部分會進入 exit 狀態,進而被最終刪除;反之,如果新數據集大於當前數據集,則新多出的部分會進入 enter 狀態,進而渲染到頁面;如果前後兩個數據集一樣大,則只執行數據對應位置的更新,不存在頁面元素的增刪環節。
例2:提升頁面渲染性能
此外,從數據連接的角度考慮問題,還能使編碼風格將更趨於聲明式:只需要對 enter、update、exit 這三個狀態實現交互、過渡、動畫等各類可視化效果即可,省去了具體的 if
分支或 for
循環細節。比如,可以在 enter 階段而非 update 階段給 circle
元素的半徑賦一個常量值,達到最小化 DOM 操作的效果,極大地提升頁面渲染的性能。
circle.enter().append("circle")
.attr("r", 0)
例3:針對狀態設置過渡
// Expand-in:
circle.enter()
.append("circle")
.attr("r", 0)
.transition()
.attr("r", 2.5);
// Shrink-out:
circle.exit()
.transition()
.attr("r", 0)
.remove();
例子倒是給出了不少,但對我這樣的重度強迫症已棄療者來說,百聞不如一見——沒有見到實際的效果,總有點小遺憾。誰曾想,在文末的附錄補遺的位置,看到了大神八年前留下的文末彩蛋(要不要這麼心有靈犀啊😂😂😂)
不看不知道,一看嚇一跳,這是一個帶實時編輯預覽功能的 D3 可視化實驗室,用 GitHub 帳號就可以登錄,叉取自己感興趣的示例文章。8 年前的這篇 通用更新模式 已經在 2019 年 1 月 23 日被 Mike Bostock 大神更新到了 新文章 上,看得我那叫個過癮啊。這不就是 D3.js
版的 字符跳動效果 的手把手教程嗎。代碼的簡潔、工整、高效讓人折服!不知道國內是否有這樣的模擬環境,母語爲英語的崽兒們簡直太幸福了。。。
靜態效果:
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", 33)
.attr("viewBox", `0 -20 ${width} 33`);
svg.selectAll("text")
.data(randomLetters())
.join("text")
.attr("x", (d, i) => i * 16)
.text(d => d);
return svg.node();
}
每 2.5 秒動態更新一次:
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", 33)
.attr("viewBox", `0 -20 ${width} 33`);
while (true) {
svg.selectAll("text")
.data(randomLetters())
.join("text")
.attr("x", (d, i) => i * 16)
.text(d => d);
yield svg.node();
await Promises.tick(2500);
}
}
.join()
還可以接收三個回調函數,分別對應 enter、update、exit 三個選中狀態,例如:
動態字符跳動效果:
{
function randomLetters() {
return d3.shuffle("abcdefghijklmnopqrstuvwxyz".split(""))
.slice(0, Math.floor(6 + Math.random() * 20))
.sort();
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", 33)
.attr("viewBox", `0 -20 ${width} 33`);
while (true) {
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", "black")
.attr("y", 0)
.call(update => update.transition(t)
.attr("x", (d, i) => i * 16)),
exit => exit
.attr("fill", "brown")
.call(exit => exit.transition(t)
.attr("y", 30)
.remove())
);
yield svg.node();
await Promises.tick(2500);
}
}
最後一個比較有意思的地方,是隨機英文字母序列的實現,用到了 d3.shuffle(array)
:
function randomLetters() {
return d3.shuffle("abcdefghijklmnopqrstuvwxyz".split(""))
.slice(0, Math.floor(6 + Math.random() * 20))
.sort();
}
也可以用 ES6 的 Array.from()
實現:
const randomLetters = () => d3.shuffle(Array.from({length: 26}, (e, i) =>
String.fromCharCode(i + 97)))
.slice(0, Math.floor(6 + Math.random() * 20))
.sort();
示例2:繪製露天廣場的日平均流量
(更新中)