在数据可视化中,图表动画可以帮助我们更好地理解和展示数据的变化趋势。通过动态地展示数据的演变过程,图表动画能够吸引观众的注意力,增强数据的表现力,使信息更加清晰易懂。
在《魔力之帧(上):前端图表库动画实现原理》中,我们介绍了 VisActor 可视化解决方案中的动画实现原理。在这篇文章中,我们将从一些常见的图表动画入手,详细介绍在 VChart 中的编程实践,帮助您在自己的项目中应用这些动画效果。
图表动画在 VChart 中根据状态场景会被区分为:入场动画、更新动画和退场动画。
图表的入场动画是指在初次展示图表时,让数据图元以某种动态方式进入画面的动画效果。入场动画可以为图表增添动态元素,使数据更加生动和有趣。在 VChart 中,绝大多数图表默认会有一个总长 1 s 的入场动画,同时也为一些常见图表提供了不同的入场动画预设。
以折线为例,默认的入场动画为一个逆时针方向展开的角度生长动画,你可以通过 animationAppear.duration
来调整入场动画的时长:
"animationAppear": {
"duration": 2000 // 单位 ms
}
常见的动画配置,除了动画时长(duration
)以外,还有动画延迟(delay
)、动画缓动函数(easing
)等。
有时我们希望图表基础标注(例如轴、标题)出现后,再经过一段时间才开始播放动画,从而增强数据图元的视觉效果,吸引用户的注意力。我们可以通过配置 animationAppear.delay
来实现:
"animationAppear": {
"duration": 2000, // 单位 ms,
"delay": 1000 // 单位 ms
}
缓动效果是描述动画“变速度”的过程,设置合适的缓动效果可以让图表动画更具生动感,甚至可以传达不同的“数据情绪”。所VChart 内部默认的缓动函数是 'cubicOut'
。我们可以尝试一下,将 animationAppear.easing
设置为 'linear'
:
"animationAppear": {
"duration": 2000, // 单位 ms,
"easing": "linear"
}
观察上面的例子,你可以看到线性动画在入场动画的情况下是多么不自然。在现实世界中,一般情况下没有什么东西是以线性方式运动的,当物体运动时,它通常会伴随加速和减速。在动画中,物体在运动的开始和结束时会有更多的帧被压缩在一个较小的区域内。简而言之,物体的缓动方式可以传达不同的感觉,我们可以通过可视化缓动函数来感受一下(https://visactor.io/vchart/demo/combination/easing-visualization):
通常情况下,数据图元会同时执行入场动画,例如多分组的柱状图中,所有柱子会同时进行高度生长的动画。VChart 支持配置 oneByOne
来实现数据图元按照一定顺序依次执行动画,通常这个顺序是数据顺序,当然为了更好的实用性,在柱状图中,我们封装了一些逻辑,当配置了oneByOne
后,会默认按照 x 轴(更准确的说是离散轴)的顺序进行动画播放;
"animationAppear": {
// 开启依次动画
"oneByOne": true,
}"animationAppear": {
// 开启依次动画,且每次动画结束后延迟 500ms 执行下一次动画
"oneByOne": 500
}
那么如何实现每根柱子都依次执行的效果呢?其实很简单,我们转换一下思路:oneByOne 动画其实就是给图元按照一定规则配置上了动画的延迟(delay)。即第一根柱子入场动画没有延迟,第二根柱子延迟一段时间,使第一根柱子动画执行完毕后再执行自身动画。而这个一段时间,其实就是第一根柱子的动画时长(duration)。依次类推,第三根柱子的延迟 = 2 * duration,即:
在 vchart 中,delay 可以配置为函数:
"animationAppear":{
duration: 500, // 每个柱子的动画时长为 500ms
delay: (datum, element,ctx) => {
const { elementIndex } = ctx.VGRAMMAR_ANIMATION_PARAMETERS;
return elementIndex * (500 + 50) // 柱子延迟为 500ms(之前柱子的动画时长)+ 50ms(动画间隔时间)
},
}
这里还有一个例子,是根据图例的分组进行依次动画执行,这种场景在实际业务中要更常见一些:
"animationAppear":{
duration: 500, // 每个柱子的动画时长为 500ms
delay: (datum, element,ctx) => {
const { elementIndex } = ctx.VGRAMMAR_ANIMATION_PARAMETERS;
return elementIndex * (500 + 50) // 柱子延迟为 500ms(之前柱子的动画时长)+ 50ms(动画间隔时间)
},
}
在 VChart 里,我们为绝大多数图表提供了可选的预设入场动画。以饼图为例,默认的入场动画是一个逆时针的角度生长动画(下图左)。我们还提供了半径生长动画(中)和渐入动画(右):
"animationAppear":{
// 半径生长动画
"preset":"growRadius",
},"animationAppear":{
// 渐入动画
"preset":"fadeIn",
},
具体预设的动画类型,你可以通过 VChart 配置项文档查阅:图表入场动画预设
为了方便大家的使用,VChart 下层的图形语法层 VGrammar 内置了一些动画类型,例如下表中的所有动画类型,可以配置在任意图元上:
fadeIn
/fadeOut
: 渐入渐出动画。scaleIn
/scaleOut
: 缩放动画。moveIn
/moveOut
: 移入移出动画。rotateIn
/rotateOut
: 旋转动画。update
: 更新动画,通常用于图元更新动画(animationUpdate
)。渐入动画是一种常见的出场动画:图元的透明度从 0 逐渐变化为 1。渐出动画则刚好相反,常用于图表的退场动画。 以一个柱-线组合图为例,我们为柱图元新增一个动画类型的配置type: 'fadeIn'
,同时稍微调整了下动画延迟(delay
),从下面的效果可以看到,图表的入场动画已经变为了我们配置的渐入动画。
{
"type": "common",
...
"series": [
{
"type": "bar",
...
"animationAppear": {
// 柱图元动画配置
"bar": {
"type": "fadeIn", // 渐入动画
"oneByOne": true,
"duration": 500,
"totalTime": 3500
}
}
},
{
"type": "line",
...
"animationAppear": {
// 点图元动画配置
"point": {
"type": "fadeIn", // 渐入动画
"duration": 500,
"delay": 3500
},
// 线图元动画配置
"line": {
"type": "fadeIn", // 渐入动画
"duration": 1500,
"delay": 4000,
"easing": "cubicOut"
}
}
}
]
}
缩放动画是一种常见的出场动画:图元的scaleX
/scaleY
从 0 逐渐变化为 1。scaleOut
动画则刚好相反,常用于图表的退场动画。 VChart 提供了内置动画类型 scaleIn
/scaleOut
的额外配置参数 options
:
export interface IScaleAnimationOptions {
direction?: 'x' | 'y' | 'xy';
}
其中,direction
属性用于指定缩放的方向,可选值有:
'x'
:x 方向进行缩放。'y'
:y 方向进行缩放。'xy'
:同时在 x 和 y 方向进行缩放(默认)。{
...
"series": [
{
"type": "bar",
...
"animationAppear": {
"bar": {
"type": "scaleIn", // 柱图元执行在 y 方向的缩放动画
"options": {
"direction": "y"
},
"duration": 1000
}
}
},
{
"type": "line",
...
"animationAppear": {
"point": {
"type": "scaleIn", // 点图元执行默认的缩放动画
"delay": 1000,
"duration": 1000
},
"line": {
"duration": 1500,
"delay": 1000,
"easing": "cubicOut"
}
}
}
]
}
移入移出动画是指图表中基本图形元素在某一方向上进行平移的动画效果。VChart 提供了 IMoveAnimationOptions
接口来配置移入移出动画。 VChart 提供了内置动画类型 moveIn
/moveOut
的额外配置参数 options
:
export interface IMoveAnimationOptions {
direction?: 'x' | 'y' | 'xy';
orient?: 'positive' | 'negative';
offset?: number;
point?: { x?: number; y?: number };
}
其中:
direction
:移动方向,可选值同缩放动画。
orient
:移动的方向,可选值有:
'positive'
:正方向(默认)'negative'
:负方向point
:移动的起始点坐标。
offset
:移动的距离(像素值),默认为当前区域的(0,0)
坐标。
{
"series": [
{
"type": "bar",
...
"animationAppear": {
"bar": {
"type": "moveIn",
"options": {
"direction": "x" // 柱图元从 x 方向移入
},
"duration": 1000
}
}
},
{
"type": "line",
...
"animationAppear": {
"point": {
"type": "moveIn",
"options": {
"direction": "y" // 点图元从 y 方向移入
},
"easing": "bounceOut", // 点图元的缓动设置为
"duration": 1000
},
"line": {
"duration": 1500,
"delay": 1000,
"easing": "cubicOut"
}
}
}
]
}
这个例子里,我们将点图元的缓动函数修改为了 "bounceOut"
,这是一个很有趣的应用。bounceOut
缓动函数是一种常见的缓动类型,它在动画的结束阶段产生反弹效果,可以用来简单模拟物体下落的过程,使整个图表显得更加生动活泼。
旋转动画是针对配置了angle
属性的图元提供的一种图元动画。多用于极坐标系下的图元,例如仪表盘指针变化动画。 VChart 提供了内置动画类型 rotateIn
/rotateOut
的额外配置参数 options
:
export interface IRotateAnimationOptions {
orient?: 'clockwise' | 'anticlockwise';
angle?: number;
}
其中:
orient
:旋转方向,可选值有:
angle
:旋转的角度,单位为弧度。
{
"type": "line",
...
"animationAppear": {
"point": {
"type": "rotateIn", // 旋转动画
"duration": 1000
},
}
}
如果需要指定图元在动画执行前后具体变化效果,例如,设置图元的颜色透明度(fillOpacity)在动画过程中从 1 变化 0.3。视觉通道动画配置示例如下:
{
"animationAppear": {
"bar": {
"channel": {
"fillOpacity": { "from": 1, "to": 0.3 }
}
}
}
}
你会发现柱子在入场之后执行了一个透明度的变化并停留在了这个状态。当然我们并不会真的这么配置入场动画。不过这给我们提供了灵感:犹如拉开舞台帷幕后,瞬时关闭所有灯光,在黑暗中,通过射灯的聚焦讲观众视线凝聚在一个关键位置上。
结合接下来要给大家介绍的常规动画,就会演变出更多更丰富的“化学反应”。
VChart 提供基于 json spec 的图元动画编排配置,以尽可能丰富的满足动画需求。
一些简单的动画可以通过调整动画执行时间来达到顺序执行的效果,我们来看一个例子。
下面是一个大屏业务场景中的示例
动画效果并不复杂,我们来拆解一下:
相应的动画配置如下所示,也很好理解:
{
animationAppear: {
point: [
{
type: 'moveIn',
duration: 2000,
options: {
direction: 'y',
orient: 'negative',
point: (...args) => {
const { height } = args[3].getRegion().getLayoutRect();
return { y: height };
}
}
},
{
duration: 2000,
channel: {
fillOpacity: {
from: 0.1
},
size: {
from: 10
}
}
},
{
duration: 1000,
delay: 2000, // 通过手动设置 delay 达到前两个动画完成后执行颜色动画,此时,delay 的值应当和先执行动画的 duration 保持一致
channel: {
fill: {
from: '#b2e3ff'
}
}
}
]
}
}
在《魔力之帧:VisActor动画揭秘》中,我们详细介绍过 VisActor 中的动画设计理念,你可以再回顾一下。
简单来说,VChart 底层会提供一个统一的时间线,在这个时间线上,图元支持配置一组独立的动画单元,如下图所示。
时间线(Timeline)表示在特定一段时间内图元的动画表现,一条时间线上包含了一组串行执行的动画分片(TimeSlice)。时间线描述了一段时间内图元的动画表现。不同 timeline 之间动画可以并行。时间线也可以被设置 loop: true
以循环执行所配置的动画效果。
这个例子中通过设置循环的时间线动画,实现了一个手风琴的效果的循环播放。
简化一下问题,我们先观察单个柱子的动画效果。
在入场动画后,柱子外观保持了一小段时间,随后在短时间内透明度降低,随即恢复。这是一个典型的单时间线上有两个并行的动画,我们可以借助 timeline 的特性实现:
{
"animationNormal": {
"bar": [
{
"loop": true,
"startTime": 100,
"oneByOne": 100,
"timeSlices": [
{
"delay": 1000,
"effects": {
"channel": {
"fillOpacity": {
"to": 0.5
}
},
"easing": "linear"
},
"duration": 500
},
{
"effects": {
"channel": {
"fillOpacity": {
"to": 1
}
},
"easing": "linear"
},
"duration": 500
}
]
}
]
}
}
另一个常见的动画效果就是数据高亮的轮播动画,如下面的环形图。
动画时间线可以简单绘制如下:
暂时无法在飞书文档外展示此内容
{
"animationNormal": {
"pie": [
{
"startTime": 100,
"loop": 0,
"timeSlices": [
{
"effects": {
"channel": {
"fillOpacity": { "to": 0.3 }
}
},
"duration": 500
}
]
},
{
"loop": true,
"startTime": 800,
"oneByOne": true,
"timeSlices": [
{
"effects": {
"channel": {
"fillOpacity": { "to": 1 },
"outerRadius": {
"to": (datum) => datum.radius + 10
}
},
},
"duration": 500
},
{
"effects": {
"channel": {
"fillOpacity": { "to": 0.3 },
"outerRadius": { "to": (datum) => datum.radius }
}
},
"duration": 500
}
]
}
]
}
}
前面我们花了大量的篇幅,向你介绍了以视觉效果为出发点的动画配置。然而除了增强视觉以外,动画的另一大作用,就是增强对数据变化的追踪和感知。
在图表数据更新时,VChart 会根据一定规则,为每一条数据构造一个唯一 id,在数据更新时进行 diff 逻辑判断。在绝大多数场景下,你并不需要去关注这个逻辑。当识别到一条数据记录是新增的、减少的或是发生了数值更新,则会分别去执行图元的 enter/exit/update 动画,从而在数据更新时能有如下效果:
当柱状图的更新体现在柱子沿X轴的横向移入移出时,可以通过 updateSpec
接口,在新数据的 spec 上配置动画:
{
"animationExit": {
"bar": [
{
"type": "moveOut",
"options": {
"direction": "x"
},
"duration": 1000,
"easing": "linear"
}
]
},
"animationAppear": {
"bar": {
"type": "moveIn",
"options": {
"direction": "x",
"orient": "negative"
},
"duration": 1000,
"easing": "linear"
}
}
}
熟悉数据分析的同学一定知道,对于同一份数据,我们会经过一些数据下钻、聚合等计算,来观察不同的数据信息。经过数据运算后的新数据,可以通过不同的图表类型进行呈现。
// 柱状图 spec
{
"series": [
{
"type": "bar",
"name": "A",
"morph": {
"morphKey": "A"
}
},
{
"type": "bar",
"name": "B",
"morph": {
"morphKey": "B"
}
}
]
}
// 散点图 spec
{
"series": [
{
"type": "scatter",
"morph": {
"morphKey": "A",
"morphElementKey": "type"
}
},
{
"type": "scatter",
"morph": {
"morphKey": "B",
"morphElementKey": "type"
}
}
]
}
morph.morphKey
: 指定不同图表的系列之间的关联关系;拥有相同 morphKey
的系列将会建立一个关联,作为更新前、后的系列,对数据图元进行 diff 分析;morph.morphElementKey
:指定拥有关联关系的系列数据图元的匹配字段。本文详细介绍了动画在 VChart 中的编程实践,希望能够帮助您在自己的项目中应用这些动画效果。
欢迎大家与我们进行交流:
1)VisActor 微信订阅号留言(可以通过订阅号菜单加入微信群):
2)VisActor 官网:https://www.visactor.io/
今夜无月,期待你点亮星空,感谢Star:
github:https://github.com/VisActor/VChart
相关参考: