D3 过渡
参考
本文主要介绍 Transitions、Interpolates 和 Easing 模块
D3 提供了 d3-transition 模块用于实现图形元素的过渡动画。它和 selection 模块类似,有相似的方法,例如为选中的 DOM 元素设置样式属性。但是在设置这些属性时,一般都不是直接设置为目标值,而是在过渡时间中多次使用插值器(d3-interpolate 模块提供了多种内置的插值器)计算出起始值和目标值之间的过渡值,从而实现图形元素的某个可视化变量从起始值顺滑变换到目标值的效果。
创建过渡对象
使用方法 selection.transition([name])
创建一个过渡对象(以下称为过渡管理器 transition
),其中 selection
是选择集合,然后可以通过过渡管理器为选择集合中的元素设置过渡动画。
// 选择集合中含有元素 <body>
d3.select("body")
.transition() // 创建一个过渡管理器
.style("background-color", "red"); // 使用过渡管理器设置元素的属性
// 这里将 <body> 的背景色目标值设置为红色,则网页就会在默认的 250ms 内,从当前颜色过渡为红色
该方法有一个可选参数 name
用以给过渡管理器设置名称,如果没有入参则将名称设置为 null
,如果之前选择集合的元素已经设置了同名的过渡管理器,则旧的过渡管理器会被新的覆盖(原来的过渡也会中断)。
该方法的入参 name
的数据类型除了是字符串,也可以是一个过渡管理器对象,会基于传入的(已有)过渡管理器,创建一个同名同 id
的过渡管理器,这样可以方便地复用过渡动画的设置。
// 在根元素上创建一个过渡管理器
const t = d3.transition()
.duration(750) // 过渡时间是 750 ms
.ease(d3.easeLinear); // 设置过渡动画的缓动函数
// 为元素 .apple 设置过渡,复用原有的过渡管理器 t 的相关设置
d3.selectAll(".apple").transition(t)
.style("fill", "red");
// 为元素 .orange 设置过渡,复用原有的过渡管理器 t 的相关设置
d3.selectAll(".orange").transition(t)
.style("fill", "orange");
除了上述基于选择集合创建过渡管理器 selection.transition()
,还可以使用方法 d3.transition([name])
为根元素 document.documentElement
即 <html>
创建过度管理器
此外还可以使用方法 transition.transition()
基于原有的过渡管理器所绑定的选择集合,创建一个新的过渡管理器,而且继承了原有过渡的名称、时间、缓动函数等配置,而且新的过渡会在当前过渡结束后开始执行。一般通过该方法为同一个选择集合设置一系列依次执行的过渡动效
d3.selectAll(".apple")
.transition() // First fade to green.
.style("fill", "green") // 返回 transition 便于链式调用
.transition() // Then red.
.style("fill", "red")
.transition() // Wait one second. Then brown, and remove.
.delay(1000)
.style("fill", "brown")
.remove();
提示
可以使用方法 transition.selection()
获取该过渡管理器所绑定的选择集合。
注意
虽然 transition 模块有支持大部分 selection 模块所提供的方法,例如 selection.attr
和 selection.style
分别是用于设置元素的属性和样式(其中 selection
是选择集合),相应的过渡管理器提供了 transition.attr
和 transition.style
方法;但是也有不同,例如新增元素或绑定数据都只能在创建过渡(管理器)之前完成。
过渡管理器是基于选择集合创建的,但还可以进一步对选择集合进行「筛选」,即可以针对原有选择集的子集设置过渡:
transition.select(selector)
基于 CSS 选择器selector
对过渡管理器所绑定的选择集合进行筛选,选出第一个匹配的后代元素,并返回基于该元素创建的过渡管理器,该过渡管理器和原有的过渡管理器有相同的配置参数(相同的名称、id
、过渡时间等),便于后续的链式调用,针对这个元素设置另外的过渡效果
参数selector
可以是一个表示 CSS 选择器的字符串,也可以是一个函数。
如果参数selector
是一个函数,则该函数返回一个 DOM 元素(或null
)。过渡管理器所绑定的选择集合中的元素会依次调用该函数,并传递三个参数(后续的方法中,允许函数作为入参的,函数一般也会接收这三个参数):- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的this
指向当前遍历的元素(即nodes[i]
)提示
可以将该方法看作是以下代码的「封装」,一种简便的实现方式
jstransition .selection() // 获取 transition 过渡对象所绑定的选择集 // 从选择集中进行二次选择,选择满足条件的元素,构成一个新的选择集 .selectChild(selector) // 为新的选择集(所包含的元素)设置过渡效果 .transition(transition)
- 当前元素绑定的数据 datum
transition.selectAll(selector)
基于 CSS 选择器selector
对过渡管理器所绑定的选择集合进行筛选,选出所有匹配的后代元素,并返回基于这些筛选得到的元素集合创建的过渡管理器。其入参也可以是一个函数。transition.selectChild([selector])
基于 CSS 选择器selector
对过渡管理器所绑定的选择集合进行筛选,选出第一个匹配的直接子代元素,并返回基于筛选得到的元素创建的过渡管理器。其入参也可以是一个函数。transition.selectChildren([selector])
基于 CSS 选择器selector
对过渡管理器所绑定的选择集合进行筛选,选出所有匹配的直接子代元素,并返回基于这些筛选得到的元素创建的过渡管理器。其入参也可以是一个函数。transition.filter(filter)
一个选择集合筛选器,其入参可以是表示 CSS 选择器的字符串;也可以是函数,该函数会被选择集合中的元素会依次调用,也是传递三个参数,但是函数的返回值是一个布尔值,以表示当前遍历的元素是否符合筛选条件。最后该方法返回的是基于筛选得到的这些元素集合创建的一个过渡管理器。
相反操作也可以实现,使用方法 transition.merge(otherTransition)
可以将两个过渡管理器进行「合并」(实际上是将两个过渡管理器所绑定的选择集合进行合并),并返回合并后的过渡管理器。该方法接收需要合并的过渡管理器,需要具有相同的 id
,这样返回的过渡管理器才具有相同的名称和 id
,以及相同的父容器。
该方法等价于以下代码,先获取过渡管理器所绑定的选择集合,再通过选择集合的 .merge()
方法和其他选择集合「合并」,然后再基于原有的过渡管理器配置参数,为合并的选择集合创建一个过渡管理器:
transition
.selection()
.merge(otherTransition.selection())
.transition(transition)
过渡的生命周期
过渡动画一般持续一段时间,它的不同的阶段对应于过渡管理器的不同生命周期,而且 D3 会在过渡的特定阶段分发不同的事件(自定义事件),便于执行相应的处理。
- 当创建了过渡管理器后(使用方法
selection.transition()
或transition.transition()
),在当前一帧的末尾或者下一帧开始,新的过渡就会添加到队列中,等待执行。此时 delay 和start
事件监听器无法改变
提示
使用过渡管理器的相关的方法对过渡进行设置时,或对过渡管理器所绑定的选择集合进行样式属性设置时,有的方法会立即同步执行,有的需要等到过渡开始时才执行,例如使用方法 transition.attrTween()
或 transition.styleTween()
结合开始值和目标值进行插值。
- 当过渡开始时,会分发
interrupt
事件和cancel
事件,相关的事件监听器会被触发,用以中断或取消在相同选择集合上同名的旧的过渡(中断正在执行的 active 的同名过渡,和取消事前添加的还在等待中 pending 的同名过渡)。然后分发start
事件,相关的事件监听器会被触发,这个阶段也是过渡可以进行配置的「最后机会」了,例如设置过渡的时间,插值函数等。
注意
由于中断旧的过渡是在新的过渡发生时才执行的,而非在创建过渡管理器的时候,所以即使新的过渡是一个零延迟过渡,旧的过渡也不一定会立即停止,因为创建过渡管理器后,新的过渡只是添加到队列中,等待执行。
- 当过渡开始那一帧,插值器就会被调用,此时的标准时间是
t=0
。然后过渡执行中的每一帧都会不断调用插值器,以计算出该时间点的相应属性值,实现补间动画,该过程标准时间是在t=0
和t=1
之间。 - 在过渡结束时,此时的标准时间是
t=1
,会分发end
事件,相关的事件监听器会被触发,这个阶段也是过渡可被检测到的「最后机会」了,之后过渡就会被删除。
可以使用方法 transition.on(typeNames[, listener])
监听过渡所分发的(自定义)事件,并执行相应的回调操作 listener
:
- 第一个参数
typeNames
表示需要监听的过渡事件,可以是以下四种类型之一:start
过渡开始时分发的事件end
过渡结束时分发的事件interrupt
过渡被中断时分发的事件cancel
过渡被取消时分发的事件
提示
为了监听特定的过渡,可以将需要监听的过渡名称
name
作为以上的四种事件的后缀,以.
连接,例如start.foo
和start.bar
。提示
如果希望同一个回调操作响应多种事件,可以在设置监听器的
typeNames
时将多种事件用空格分开,例如interrupt end
以及start.foo start.bar
- 第二个(可选)参数
listener
是回调函数,当相关的事件被触发后,该回调函数就会被执行,而且会传入与过渡所绑定的选择集合相关的三个参数:- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的this
指向当前遍历的元素(即nodes[i]
)提示
因为过渡过程中数据可能会动态变化,函数接收的数据总是元素的「最新」的数据,而不是绑定的原始数据。
可以通过方法d3.active(node[, name])
获取指定元素的指定名称的执行中的过渡管理器 - 当前元素绑定的数据 datum
提示
如果希望更新事件监听器,需要先移除原有的监听器,使用相同的方法 transition.on(typeNames, null)
但是传递 null
作为回调函数。例如移除所有过渡名称为 foo
的过渡事件监听器 transition.on('.foo', null)
,移除所有过渡事件监听器 transition.on('.', null)
提示
如果只是希望在过渡结束时做出响应,执行特定的操作,可以使用 transition.end()
方法,它返回一个 Promise,仅在过渡管理器所绑定的选择集合的所有过渡完成时才 resolve
;如果过渡被中断或取消,就会被 reject
提示
可以通过相关的方法来「手动」中断过渡
d3.interrupt(node[, name])
中断指定元素上指定名称的(执行中或等待中的)过渡。如果参数name
没有指定,则使用null
selection.interrupt([name])
中断选择集合中设置的(执行中或等待中的)过渡,并返回该选择集合(便于后续链式调用)。注意
在选择集合中(所含的元素上)执行中断过渡操作,并不会对这些元素的后代元素有影响,因此对于一个包含多个独立元素的过渡动画,例如对于一个坐标轴过渡动画,它需要由多个元素(如刻度线、坐标标签、轴线等)同步执行过渡而生成的,需要分别对这些元素执行中断操作。
js// 通配符 * 选中了选择集合中所有的后代元素 selection.selectAll("*").interrupt(); // 如果想同时中断这些元素的父元素,一般是 <g> 元素 selection.interrupt().selectAll("*").interrupt();
过渡参数配置
过渡管理器有多种方法,对其绑定的选择集合中的元素进行属性样式设置,一般结合插值器(它会在过渡期间不断调用,以计算出不同的过渡值,实现补间动画):
transition.attr(attrName, value)
将选择集合中每个元素的属性attrName
的目标值设置为value
,D3 会根据起始值(即过渡开始时该元素属性attrName
的属性值)和目标值,自动调用相应插值器:- 当
value
是数值时,使用d3.interpolateNumber()
插值器 - 当
value
是颜色(或可以转换为颜色的字符串)时,使用d3.interpolateRgb()
插值器 - 当
value
是其他数据类型时,使用d3.interpolateString()
插值器
提示
第二个参数也可以是一个返回目标值的函数,它会立即被选择集合中的每个元素依次调用,并传入三个参数,最后返回每个元素的属性
attrName
的目标值- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的
this
指向当前遍历的元素(即nodes[i]
)提示
如果目标值是
null
则该元素的属性attrName
会在过渡开始时被移除- 当
transition.attrTween(attrName[, factory])
也是用于设置元素的属性attrName
,但该方法可以自定义插值器
第二个参数不是过渡最终的目标值,而是一个函数,它返回一个插值器,因此称为插值器工厂函数 interpolator factoryinterpolator factory
工厂函数接收三个参数:
- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
插值器工厂函数 interpolator factory 用于生成/返回一个插值器
插值器
插值器工厂函数最后需要返回一个函数(即插值器),它接受标准时间
t
作为参数(其值的范围是[0, 1]
)返回的这个函数会在过渡期间被不断调用,用于生成不同时间点的属性
attrName
的值D3 在 d3-interpolate 模块中提供了多种插值器,一般在工厂函数里对它们进行按需「修改」,最后返回一个定制化的插值器
js// 填充的颜色从当前的颜色过渡到蓝色 transition.attrTween("fill", function() { // 返回一个 D3 内置的颜色插值器 return d3.interpolateRgb(this.getAttribute("fill"), "blue"); });
jstransition.attrTween("fill", function() { // 返回一个自定义的颜色插值器 return function(t) { return "hsl(" + t * 360 + ",100%,50%)"; }; });
提示
如果希望在过渡开始时,移除属性
attrName
请使用transition.attr(attrName, null)
;而如果希望在过渡结束时,移除属性attrName
可以使用transition.on()
监听过渡结束end
事件,并在回调函数中执行相应的操作。- 当前元素绑定的数据 datum
transition.style(styleName, value[, priority])
用于设置元素的样式styleName
,其目标值是value
,还可以设置样式的优先等级(默认是空字符串,可以设置为important
以使新设置的样式具有最高优先级)。D3 会根据起始值和目标值,自动调用相应插值器。value
也可以是一个返回目标值的函数(也是在调用时传入三个参数)。提示
如果目标值是
null
则该元素的样式styleName
会在过渡开始时被移除transition.styleTween(styleName[, factory[, priority]])
也是用于设置元素的样式styleName
,但该方法可以自定义插值器(D3 在 d3-interpolate 模块中提供了多种插值器),因为第二个参数不是目标值,而是一个插值器,或返回插值器的函数。jstransition.styleTween("fill", function() { return d3.interpolateRgb(this.style.fill, "blue"); });
transition.text(value)
用于在过渡开始时将value
设置为元素的 textContent 文本内容。参数value
也可以是一个返回文本内容的函数(也是在调用时传入三个参数)。提示
如果希望在清除元素的文本内容,可以传递
null
作为参数提示
由于一般文字不适合使用插值器计算过渡值,如果希望实现动态过渡效果,可以通过元素替换和透明的设置实现淡入淡出的效果。
transition.textTween(factory)
用于让具有数字的文本内容实现过渡效果,接收一个插值器作为参数,或传递一个返回插值器的函数(也是在调用时传入三个参数)js// 文本内容从 0 变动到 100 transition.textTween(function() { return d3.interpolateRound(0, 100); });
提示
如果希望在清除元素的文本内容,可以传递
null
作为参数transition.tween(name[, value])
一个更通用的用于设置补间动画的方法,第一个参数是需要设置的元素属性,第二参数是一个返回插值器的函数(也是在选择集合的每个元素调用它时,传入三个参数),可以在 d3-interpolate 模块中选择内置的插值器,也可以自定义插值器jstransition.tween("attr.fill", function() { const i = d3.interpolateRgb(this.getAttribute("fill"), "blue"); // 返回一个插值器 return function(t) { this.setAttribute("fill", i(t)); }; });
提示
如果传递
null
作为第二个参数,则该元素的属性name
会被移除
过渡除了需要设置过渡状态(起始状态、目标状态、中间状态),还需要设置过渡的时间:
transition.delay([value])
设置过渡的延迟启动时间,单位是毫秒ms
,默认是0
即零延迟。入参也可以是一个返回时间的函数,过渡管理器绑定的选择集合中的元素会依次调用该函数,并传递三个参数:- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的this
指向当前遍历的元素(即nodes[i]
)js// 对选择集合中的每个元素设置不同的过渡延迟时间,可以实现交错过渡的效果 transition.delay(function(d, i) { return i * 10; });
提示
如果该方法没有入参,则返回选择集合中第一个元素的延迟时间,一般用于选择集合中就只有一个元素的情况,希望获取它的过渡延迟时间的配置。
- 当前元素绑定的数据 datum
transition.duration([value])
设置过渡的持续时间,单位是毫秒ms
,默认值是250
毫秒。入参也可以是一个返回时间的函数。transition.ease([value])
设置过渡的缓动函数,D3 在 d3-ease 模块中提供了多种不同类型的缓动函数js// 过渡默认使用的缓动函数是 d3.easeCubic transition.ease(d3.easeCubic)
transition.easeVarying(factory)
用于为每个元素设置不同的缓动函数,入参是一个工厂函数factory
(因为它最终返回的是一个缓动函数),该工厂函数会接收三个参数:- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的this
指向当前遍历的元素(即nodes[i]
)- 当前元素绑定的数据 datum
过渡管理器还提供了一些类似 d3-selection 模块的方法,它们作用于选择集合的元素上:
transition.each(function)
对过渡管理器绑定的选择集合中的每个元素都调用传入的函数function
,调用时也是传入三个参数:- 当前元素绑定的数据 datum
d
- 当前元素在选择集合中的索引 index
i
- 选择集合
nodes
而函数内的this
指向当前遍历的元素(即nodes[i]
)- 当前元素绑定的数据 datum
transition.call(function[, arguments…])
执行一次函数function
,而且将过渡管理器作为第一个入参传递给function
,而其他传入的参数arguments...
同样传给function
,最后返回当前过渡管理器,这样是为了便于后续进行链式调用。提示
它其实和 d3-selection 模块的方法
selection.call()
作用类似transition.remove()
当过渡结束时(且选择集合没有其他过渡需要执行时),将选择集合中的元素从页面移除
还有一些获取过渡管理器所绑定的选择集合信息的方法:
transition.empty()
返回一个布尔值,以判断过渡管理器所绑定的选择集合中是否有元素,如果没有元素就返回true
提示
等价于 d3-selection 模块的方法
selection.empty()
transition.nodes()
返回一个数组,包含过渡管理器所绑定的选择集合中的所有元素提示
等价于 d3-selection 模块的方法
selection.nodes()
transition.node()
返回过渡管理器所绑定的选择集合中的第一个元素提示
等价于 d3-selection 模块的方法
selection.node()
transition.size()
返回过渡管理器所绑定的选择集合中的元素数量提示
等价于 d3-selection 模块的方法
selection.size()
缓动函数
D3 将具体的过渡时间(即以毫秒为单位的时间)进行线性映射,约束在 [0, 1]
范围内,称为标准时间,这样的统一规范便于其他模块对接调用。
然后在过渡开始时,具体时间会映射为标准时间 t
,即 t
会依次遍历 [0, 1]
范围的值,并作为入参传递给插值器,插值器就会计算出相应的过渡值,用以实现补间动画。如果直接使用标准时间传递给插值器,最后的效果是生成的补间动画 进程是「线性」 的。
为了实现更真实的补间动画(模拟物理世界的运动效果),可以借助缓动函数对标准时间进行变换(相当于改变具体的过渡时间与标准时间的映射关系),实现更丰富的过渡进程,例如缓进缓出等。
缓动函数的入参是标准时间 t
,然后进行计算得到变换时间 t'
,它的范围也是在 [0, 1]
之间,然后使用该变换时间 t'
作为插值器的入参,例如过渡默认会使用缓动函数 d3.easeCubic
(和 d3.easeCubicInOut
一样)其效果是过渡开始和结尾较慢而中间较快,函数图形如下:
可以将横坐标看作是标准时间 t
,即定义域的范围是 [0, 1]
,标准时间 t
通过缓动函数图形的映射,可以得到对应的纵坐标,变换时间 t'
的值。
例如根据 d3.easeCubic
缓动函数的图形可知,在过渡的初期(即 t
接近 0
的时间),变换得到的时间 t'
变化很小,所以过渡的变化不明显;但在过渡中期,变换接近线性,所以过渡较快;在过渡的后期(即 t
接近 1
的时间),变换得到时间 t'
变化也很小,所以该阶段过渡的变化又不明显了
提示
缓动函数一般会在端点与标准时间对齐,即当 t=0
时缓动函数返回的变换时间 t'
也是 0
表示过渡的开始;当 t=1
时缓动函数返回的变换时间 t'
也是 1
表示过渡的结束。
提示
虽然缓动函数返回的值一般在 [0, 1]
内,但是有一些缓动函数会稍微超出该范围,例如弹性类型 elastic 的缓动函数,以模拟弹簧的弹动效果
D3 在 d3-ease 模块中提供了多种不同类型的缓动函数,可以查看它们的函数图像和过渡效果:
Linear
d3.easeLinear
Polynomial
d3.easePolyIn
d3.easePolyOut
d3.easePoly
或d3.easePolyInOut
以上多项式 poly
缓动函数可以设置指数 poly.exponent(e)
const linear = d3.easePoly.exponent(1);
const quad = d3.easePoly.exponent(2);
const cubic = d3.easePoly.exponent(3);
Quadratic
d3.easeQuadIn
d3.easeQuadOut
d3.easeQuad
或d3.easeQuadInOut
Cubic
d3.easeCubicIn
d3.easeCubicOut
d3.easeCubic
或d3.easeCubicInOut
过渡的默认缓动函数
Sinusoidal
d3.easeSinIn
d3.easeSinOut
d3.easeSin
或d3.easeSinInOut
Exponential
d3.easeExpIn
d3.easeExpOut
d3.easeExp
或d3.easeExpInOut
Circle
d3.easeCircleIn
d3.easeCircleOut
d3.easeCircle
或d3.easeCircleInOut
Elastic
d3.easeElasticIn
d3.easeElastic
或d3.easeElasticOut
d3.easeElasticInOut
以上弹性 elastic
缓动函数可以设置参数:
elastic.amplitude(a)
振幅elastic.period(p)
周期
Bounce
d3.easeBounceIn
d3.easeBounce
或d3.easeBounceOut
d3.easeBounceInOut
缓动函数都接收标准时间 t
作为参数,形式如 ease(t)
// Before the animation starts, create your easing function.
const customElastic = d3.easeElastic.period(0.4);
// During the animation, apply the easing function.
const te = customElastic(t);
插值器
插值器是用于计算过渡值的,以便实现补间动画。具体介绍可以查看 d3-interpolate 模块,或另一篇笔记 D3 插值器