用D3.js 十分鐘實現字元跳動效果

ssthouse發表於2019-03-02

本文基於 D3.js 作者 Mike Bostock例子

原文分為三部分, 在這裡筆者將其整合為了一篇方便閱讀.

該效果基於 D3.js, 主要使用到了 d3-selection. 如果對d3-selection的基本使用邏輯不太清楚可以參見 這篇文章.

效果圖

  • Step1 首先程式碼會隨機生成一個字串, 該字元以綠色進入畫面.

  • Step2 接下來, 程式碼隨機生成一個新字串, 新生成的字串會和原始字串進行對比:

    2.1 新字串和原始字串中相同的字母,會變成黑色保留在螢幕上

    2.2 原始字串中有, 而新字串中沒有的字母, 會變成紅色,被移除螢幕

    2.3 新字串中有, 而原始字串中沒有的字母, 會變成綠色,被新增到螢幕

final demo

程式碼實現

1. 字元切換

第一步要完成的效果是:

  • 完成基本字元切換
  • 進入時為綠色, 不變時為黑色
  • 被移除的字元直接被從介面中移除

先上程式碼, 點我執行

<script>
var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    g = svg.append("g").attr("transform", "translate(32," + (height / 2) + ")");

function update(data) {

  // DATA JOIN
  // Join new data with old elements, if any.
  var text = g.selectAll("text")
    .data(data);

  // UPDATE
  // Update old elements as needed.
  text.attr("class", "update");

  // ENTER
  // Create new elements as needed.
  //
  // ENTER + UPDATE
  // After merging the entered elements with the update selection,
  // apply operations to both.
  text.enter().append("text")
      .attr("class", "enter")
      .attr("x", function(d, i) { return i * 32; })
      .attr("dy", ".35em")
    .merge(text)
      .text(function(d) { return d; });

  // EXIT
  // Remove old elements as needed.
  text.exit().remove();
}

// The initial display.
update(alphabet);

// Grab a random sample of letters from the alphabet, in alphabetical order.
d3.interval(function() {
  update(d3.shuffle(alphabet)
      .slice(0, Math.floor(Math.random() * 26))
      .sort());
}, 1500);

</script>
複製程式碼

程式碼不長, 接下來一步步分析程式碼邏輯:

首先, 獲取svg的寬高資訊. 並建立一個 元素用來承接接下來要建立的字元( 元素)

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    g = svg.append("g").attr("transform", "translate(32," + (height / 2) + ")");
複製程式碼

update() 方法中, 我們傳進來一個字串 data ** (data由26個字母中隨機選出一些組成). 首先我們選中所有的 text 元素並將其和data** 進行繫結:

var text = g.selectAll("text")
    .data(data);
複製程式碼

然後我們處理enter集合 (也就是新加入的字元), 為每一個新字元新增一個 text 元素, 併為其新增class屬性, 使用index為其計算出橫縱座標:

text.enter().append("text")                            // 新增svg text元素
      .attr("class", "enter")                          // 新增class屬性 (綠色)
      .attr("x", function(d, i) { return i * 32; })    // 按每個字元32畫素, 為其計算x座標
      .attr("dy", ".35em")                             // 設定y軸偏移量
複製程式碼

接下來同時處理 enterupdate 集合, 將字元資料填入 text 元素中:

  .merge(text)
      .text(function(d) { return d; });
複製程式碼

最後處理 exit集合, 我們暫時直接將其從螢幕中移除:

text.exit().remove();
複製程式碼

我們用 d3.interval(callback, timeInterval) 定時呼叫update()來實現字元重新整理. 現在我們就得到了下面的效果:

first step

2. 為字元設定key值

如果你觀察仔細的話, 你可能已經發現第一步實現的效果中: 新加的字元總是出現在最後. 這顯然不是我們想要的效果, 新加的字元出現的位置應該是隨機的. 那麼出現現在這個效果的原因是什麼呢?

答案在於我們在繫結字元資料時: data(data) 並沒有指定data的key值, 那麼d3會預設使用index作為key, 這樣就是為什麼新增加的字元總是出現在最後面.

所以我們為字元加入key值的accessor:

  var text = g.selectAll("text")
    .data(data, function(d) { return d; });
複製程式碼

現在 key 值繫結好後, 已經存在的字元在update時text已經不會再改變, 但是座標需要重新計算, 所以我們做以下改動:

text.enter().append("text")
      .attr("class", "enter")
      .attr("dy", ".35em")
      .text(function(d) { return d; })                 // 將text賦值移動到 enter中
    .merge(text)
      .attr("x", function(d, i) { return i * 32; });   // 將座標計算移動到 merge後 (enter & update)
