譯者注:
原文: Mike Bostock (D3.js 作者) -- Nested Selections
譯者: ssthouse
本文講解的是關於 D3.js 中 d3-selection 的使用. d3-selection 是 d3 的核心所在, 它提供了一種和以往 Dom 操作 和 資料操作 完全不同的思路, 讓我們能非常優雅的進行資料視覺化工作. 本文是 d3 作者對於 d3-selection 中 巢狀選擇集 的講解, 本人閱讀後覺得很有啟發, 所以翻譯成中文, 希望對讀者也能有所幫助.
譯文
D3 的 選擇集是分層級的, 就像 Dom 元素和資料集是可以有層級的一樣. 比如說 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, 你會如何操作? 直接使用 d3.selectAll('td')
顯然會選中所有的 td 元素(包括 thead 和 tbody). 如果想只選中存在與 B 元素中的 A 元素, 你需要這樣操作:
var td = d3.selectAll('tbody td')
複製程式碼
除了上面那種方法, 你還可以先選中 tbody, 再選中 td 元素, 像這樣:
var td = d3.select('tbody').selectAll('td')
複製程式碼
因為 selectAll 會對當前選擇集中的每個元素(如: tbody)選中其符合條件的子元素(如: td). 這種方法會得到和 d3.selectAll('tbody td')
相同的結果. 但如果你之後想要對同一父元素的選擇集引申出更多的選擇集的話 (比如區分選中 table 中的奇數行選擇集和偶數行選擇集), 這種巢狀選擇集方式會方便的多.
巢狀中索引的使用
接著上面的例子, 如果你使用 d3.selectAll('td')
, 你會得到一個平展的選擇集, 像這樣:
var td = d3.selectAll('tbody td')
複製程式碼
平展的選擇集缺少了層級結構: 不論是 thead 中的 td 還是 tbody 中的 td 全都被展開成了一個陣列, 而不是以父元素進行分組. 這讓我們想對每一行或是每一列的 td 進行操作變得很困難. 與此相反的, D3 的巢狀選擇集能儲存層級關係. 比如我們想以行的方式對選擇集分組, 我們首先選中 tr 元素. 然後選中 td 元素.
var td = d3.selectAll('tbody tr').selectAll('td')
複製程式碼
現在, 如果你想讓第一列的所有元素變紅, 你可以利用 index 引數 i:
td.style('color', function(d, i) {
return i ? null : 'red'
})
複製程式碼
上面的引數 i 是 td 元素在 tr 中的索引, 你還可以通過新增一個 j 引數來獲得當前的行數的索引. 像這樣:
td.style('color', function(d, i, j) {
console.log(`current row: ${j}`)
return i ? null : 'red'
})
複製程式碼
巢狀和資料間的關聯
層級結構的 Dom 元素常常是由層級結構的資料來驅動的. 而層級的選擇集能更方便的處理資料繫結. 繼續上面的例子, 你可以把 table 的資料表示為一個矩陣:
var matrix = [
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15]
]
複製程式碼
為了讓這些資料繫結上對應的 td 元素, 我們首先將矩陣的每一行和 tr 繫結對應起來, 然後再將矩陣中一行的每一個元素和 tr 中的每一個 td 繫結起來:
var td = d3
.selectAll('tbody tr')
.data(matrix)
.selectAll('td')
.data(function(d, i) {
return d
}) // d is matrix[i]
複製程式碼
需要注意的是, data()
不僅可以傳入一個資料, 它還可以傳入一個 返回一個陣列的 function. 平展的選擇集通常對應的是單個陣列, 是因為平展的選擇集只有一個 group.
上面的 row 選擇集是一個平展的選擇集, 因為它是直接由 d3.selectAll 建立的:
var tr = d3.selectAll('tbody tr').data(matrix)
複製程式碼
而 td 的選擇集是巢狀的:
var td = tr.selectAll('td').data(function(d) {
return d
}) // matrix[i]
複製程式碼
data 傳入的 操作函式給每一個 group 繫結了一個陣列資料. d3 會對每一行 tr 呼叫操作函式. 因為父元素資料是矩陣, 所以操作函式在每次被呼叫時只是簡單的返回矩陣中當前行的資料, 來和 tr 進行繫結.
巢狀中父節點作用
巢狀選擇集有一個微妙但可能造成嚴重影響的副作用: 它會給每個 group 設定父節點. 父節點是選擇集的一個隱藏屬性, 它會在被呼叫 append 方法時使用, 將子元素新增到父節點的 Dom 元素當中. 比如: 如果你想通過下面的方式進行資料繫結操作, 你會得到一個 error:
d3.selectAll('table tr')
.data(matrix)
.enter()
.append('tr') // error!
複製程式碼
上面的程式碼之所以會報錯, 是因為預設的父節點是 html 元素, 你不能直接將 tr 元素新增到 html 元素中. 所以, 我們應該在進行資料繫結前, 先選擇好父節點:
d3.select('table')
.selectAll('tr')
.data(matrix)
.enter()
.append('tr') // success
複製程式碼
這種方式可以用來選擇任意層級的巢狀選擇集. 比如你想從頭建立一個 table, 並填入上面矩陣中的資料, 你可以首選選中 body 元素:
var body = d3.select('body')
複製程式碼
接下來在父節點 body 中新增一個 table:
var table = body.append('table')
複製程式碼
接下來繫結矩陣資料, 建立 tr 元素. 因為 selectAll 是在 table 元素上進行呼叫的, 所以父節點是 table:
var tr = table
.selectAll('tr')
.data(matrix)
.enter()
.append('tr')
複製程式碼
最後我們以 tr 作為父節點, 建立 td 元素:
var td = tr
.selectAll('td')
.data(function(d) {
return d
})
.enter()
.append('td')
複製程式碼
要不要使用巢狀選擇集 ?
在 D3 中, select 和 selectAll 有一個很重要的區別: select 會繼續使用當前存在的 group, 而 selectAll 總是會建立新的 group. 因此呼叫 select 能儲存原有 selection 的資料, 索引位置, 甚至父節點.
比如, 下面的平展的選擇集, 它的父節點仍然是 html 節點:
var td = d3.selectAll('tbody tr').select('td')
複製程式碼
想要得到巢狀的選擇集, 唯一的方法就是在已有的選擇集的基礎上, 呼叫 selectAll 方法. 這就是為什麼資料繫結總是出現在 selectAll 之後, 而不是 select 之後.
這篇文章使用 table 作為例子僅僅是為了方便講解層級結構. 其實 table 的使用並不是特別具有典型性. 其實還有許多其它 巢狀選擇集的例子(點我檢視, 點我檢視)
就像 用 join 的方式思考 一樣, 巢狀選擇集同樣使用了一種思想上完全不同的處理 Dom 元素 和 資料 的思路. 這種思路剛開始可能很難理解, 但你一旦掌握了, 你就能駕輕就熟的使用 D3.js 來完成你的資料視覺化任務了.