Vincent Ko

VK's Blog

D3.js 動畫

D3.js 提供了多種工具支持數據可視化的互動,其中d3.transition讓簡單而高效的為圖像添加動畫成為了可能。

單單從 API 來講,d3.transition非常簡單,用法類似 Jquery。 但是想要設計出理想的動畫效果,就不得不提到 D3 繪製圖形的一個核心概念General Update Pattern。D3 的數據驅動特性的核心和實現就是依靠這個 Pattern,而動畫和互動自然要從它說起了。

並不是所有圖形都必須遵循 Update Pattern,比如一次性繪圖,無互動的靜態圖形等。但如果涉及到了動態數據,這個 Update Pattern 不僅利於寫出易於維護的代碼,也能更好的發揮 D3 強大的功能。

General Update Pattern#

image-20200308211328582

D3 的數據驅動模式如上圖所示,當使用d3.data()將數據Array與 DOM 元素綁定的時,數據與元素之間有著三個階段,即

  • Enter 已有數據,但頁面還未有與之對應的 DOM

  • Update 數據元素與 DOM 元素相綁定

  • Exit 數據元素已經被刪除,但 DOM 元素還存在,即失去了綁定元素的 DOM

關於這個點,這裡不做詳細贅述,可參考文檔。這裡直接對 V4 和 V5 版本的General Update Pattern進行介紹。舉一個簡單的例子:

假設目前已有數據 ['a', 'b', 'c'....] 等字母序列,現在希望通過 D3, 使用 SVG 將其呈現在頁面上

V4#

通過selection.enter(), selection.exit()selection(update)分別指定相應邏輯,內容如下:

const d3Pattern = (dataSet) => {
  const text = g.selectAll('text').data(dataSet)  // 數據綁定
  
  text.enter()     // enter() 返回綁定數據但是還未生成dom元素的部分
      .append('text')
      .attr('class', 'new')
      .text(d => d)
      .merge(text)  // merge後面的代碼,將會分別應用於enter於update兩個部分,省略寫法
        .attr('x', (d, i) => 18 * i)
  
  text.attr('class', 'update')  // text 本身是update部分

  text.exit().remove()  // exit() 返回數據已經被刪除,但是還存在dom的元素
}

V5#

d3 V5.8.0 引入了一個新的 API, selection.join

這個 API 的優勢在於,對於一些比較簡單、不需要特殊定義 enter\exit 過程的動作的 d3 圖形,可以簡化代碼,以上的代碼,使用 V5 的版本寫,即

const d3Pattern = (dataSet) => {
  const text = g.selectAll('text').data(dataSet)  // 數據綁定
  
  text.join(
    enter => enter.append('text')
      .attr('calss', 'new')
      .text(d => d),
    update => update.attr('class', 'update')
  )
    .attr('x', (d, i) => 18 * i)
  
  // join的exit默認就是 exit().remove(),因此可以免去
}

可以發現,使用selection.join(), 並不需要再手動寫selection.exit().remove(),這是因為selection.join()這個函數默認的exit()函數已經幫你寫好了,該 API 下,d3 的 Update Pattern 可以寫為

selection.join(
    enter => // enter.. ,
    update => // update.. ,
    exit => // exit.. 
  )
  // 注意,enter,update等函數一定要return,這樣可以對selection繼續鏈式調用

當然,這個 API 的好處在於,一般的使用場景下(不需要在 enter、exit 等加特殊的動畫或操作),完全可以簡寫,比如:

svg.selectAll("circle")
  .data(data)
  .join("circle")
    .attr("fill", "none")
    .attr("stroke", "black");

以上的寫法,完全等價於

svg.selectAll("circle")
  .data(data)
  .join(
    enter => enter.append("circle"),
    update => update,
    exit => exit.remove()
  )
    .attr("fill", "none")
    .attr("stroke", "black");

等價於 V4 版本的

circles = svg.selectAll('circle')
  .data(data)

circles.enter()
  .append('circle')
  .merge('circle')
    .attr('fill', 'none')
    .attr('stroke', 'black')

circles.exit().remove()

當然,V5 完全兼容 V4 的 update Pattern,無論是 V4 還是 V5 的新版 API,這種 Update Pattern 的本質沒有變,D3 仍然是數據綁定,enter/update/exit 的工作模式。

Pattern 中的 key#

當使用d3.data()綁定數據和 dom 時,相對應的關係,可能第一個元素對應第一個 dom,第二元素對應第二 dom 等; 但當Array發生變化時,比如重新排序、插入等操作,這時候,數組中元素可能與 dom 的綁定關係就發生了一些微妙的變化。

最直觀的例子就比如動態改變字符的例子

Kapture 2020-03-08 at 21.56.19

如圖,發現新增的字符總是排在最後,實際上,如果數據一致保持和 dom 綁定的話,理論隨機生成新字符,完全應該有機會出現在中間的。

在數據綁定時,傳入一個唯一的 key 值,即可避免這種情況發生

selection.data(data, d => d.id)

完成以上步驟,只要定時的調用函數d3Pattern,傳入不同的 data,即可實現上圖的效果

完整代碼

Transition#

好了,前面鋪墊了這麼多,終於到了主角d3.transition了,但實際上,與之相關 API 屈指可數,想要讓 d3 畫出帶互動和炫酷過渡效果的,重點還是對 Update Pattern 理解透徹。

基本動畫使用#

transition 的使用,與 jquery 十分相似,使用時,只需要對選擇的元素調用,並指定修改的屬性即可,即selection.transition().attr(...)

Kapture 2020-03-08 at 22.17.46

比如現在畫布上有一個方塊,該元素為rect, 我想要使其位置從默認的地方,到 30 位置,並加上動畫,代碼為

rect.transition()
  .attr('x', 30)  //  設置新位置

效果如下

動畫的基本使用,就是如此簡單,下面簡單看下相關的 api

方法

描述

selection.transition()

this schedules a transition for the selected elements

transition.duration()

duration specifies the animation duration in milliseconds for each element

transition.ease()

ease specifies the easing function, example: linear, elastic, bounce

transition.delay()

delay specifies the delay in animation in milliseconds for each element

這裡注意 d3 的 api 都支持鏈式調用,因此比如上面的例子,希望將動畫時間設置為 1s,可以

rect.transition()
.duration(1000)
.attr('x', 30)  //  設置新位置

同理,ease 和 delay 可以分別設置動畫曲線和延遲。

Update Pattern 下的動畫#

回到最開始的例子,這裡用 V4 版本的 Update Pattern 舉例

因為 transition 是應用在selection上的,所以為了方便使用,我們可以先定義好動畫

const t = d3.transtion().duration(750)

接下來,我們希望新加入的文字從上面掉下來,且位置更新時,能有一個動畫效果,這時候需要設置在enter()時,位置有一個從上倒下的過程 (transtion)

const d3Pattern = (dataSet) => {
  const t = d3.transtion().duration(750) // 定義動畫
  const text = g.selectAll('text').data(dataSet)
  
  text.enter()  
      .append('text')
      .attr('class', 'new')
      .text(d => d)
      .attr('y', -60)
  .transition(t)
  .attr('y', 0)
  .attr('x', (d, i) => 18 * i)
  
  text.attr('class', 'update')
  .attr('y', 0)
  .transition(t)
  .attr('x', (d, i) => 18 * i)

  text.exit()
    .transition(t)
      .attr('y', 60)
      .remove()
}

Kapture 2020-03-08 at 22.40.14

可以看到,原來呆板的樣式,已經變的靈動多了。 當然,也可以繼續為退出 (exit) 的文字,加上紅色,與掉落的動畫,讓整體更具有動效,只需要對 exit 的部分做相應的處理:

text.exit()
.transition(t)
.attr('y', 60)
.remove()

Kapture 2020-03-08 at 22.46.24

如圖,這是加了向下掉落和透明度變化的動畫效果。

完整代碼

實戰應用#

比如現在已經有一個靜態的柱狀圖,希望在鼠標 hover 的時候,有一些動態效果變化,如下圖

Kapture 2020-03-08 at 23.03.53

對於柱狀圖的實現,這裡就不贅述,這裡解釋下核心代碼,思路與上面提到的完全相同:

  1. 監聽鼠標移入事件

  2. 選擇當前的 bar,通過 transition 修改屬性

  3. 監聽鼠標移出

  4. 選擇當前 bar,鼠標移出,恢復屬性

核心代碼如下:

svgElement
    .on('mouseenter', (actual, i) => {
        d3.select(this)
          .transition()
        .duration(300)
        .attr('opacity', 0.6)
        .attr('x', (a) => xScale(a.language) - 5)
        .attr('width', xScale.bandwidth() + 10)
    })
    .on('mouseleave’, (actual, i) => {
        d3.select(this)
          .transition()
          .duration(300)
          .attr('opacity', 1)
          .attr('x', (a) => xScale(a.language))
          .attr('width', xScale.bandwidth())
    })

這個柱狀圖的源碼與教程出自D3.js Tutorial: Building Interactive Bar Charts with JavaScript

插值動畫#

對於一些特殊的過渡,比如顏色的變化、數字的跳變等,如果沒有插值函數,直接使用transition().attr()是無法實現的。

因此,d3 提供了插值函數和插值動畫的接口用於這類動畫實現。當然,對於大多數場景,非差值動畫都可滿足了。

特殊的插值#

對於一些常用的屬性插值,d3 提供了非常方便入口,分別是attrTween(屬性插值)/styleTween(樣式插值)/textTween 文字插值

這類插值主要用於比如顏色、線條粗細等 “屬性” 差值,可以使用attrTween()styleTween,對於數字變化,連續跳變,可以使用textTween他們的用法類似,如下:

//顏色插值,從紅色變為藍色
transition.attrTween('fill', function() {
  return d3.interpolateRgb("red", "blue");
})

transition.styleTween('color', function() {
  return d3.interpolateRgb("red", "blue");
})

插值函數的第一個參數,是要修改的內容或屬性,功能類似transition().attr()裡,attr 的內容;第二個參數是返回的插值函數,可以使用 d3 提供的一些插值函數,當然也可以自定義插值函數。

舉個簡單的例子,比如想要實現一下效果:

Kapture 2020-03-13 at 20.30.23

只需要對元素添加鼠標事件,並通過以上的插值函數完成即可

svg.append('text')
.text('A')
.on('mouseenter', function() {
  d3.select(this)
  .transition()
  .attrTween('fill', function() {
      return d3.interpolateRgb("red", "blue");
    })
})
  .on('mouseleave', function() {...})

接下來說下自定義函數,比如仍然是紅色變為藍色,我們可以在插值函數返回自己定義的函數func(t), 該函數會在動畫時間內不斷的運行,t 為 [0, 1],借助這個思路,以上的效果可以用自定義函數實現如下:

svg.append('text')
.text('A')
.on('mouseenter', function() {
  d3.select(this)
  .transition()
  .attrTween('fill', function() {
      return function(i) {
          return `rgb(${i * 255}, 0, ${255-(i * 255)})`
        }
    })
})
.on('mouseleave', function() {
  ... // 與上類似
})

以上兩種方案,均可以實現動圖的效果哦。

可以看到,對於插值動畫,核心在於插值內容的產生。d3 提供了多款插值,相關的列表如下,比如在使用數字跳變動畫時,就可以使用d3.interpolatorRound(start,end)來產生整形的數值插值; d3.interpolateRgb(color, color2)來產生顏色插值等,具體的插值函數用法可查閱相關 API。

  • d3.interpolatNumber

  • d3.interpolatRound

  • d3.interpolatString

  • d3.interpolatRgb

  • d3.interpolatHsl

  • d3.interpolatLab

  • d3.interpolatHcl

  • d3.interpolatArray

  • d3.interpolatObject

  • d3.interpolatTransform

  • d3.interpolatZoom

通用插值#

當然,除了前面提到的 API,還有一個更通用的產值函數 API,d3.tween()

attrTween()等類似,它的第二個參數也是傳入插值函數;不同的是,第一个参数,可以传入更通用的想要改变的内容,比如同样是上面的fill属性,使用通用插值函数的写法就是:

selection.transition()
.tween('attr.fill', function() {
    return function(i) {
            return `rgb(${i * 255}, 0, ${255-(i * 255)})`
          }
})

於是我們發現,其實通用 API 與前面的特殊的三個 API 用法及其類似,唯一不同的就是通用 API 的第一個參數可以接受更廣泛的變更屬性。

這裡就不多舉例了,關於插值函數的一些參考實例可以在這裡查看

參考資料#

  1. D3.js Tutorial: Building Interactive Bar Charts with JavaScript

  2. How to work with D3.js’s general update pattern

  3. Interaction and Animation: D3 Transitions, Behaviors, and Brushing

  4. d3-selection-join

  5. sortable-bar-chart

  6. Using Basic and Tween Transitions in d3.js

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。