複製程式碼

現在我們的程式碼長這樣, 點我線上執行:

<script>

var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    g = svg.append("g").attr("transform", "translate(32," + (height / 2) + ")");

function update(data) {

  // DATA JOIN
  // Join new data with old elements, if any.
  var text = g.selectAll("text")
    .data(data, function(d) { return d; });

  // UPDATE
  // Update old elements as needed.
  text.attr("class", "update");

  // ENTER
  // Create new elements as needed.
  //
  // ENTER + UPDATE
  // After merging the entered elements with the update selection,
  // apply operations to both.
  text.enter().append("text")
      .attr("class", "enter")
      .attr("dy", ".35em")
      .text(function(d) { return d; })
    .merge(text)
      .attr("x", function(d, i) { return i * 32; });

  // EXIT
  // Remove old elements as needed.
  text.exit().remove();
}

// The initial display.
update(alphabet);

// Grab a random sample of letters from the alphabet, in alphabetical order.
d3.interval(function() {
  update(d3.shuffle(alphabet)
      .slice(0, Math.floor(Math.random() * 26))
      .sort());
}, 1500);

</script>
複製程式碼

現在我們得到的效果:

second step

3.新增動畫

動畫能讓我們更好的觀察元素的變化過程和狀態, 給不同狀態的元素賦予不同的動畫可以更直觀的展示我們的資料.

現在我們給字元的變化增加動畫效果, 並把字元移除時的顏色變化補上.

首先我們定義一個 transition變數, 並設定其動畫間隔為 750

var t = d3.transition()
      .duration(750);
複製程式碼

在D3中使用動畫非常簡單, 在動畫前指定元素的一些屬性, 呼叫動畫, 再指定動畫後的一些屬性. D3會自動根據插值器生成動畫.

下面的程式碼對於離開介面的字元(exit selection)進行了處理:

text.exit()
      .attr("class", "exit")          // 動畫前, 設定class屬性, 字型變紅
    .transition(t)                    // 設定動畫
      .attr("y", 60)                  // 設定y座標, 使元素向下離開介面 (y: 0 => 60)
      .style("fill-opacity", 1e-6)    // 設定透明度, 使元素漸變消失 (opacity: 1 => 0)
      .remove();                      // 最後將其移出介面
複製程式碼

同樣的, 我們對 enterupdate selection進行處理:

// UPDATE old elements present in new data.
  text.attr("class", "update")
      .attr("y", 0)
      .style("fill-opacity", 1)
    .transition(t)
      .attr("x", function(d, i) { return i * 32; });

  // ENTER new elements present in new data.
  text.enter().append("text")
      .attr("class", "enter")
      .attr("dy", ".35em")
      .attr("y", -60)
      .attr("x", function(d, i) { return i * 32; })
      .style("fill-opacity", 1e-6)
      .text(function(d) { return d; })
    .transition(t)
      .attr("y", 0)
      .style("fill-opacity", 1);
複製程式碼

最終我們的程式碼長這樣, 點我執行

<script>

var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    g = svg.append("g").attr("transform", "translate(32," + (height / 2) + ")");

function update(data) {
  var t = d3.transition()
      .duration(750);

  // JOIN new data with old elements.
  var text = g.selectAll("text")
    .data(data, function(d) { return d; });

  // EXIT old elements not present in new data.
  text.exit()
      .attr("class", "exit")
    .transition(t)
      .attr("y", 60)
      .style("fill-opacity", 1e-6)
      .remove();

  // UPDATE old elements present in new data.
  text.attr("class", "update")
      .attr("y", 0)
      .style("fill-opacity", 1)
    .transition(t)
      .attr("x", function(d, i) { return i * 32; });

  // ENTER new elements present in new data.
  text.enter().append("text")
      .attr("class", "enter")
      .attr("dy", ".35em")
      .attr("y", -60)
      .attr("x", function(d, i) { return i * 32; })
      .style("fill-opacity", 1e-6)
      .text(function(d) { return d; })
    .transition(t)
      .attr("y", 0)
      .style("fill-opacity", 1);
}

// The initial display.
update(alphabet);

// Grab a random sample of letters from the alphabet, in alphabetical order.
d3.interval(function() {
  update(d3.shuffle(alphabet)
      .slice(0, Math.floor(Math.random() * 26))
      .sort());
}, 1500);

</script>
複製程式碼

最終效果:

final demo

想繼續瞭解 D3.js ?

這裡是我的 D3.js資料視覺化 的github 地址, 歡迎 start & fork :tada:

D3-blog

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

github主頁

知乎專欄

掘金

相關文章