[譯] D3.js 嵌套選擇集 (Nested Selection)
譯者注:
原文: Mike Bostock (D3.js 作者) -- Nested Selections
譯者: ssthouse
本文講解的是關(guān)于 D3.js 中 d3-selection 的使用. d3-selection 是 d3 的核心所在, 它提供了一種和以往 Dom 操作 和 數(shù)據(jù)操作 完全不同的思路, 讓我們能非常優(yōu)雅的進(jìn)行數(shù)據(jù)可視化工作.
本文是 d3 作者對(duì)于 d3-selection 中 嵌套選擇集 的講解, 本人閱讀后覺得很有啟發(fā), 所以翻譯成中文, 希望對(duì)讀者也能有所幫助.
譯文
D3 的 選擇集是分層級(jí)的, 就像 Dom 元素和數(shù)據(jù)集是可以有層級(jí)的一樣. 比如說 Table:
<table>
<thead>
<tr><td> A</td><td> B</td><td> C</td><td> D</td></tr>
</thead>
<tbody>
<tr><td> 0</td><td> 1</td><td> 2</td><td> 3</td></tr>
<tr><td> 4</td><td> 5</td><td> 6</td><td> 7</td></tr>
<tr><td> 8</td><td> 9</td><td> 10</td><td> 11</td></tr>
<tr><td> 12</td><td> 13</td><td> 14</td><td> 15</td></tr>
</tbody>
</table>
如果讓你只選中 tbody 元素中的 td, 你會(huì)如何操作? 直接使用 d3.selectAll('td') 顯然會(huì)選中所有的 td 元素(包括 thead 和 tbody). 如果想只選中存在與 B 元素中的 A 元素, 你需要這樣操作:
var td = d3.selectAll('tbody td')
除了上面那種方法, 你還可以先選中 tbody, 再選中 td 元素, 像這樣:
var td = d3.select('tbody').selectAll('td')
因?yàn)?selectAll 會(huì)對(duì)當(dāng)前選擇集中的每個(gè)元素(如: tbody)選中其符合條件的子元素(如: td). 這種方法會(huì)得到和 d3.selectAll('tbody td') 相同的結(jié)果. 但如果你之后想要對(duì)同一父元素的選擇集引申出更多的選擇集的話 (比如區(qū)分選中 table 中的奇數(shù)行選擇集和偶數(shù)行選擇集), 這種嵌套選擇集方式會(huì)方便的多.
嵌套中索引的使用
接著上面的例子, 如果你使用 d3.selectAll('td') , 你會(huì)得到一個(gè)平展的選擇集, 像這樣:
[圖片上傳失敗...(image-6493d1-1530010072875)]
var td = d3.selectAll('tbody td')
平展的選擇集缺少了層級(jí)結(jié)構(gòu): 不論是 thead 中的 td 還是 tbody 中的 td 全都被展開成了一個(gè)數(shù)組, 而不是以父元素進(jìn)行分組. 這讓我們想對(duì)每一行或是每一列的 td 進(jìn)行操作變得很困難. 與此相反的, D3 的嵌套選擇集能保存層級(jí)關(guān)系. 比如我們想以行的方式對(duì)選擇集分組, 我們首先選中 tr 元素. 然后選中 td 元素.
[圖片上傳失敗...(image-16b4f6-1530010072875)]
var td = d3.selectAll('tbody tr').selectAll('td')
現(xiàn)在, 如果你想讓第一列的所有元素變紅, 你可以利用 index 參數(shù) i:
td.style('color', function(d, i) {
return i ? null : 'red'
})
上面的參數(shù) i 是 td 元素在 tr 中的索引, 你還可以通過添加一個(gè) j 參數(shù)來獲得當(dāng)前的行數(shù)的索引. 像這樣:
td.style('color', function(d, i, j) {
console.log(`current row: ${j}`)
return i ? null : 'red'
})
嵌套和數(shù)據(jù)間的關(guān)聯(lián)
層級(jí)結(jié)構(gòu)的 Dom 元素常常是由層級(jí)結(jié)構(gòu)的數(shù)據(jù)來驅(qū)動(dòng)的. 而層級(jí)的選擇集能更方便的處理數(shù)據(jù)綁定.
繼續(xù)上面的例子, 你可以把 table 的數(shù)據(jù)表示為一個(gè)矩陣:
var matrix = [
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15]
]
為了讓這些數(shù)據(jù)綁定上對(duì)應(yīng)的 td 元素, 我們首先將矩陣的每一行和 tr 綁定對(duì)應(yīng)起來, 然后再將矩陣中一行的每一個(gè)元素和 tr 中的每一個(gè) td 綁定起來:
var td = d3
.selectAll('tbody tr')
.data(matrix)
.selectAll('td')
.data(function(d, i) {
return d
}) // d is matrix[i]
需要注意的是, data() 不僅可以傳入一個(gè)數(shù)據(jù), 它還可以傳入一個(gè) 返回一個(gè)數(shù)組的 function. 平展的選擇集通常對(duì)應(yīng)的是單個(gè)數(shù)組, 是因?yàn)槠秸沟倪x擇集只有一個(gè) group.
上面的 row 選擇集是一個(gè)平展的選擇集, 因?yàn)樗侵苯佑?d3.selectAll 創(chuàng)建的:
[圖片上傳失敗...(image-9b5f49-1530010072875)]
var tr = d3.selectAll('tbody tr').data(matrix)
而 td 的選擇集是嵌套的:
[圖片上傳失敗...(image-b51862-1530010072875)]
var td = tr.selectAll('td').data(function(d) {
return d
}) // matrix[i]
data 傳入的 操作函數(shù)給每一個(gè) group 綁定了一個(gè)數(shù)組數(shù)據(jù). d3 會(huì)對(duì)每一行 tr 調(diào)用操作函數(shù). 因?yàn)楦冈財(cái)?shù)據(jù)是矩陣, 所以操作函數(shù)在每次被調(diào)用時(shí)只是簡(jiǎn)單的返回矩陣中當(dāng)前行的數(shù)據(jù), 來和 tr 進(jìn)行綁定.
嵌套中父節(jié)點(diǎn)作用
嵌套選擇集有一個(gè)微妙但可能造成嚴(yán)重影響的副作用: 它會(huì)給每個(gè) group 設(shè)置父節(jié)點(diǎn). 父節(jié)點(diǎn)是選擇集的一個(gè)隱藏屬性, 它會(huì)在被調(diào)用 append 方法時(shí)使用, 將子元素添加到父節(jié)點(diǎn)的 Dom 元素當(dāng)中. 比如: 如果你想通過下面的方式進(jìn)行數(shù)據(jù)綁定操作, 你會(huì)得到一個(gè) error:
[圖片上傳失敗...(image-61adb-1530010072875)]
d3.selectAll('table tr')
.data(matrix)
.enter()
.append('tr') // error!
上面的代碼之所以會(huì)報(bào)錯(cuò), 是因?yàn)槟J(rèn)的父節(jié)點(diǎn)是 html 元素, 你不能直接將 tr 元素添加到 html 元素中. 所以, 我們應(yīng)該在進(jìn)行數(shù)據(jù)綁定前, 先選擇好父節(jié)點(diǎn):
[圖片上傳失敗...(image-56bf04-1530010072875)]
d3.select('table')
.selectAll('tr')
.data(matrix)
.enter()
.append('tr') // success
這種方式可以用來選擇任意層級(jí)的嵌套選擇集. 比如你想從頭創(chuàng)建一個(gè) table, 并填入上面矩陣中的數(shù)據(jù), 你可以首選選中 body 元素:
[圖片上傳失敗...(image-5b5d98-1530010072875)]
var body = d3.select('body')
接下來在父節(jié)點(diǎn) body 中添加一個(gè) table:
[圖片上傳失敗...(image-691dac-1530010072875)]
var table = body.append('table')
接下來綁定矩陣數(shù)據(jù), 創(chuàng)建 tr 元素. 因?yàn)?selectAll 是在 table 元素上進(jìn)行調(diào)用的, 所以父節(jié)點(diǎn)是 table:
[圖片上傳失敗...(image-34a1f4-1530010072875)]
var tr = table
.selectAll('tr')
.data(matrix)
.enter()
.append('tr')
最后我們以 tr 作為父節(jié)點(diǎn), 創(chuàng)建 td 元素:
[圖片上傳失敗...(image-add8e2-1530010072875)]
var td = tr
.selectAll('td')
.data(function(d) {
return d
})
.enter()
.append('td')
要不要使用嵌套選擇集 ?
在 D3 中, select 和 selectAll 有一個(gè)很重要的區(qū)別: select 會(huì)繼續(xù)使用當(dāng)前存在的 group, 而 selectAll 總是會(huì)創(chuàng)建新的 group. 因此調(diào)用 select 能保存原有 selection 的數(shù)據(jù), 索引位置, 甚至父節(jié)點(diǎn).
比如, 下面的平展的選擇集, 它的父節(jié)點(diǎn)仍然是 html 節(jié)點(diǎn):
[圖片上傳失敗...(image-d0df1-1530010072875)]
var td = d3.selectAll('tbody tr').select('td')
想要得到嵌套的選擇集, 唯一的方法就是在已有的選擇集的基礎(chǔ)上, 調(diào)用 selectAll 方法. 這就是為什么數(shù)據(jù)綁定總是出現(xiàn)在 selectAll 之后, 而不是 select 之后.
這篇文章使用 table 作為例子僅僅是為了方便講解層級(jí)結(jié)構(gòu). 其實(shí) table 的使用并不是特別具有典型性. 其實(shí)還有許多其它 嵌套選擇集的例子(點(diǎn)我查看, 點(diǎn)我查看)
就像 用 join 的方式思考 一樣, 嵌套選擇集同樣使用了一種思想上完全不同的處理 Dom 元素 和 數(shù)據(jù) 的思路. 這種思路剛開始可能很難理解, 但你一旦掌握了, 你就能駕輕就熟的使用 D3.js 來完成你的數(shù)據(jù)可視化任務(wù)了.
想繼續(xù)了解 D3.js ?
這里是我的 D3.js 、 數(shù)據(jù)可視化 的github 地址, 歡迎 start & fork :tada: