[譯] D3.js 巢狀選擇集 (Nested Selection)

ssthouse發表於2018-06-26

譯者注:

原文: 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') , 你會得到一個平展的選擇集, 像這樣:

flat selection

var td = d3.selectAll('tbody td')
複製程式碼

平展的選擇集缺少了層級結構: 不論是 thead 中的 td 還是 tbody 中的 td 全都被展開成了一個陣列, 而不是以父元素進行分組. 這讓我們想對每一行或是每一列的 td 進行操作變得很困難. 與此相反的, D3 的巢狀選擇集能儲存層級關係. 比如我們想以行的方式對選擇集分組, 我們首先選中 tr 元素. 然後選中 td 元素.

nested selection

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 建立的:

tr flat selection

var tr = d3.selectAll('tbody tr').data(matrix)
複製程式碼

而 td 的選擇集是巢狀的:

tr nested selection

var td = tr.selectAll('td').data(function(d) {
  return d
}) // matrix[i]
複製程式碼

data 傳入的 操作函式給每一個 group 繫結了一個陣列資料. d3 會對每一行 tr 呼叫操作函式. 因為父元素資料是矩陣, 所以操作函式在每次被呼叫時只是簡單的返回矩陣中當前行的資料, 來和 tr 進行繫結.

巢狀中父節點作用

巢狀選擇集有一個微妙但可能造成嚴重影響的副作用: 它會給每個 group 設定父節點. 父節點是選擇集的一個隱藏屬性, 它會在被呼叫 append 方法時使用, 將子元素新增到父節點的 Dom 元素當中. 比如: 如果你想通過下面的方式進行資料繫結操作, 你會得到一個 error:

wrong append operation

d3.selectAll('table tr')
  .data(matrix)
  .enter()
  .append('tr') // error!
複製程式碼

上面的程式碼之所以會報錯, 是因為預設的父節點是 html 元素, 你不能直接將 tr 元素新增到 html 元素中. 所以, 我們應該在進行資料繫結前, 先選擇好父節點:

choose parent node before append operation

d3.select('table')
  .selectAll('tr')
  .data(matrix)
  .enter()
  .append('tr') // success
複製程式碼

這種方式可以用來選擇任意層級的巢狀選擇集. 比如你想從頭建立一個 table, 並填入上面矩陣中的資料, 你可以首選選中 body 元素:

create table manually

var body = d3.select('body')
複製程式碼

接下來在父節點 body 中新增一個 table:

append table to body

var table = body.append('table')
複製程式碼

接下來繫結矩陣資料, 建立 tr 元素. 因為 selectAll 是在 table 元素上進行呼叫的, 所以父節點是 table:

append tr to table

var tr = table
  .selectAll('tr')
  .data(matrix)
  .enter()
  .append('tr')
複製程式碼

最後我們以 tr 作為父節點, 建立 td 元素:

append td to tr

var td = tr
  .selectAll('td')
  .data(function(d) {
    return d
  })
  .enter()
  .append('td')
複製程式碼

要不要使用巢狀選擇集 ?

在 D3 中, select 和 selectAll 有一個很重要的區別: select 會繼續使用當前存在的 group, 而 selectAll 總是會建立新的 group. 因此呼叫 select 能儲存原有 selection 的資料, 索引位置, 甚至父節點.

比如, 下面的平展的選擇集, 它的父節點仍然是 html 節點:

parent node is html

var td = d3.selectAll('tbody tr').select('td')
複製程式碼

想要得到巢狀的選擇集, 唯一的方法就是在已有的選擇集的基礎上, 呼叫 selectAll 方法. 這就是為什麼資料繫結總是出現在 selectAll 之後, 而不是 select 之後.

這篇文章使用 table 作為例子僅僅是為了方便講解層級結構. 其實 table 的使用並不是特別具有典型性. 其實還有許多其它 巢狀選擇集的例子(點我檢視, 點我檢視)

就像 用 join 的方式思考 一樣, 巢狀選擇集同樣使用了一種思想上完全不同的處理 Dom 元素資料 的思路. 這種思路剛開始可能很難理解, 但你一旦掌握了, 你就能駕輕就熟的使用 D3.js 來完成你的資料視覺化任務了.

如果覺得不錯的話, 不妨點下面的連結關注一下 : )

github 主頁

知乎專欄

掘金

相關文章