【譯】Learn D3 入門文件:Joins

XXHolic發表於2021-09-25

引子

Learn D3: Animation 第七篇,只是英文翻譯,可修改程式碼的部分用靜態圖片替代了,想要實時互動請閱讀原文。

正文

如果你熟悉此教程中的 D3 ,你可能會驚訝於 D3 選擇器被提及的如此之少。

那是因為你可能不需要它們!

D3 選擇器適用一個特殊的定位:快速,動態圖表的增量更新。如果你關注的是靜態圖表,或者每幀都可以從頭開始重新繪製的圖表,那麼你可能更喜歡不同的抽象。另一方面,如果你想要動畫過渡或從現代瀏覽器中榨取最佳效能,選擇器適合你。

(即使你決定不使用選擇器,請記住 D3 還有大量其它有用的視覺化工具。比例尺、形狀、插值器、顏色、地圖投影和許多其它功能可用於 Canvas、WebGL 或其它 DOM 抽象,如 Observable 的 HTML 標記模板文字、React 或 Svelte 。D3 還可以使用統計、分組和聚合、時間序列和解析的方法,協助資料清理和分析。)

本質上,D3 選擇器指定的是轉換而不是表示。你可以指定將當前狀態轉換為所需狀態所做的更改(插入、更新和刪除),而不是表示圖表(DOM)的所需狀態。這有時很乏味,但允許你設定過渡動畫,並最小化對 DOM 的更改,從而提高效能。

讓我們看看如何做。

假設我們想顯示字母表中的字母。雖然不是很直觀,但我們將保持簡單,以專注於技術(有關實際的示例,請參見 D3 相簿 的動畫和互動部分。)

92-1

這個圖表是靜態的,因為它是在每次單元執行時從頭開始建立的,這使得 D3 程式碼在本質上等同於 HTML 文字。

92-2

那麼,何必要用選擇器呢?對,對於靜態圖表,沒有什麼理由這樣做。

但假設你想更新此圖表以響應不斷變化的資料。並且你不想從頭開始重新繪製,希望應用最小的更新集來反映新資料。你希望重用現有元素,新增需要的元素,刪除不需要的元素。只需將上述程式碼移動到一個在資料更改時呼叫的方法中,你就可以獲得高效能的增量更新!?

92-3

讓我們把程式碼分析一下。

text 是一組文字元素,最初為空,其父元素是 SVG 元素。這個父項決定了輸入的文字元素稍後將附加到何處。

通過呼叫 selection.data ,text 被繫結到一個新的資料陣列 letters 。這將計算 text 選擇集的三個子集:enter 選擇集表示在新資料中,去除已存在元素與新資料的交集之後,剩下的資料;update 選擇集表示新資料與已存在元素的交集;exit 選擇集表示在已存在元素中,去除已存在元素與新資料的交集之後,剩下的元素。

作為圖示如下:

92-4

(這些選擇集隱藏在程式碼中:selection.data 返回 update 選擇集,從中可以呼叫 selection.enter 或 selection.exit 訪問其它選擇集。)

我們可以手動處理這三種情況,但 selection.join 本就能方便的處理。enter 選擇集新增進來;exit 選擇集被移除;最後, updateenter 選擇集被合併、排序並返回。然後,我們可以為這些加入或更新元素分配屬性和文字。

我們觀察到只要字母和文字元素之間的關聯保持不變,就不需要在更新元素時重新分配某些屬性和文字內容,這樣就可以更高效。要保留此關聯,selection.data 需要一個鍵函式;為了對 enterupdateexit 的精確操作,selection.join 需要相應的函式。如果 update 比 enter 和 exit 更常見,這將大大提高效能!

92-5

與前面一樣,selection.join 返回合併的 enter 和 update 選擇集,因此我們可以共享應用於兩者的程式碼,例如設定 x 屬性。

傳遞給 selection.data 的鍵函式用於計算每個新資料和已選元素資料的(字串)鍵,確定哪個資料繫結到哪個元素:如果元素和資料具有相同的鍵,則該資料繫結到元素,並且元素被放入 update 選擇集。字母是很好的鍵,因此標識函式(d => d)在這裡是合適的。

(如果未指定鍵函式,則資料由索引繫結:第一個資料繫結到第一個元素,依此類推。作為練習,嘗試重寫上面的程式碼以通過索引聯接。你將會也想把設定 x 屬性與設定文字內容交換!)

然而,選擇器真正閃耀的地方是過渡!✨

在下面,字母從頂部進入,更新時水平滑動,然後從底部退出。這比上面的瞬時轉變更容易理解。

92-6

好的過渡不僅僅是為了吸引注意力而讓圖表“舞動”;它們幫助觀看者通過變動了解資料是如何變化的

良好的過渡保持物件的恆定性:在過渡之前表示特定事物(如字母 C )的圖表元素應在整個過渡過程中和過渡之後表示相同的事物,從而允許觀看者連續跟蹤。相反,如果給定元素的含義在過渡期間發生變化,那麼變動將毫無意義。

舉個更實際的例子怎麼樣?

下圖顯示了按特定年齡組人口百分比排列的前十個州(以及華盛頓特區,外地居住者)。這表明猶他州的年輕人口比例過高,反映了 LDS 教會對養育家庭的重視。相比之下,佛羅里達州退休人口眾多,許多人都在 70 歲或以上。

當你更改選定的年齡組時,請觀察圖表如何重新排序以反映排名的變化。x 軸同時重新縮放以適應新資料。

92-7

chart = {
  const svg = d3.create("svg")
      .attr("viewBox", [0, 0, width, height]);
  console.info(data)

  // For the initial render, reference the current age non-reactively.
  const agedata = viewof agedata.value;

  const x = d3.scaleLinear()
      .domain([0, d3.max(agedata, d => d.value)])
      .rangeRound([margin.left, width - margin.right]);

  const y = d3.scaleBand()
      .domain(agedata.map(d => d.name))
      .rangeRound([margin.top, margin.top + 20 * data.names.length]);

  let bar = svg.append("g")
      .attr("fill", "steelblue")
    .selectAll("rect")
    .data(agedata, d => d.name)
    .join("rect")
      .style("mix-blend-mode", "multiply")
      .attr("x", x(0))
      .attr("y", d => y(d.name))
      .attr("width", d => x(d.value) - x(0))
      .attr("height", y.bandwidth() - 1);

  const gx = svg.append("g")
      .call(xAxis, x);

  const gy = svg.append("g")
      .call(yAxis, y);

  return Object.assign(svg.node(), {
    update(agedata) {
      const t = svg.transition().duration(750);

      gx.transition(t)
          .call(xAxis, x.domain([0, d3.max(agedata, d => d.value)]));

      gy.transition(t)
          .call(yAxis, y.domain(agedata.map(d => d.name)));

      bar = bar
        .data(agedata, d => d.name)
        .call(bar => bar.transition(t)
          .attr("width", d => x(d.value) - x(0))
          .attr("y", d => y(d.name)));
    }
  });
}

92-9

只有前 10 條可見,因為其餘的隱藏在圖表下方。因此,selection.join 是不需要的,因為沒有條形加入或移除,它們只會更新。這不僅簡化了程式碼,而且使過渡更有意義,因為進入或退出條形圖的速度現在暗示它們在展示外的位置。

動畫過渡通常由讀者點選或在尋找答案時點選觸發。接下來,讓我們看看如何讓圖表響應此類查詢。

Next

附錄

92-10

根據原始碼,去除了平臺依賴,提取了主要程式碼,有以下示例:

參考資料

相關文章