D3.js 提供了多種工具支持數據可視化的互動,其中d3.transition
讓簡單而高效的為圖像添加動畫成為了可能。
單單從 API 來講,d3.transition
非常簡單,用法類似 Jquery。 但是想要設計出理想的動畫效果,就不得不提到 D3 繪製圖形的一個核心概念General Update Pattern
。D3 的數據驅動特性的核心和實現就是依靠這個 Pattern,而動畫和互動自然要從它說起了。
並不是所有圖形都必須遵循 Update Pattern,比如一次性繪圖,無互動的靜態圖形等。但如果涉及到了動態數據,這個 Update Pattern 不僅利於寫出易於維護的代碼,也能更好的發揮 D3 強大的功能。
General Update Pattern#
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 的綁定關係就發生了一些微妙的變化。
最直觀的例子就比如動態改變字符的例子
如圖,發現新增的字符總是排在最後,實際上,如果數據一致保持和 dom 綁定的話,理論隨機生成新字符,完全應該有機會出現在中間的。
在數據綁定時,傳入一個唯一的 key 值,即可避免這種情況發生
selection.data(data, d => d.id)
完成以上步驟,只要定時的調用函數d3Pattern
,傳入不同的 data,即可實現上圖的效果
Transition#
好了,前面鋪墊了這麼多,終於到了主角d3.transition
了,但實際上,與之相關 API 屈指可數,想要讓 d3 畫出帶互動和炫酷過渡效果的,重點還是對 Update Pattern 理解透徹。
基本動畫使用#
transition
的使用,與 jquery 十分相似,使用時,只需要對選擇的元素調用,並指定修改的屬性即可,即selection.transition().attr(...)
比如現在畫布上有一個方塊,該元素為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()
}
可以看到,原來呆板的樣式,已經變的靈動多了。 當然,也可以繼續為退出 (exit) 的文字,加上紅色,與掉落的動畫,讓整體更具有動效,只需要對 exit 的部分做相應的處理:
text.exit()
.transition(t)
.attr('y', 60)
.remove()
如圖,這是加了向下掉落和透明度變化的動畫效果。
實戰應用#
比如現在已經有一個靜態的柱狀圖,希望在鼠標 hover 的時候,有一些動態效果變化,如下圖
對於柱狀圖的實現,這裡就不贅述,這裡解釋下核心代碼,思路與上面提到的完全相同:
-
監聽鼠標移入事件
-
選擇當前的 bar,通過 transition 修改屬性
-
監聽鼠標移出
-
選擇當前 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 提供的一些插值函數,當然也可以自定義插值函數。
舉個簡單的例子,比如想要實現一下效果:
只需要對元素添加鼠標事件,並通過以上的插值函數完成即可
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 的第一個參數可以接受更廣泛的變更屬性。
這裡就不多舉例了,關於插值函數的一些參考實例可以在這裡查看