10.4 数据更新动画
分数:8
-
更新动画:
-
代码入口:
packages/vchart/src/animation/ -
解读重点:
-
更新动画的实现
-
其他参考文档:
https://www.visactor.io/vchart/guide/tutorial_docs/Animation/Animation_Types
https://www.visactor.io/vrender/guide/asd/Basic_Tutorial/Animate
https://visactor.io/vgrammar/guide/guides/animation
魔力之帧(上):前端图表库动画实现原理一幅生动的可视化作品往往少不了动画的参与。无论是各色各样的图表还是叙事作品,组织周 - 掘金
在了解完对特定的图元数据变化时添加变更动画效果之后,我们可以对某个类型的图表中配置系列图元的数据更新动画,满足特定场景时的动画效果。
数据更新动画的实现解读
数据更新动画是指当图表的数据发生变化时,图表元素根据新的数据状态执行的动画效果。在VChart中,这种动画设计得非常灵活,可以应用于新数据加入(enter)、现有数据更新(update)和旧数据移除(exit)三种场景。以下是详细的实现解读。
1. 动画配置结构
IAnimationSpec 接口
IAnimationSpec接口定义了动画配置的基本结构,其中包含了针对不同状态的动画设置。对于数据更新动画来说,它主要涉及以下三个属性:
-
animationEnter:用于描述新数据加入时的动画效果。 -
animationUpdate:用于描述现有数据更新时的动画效果。 -
animationExit:用于描述旧数据移除时的动画效果。
interface IAnimationSpec<MarkName extends string, Preset extends string> { animationEnter?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>; animationUpdate?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>; animationExit?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>; }
每个属性都可以接受布尔值(启用/禁用)、预设配置对象或自定义配置对象作为参数,从而为开发者提供了高度定制化的可能性。
2. 动画管理器
AnimateManager 类
AnimateManager类负责管理和协调所有动画的状态。它实现了IAnimate接口,并提供了方法来更新和检索动画状态。对于数据更新动画而言,AnimateManager会确保这些动画在数据变化时自动触发,并且可以根据需要暂停或恢复。
class AnimateManager extends StateManager implements IAnimate {
updateAnimateState(state: AnimationStateEnum, noRender?: boolean) {
if (state === AnimationStateEnum.update) {
this.updateState(
{
animationState: {
callback: (datum: any, element: IElement) => element.diffState
}
},
noRender
);
} else if (state === AnimationStateEnum.appear) {
// 出现状态下的动画逻辑
} else if (state === AnimationStateEnum.exit) {
// 退出状态下的动画逻辑
}
}
}
当图表元素进入update、appear或exit状态时,updateAnimateState方法会被调用,并将状态传递给内部的状态管理逻辑。这使得所有符合条件的元素都能够执行对应的动画。
3. 动画配置生成
animationConfig 函数
为了简化用户配置和默认配置之间的合并过程,VChart提供了一个名为animationConfig的辅助函数。该函数遍历所有可能的动画状态,并根据用户提供的配置或默认配置构建出最终的动画配置对象。
function animationConfig<Preset extends string>(
defaultConfig: MarkAnimationSpec = {},
userConfig?: Partial<Record<IAnimationState, boolean | IStateAnimateSpec<Preset> | IAnimationConfig | IAnimationConfig[]>>,
params?: { dataIndex: (datum: any, params: any) => number; dataCount: () => number; }
): MarkAnimationSpec {
const config = {} as MarkAnimationSpec;
for (let i = 0; i < AnimationStates.length; i++) {
const state = AnimationStates[i];
const userStateConfig = userConfig ? userConfig[state] : undefined;
if (userStateConfig === false) continue;
if (state === 'enter' || state === 'update' || state === 'exit') {
let defaultStateConfig: IAnimationConfig[];
if (isArray(defaultConfig[state])) {
defaultStateConfig = defaultConfig[state] as IAnimationConfig[];
} else {
defaultStateConfig = [{ ...DEFAULT_ANIMATION_CONFIG[state], ...defaultConfig[state] } as any];
}
config[state] = defaultStateConfig;
}
}
return config;
}
此函数处理了enter、update和exit状态下的动画配置合并,确保用户提供的配置能够正确应用到具体的图元上。如果用户没有提供自定义的动画配置,则使用默认配置。
4. 数据更新动画的具体实现
以柱状图为例,假设我们希望为新加入的数据点添加淡入效果,为更新的数据点添加缩放效果,为移除的数据点添加淡出效果。以下是详细的实现步骤:
- 定义动画配置:首先,在图表配置中为柱状图系列指定
animationEnter、animationUpdate和animationExit配置。这里我们可以选择内置的动画类型,并调整其持续时间和缓动函数。
const chartSpec = {
series: [
{
type: 'bar',
data: [/* 初始数据数组 */],
animationEnter: {
type: 'fadeIn', // 新数据点淡入
duration: 800,
easing: 'easeInOutQuad'
},
animationUpdate: {
type: 'scaleIn', // 更新数据点缩放
duration: 500,
easing: 'easeInOutQuad'
},
animationExit: {
type: 'fadeOut', // 移除数据点淡出
duration: 600,
easing: 'easeInOutQuad'
}
}
]
};
- 注册动画:接下来,我们需要确保所需的动画已经被正确注册到系统中。这一步骤通常在项目启动时完成,或者在需要的地方显式调用。
import { Factory } from '@visactor/vchart'; import { Appear_FadeIn, ScaleInOutAnimation, Appear_FadeOut } from './series/bar/animation'; // 注册淡入动画 Factory.registerAnimation('fadeIn', Appear_FadeIn); // 注册缩放动画 Factory.registerAnimation('scaleIn', ScaleInOutAnimation); // 注册淡出动画 Factory.registerAnimation('fadeOut', Appear_FadeOut);
这里的Appear_FadeIn、ScaleInOutAnimation和Appear_FadeOut函数分别定义了淡入、缩放和淡出动画的具体逻辑,例如如何改变图形元素的透明度或尺寸。
- 初始化图表实例:有了上述配置之后,我们可以初始化一个
VChart实例,并将配置传递给它。这会触发图表的渲染过程,并应用相应的动画效果。
import { VChart } from '@visactor/vchart';
const container = document.getElementById('chart-container');
const chart = new VChart({
el: container,
spec: chartSpec,
options: {
animation: true, // 开启动画
theme: 'light' // 使用浅色主题
}
});
- 触发动画:一旦图表被渲染出来,任何数据的变化都会自动触发动画。例如,当有新的数据加入时,
animationEnter配置就会生效;当数据更新时,animationUpdate配置生效;而当数据被移除时,则是animationExit配置起作用。
// 假设一段时间后需要更新数据
setTimeout(() => {
const newData = [/* 新的数据数组 */];
chart.updateSeriesData(newData);
}, 5000);
5. 动画任务的执行
IAnimationTask 接口
对于复杂的动画序列,VChart引入了IAnimationTask接口来描述动画任务的数据结构。每个任务包含时间偏移、动作队列和后继任务列表,形成了一种链式动画执行机制。
interface IAnimationTask {
timeOffset: number;
actionList: Action[];
nextTaskList: IAnimationTask[];
}
这种设计使得多个动画任务可以按顺序或并发执行,从而实现更加复杂和细腻的动画效果。对于数据更新动画而言,它可以作为一个独立的任务链的一部分,与其他动画任务一起协同工作。
6. 示例:创建带有数据更新动画的柱状图
下面以创建一个带有数据更新动画的柱状图为例,说明如何使用VChart的数据更新动画系统来实现基础流程。
步骤 1: 定义动画配置
首先,我们需要定义柱状图的基本配置,包括数据源和其他视觉属性。同时,在这里我们也会指定animationEnter、animationUpdate和animationExit配置,以确保在数据变化时能够触发相应的动画效果。
const chartSpec = {
series: [
{
type: 'bar',
data: [
{ value: 10 },
{ value: 20 },
{ value: 30 }
],
animationEnter: {
type: 'fadeIn',
duration: 800,
easing: 'easeInOutQuad'
},
animationUpdate: {
type: 'scaleIn',
duration: 500,
easing: 'easeInOutQuad'
},
animationExit: {
type: 'fadeOut',
duration: 600,
easing: 'easeInOutQuad'
}
}
]
};
步骤 2: 注册动画
确保所需的动画已经被正确注册到系统中。这一步骤通常在项目启动时完成,或者在需要的地方显式调用。
import { Factory } from '@visactor/vchart'; import { Appear_FadeIn, ScaleInOutAnimation, Appear_FadeOut } from './series/bar/animation'; Factory.registerAnimation('fadeIn', Appear_FadeIn); Factory.registerAnimation('scaleIn', ScaleInOutAnimation); Factory.registerAnimation('fadeOut', Appear_FadeOut);
步骤 3: 初始化图表实例
有了上述配置之后,我们可以初始化一个VChart实例,并将配置传递给它。这一步骤会触发图表的渲染过程,并应用相应的动画效果。
import { VChart } from '@visactor/vchart';
const container = document.getElementById('chart-container');
const chart = new VChart({
el: container,
spec: chartSpec,
options: {
animation: true, // 开启动画
theme: 'light' // 使用浅色主题
}
});
步骤 4: 触发数据更新动画
一旦图表被渲染出来,任何数据的变化都会自动触发动画。例如,当有新的数据加入时,animationEnter配置会生效;当数据更新时,animationUpdate配置生效;而当数据被移除时,则是animationExit配置起作用。
// 模拟数据更新
setTimeout(() => {
const updatedData = [
{ value: 15 }, // 更新第一个数据点
{ value: 25 }, // 更新第二个数据点
{ value: 35 }, // 更新第三个数据点
{ value: 45 } // 添加一个新的数据点
];
// 更新图表数据并触发动画
chart.updateSeriesData(updatedData);
}, 5000);
在这个例子中,updateSeriesData方法会触发一系列动画:
-
对于新加入的数据点(第四个数据点),
animationEnter配置会使其以淡入的方式逐渐显现。 -
对于已存在的数据点(前三个数据点),
animationUpdate配置会根据新的数据值调整它们的大小,并以缩放的方式过渡。 -
如果有数据点被移除,则
animationExit配置会使其以淡出的方式消失。
步骤 5: 动态控 制动画
在某些情况下,你可能想要动态地控制数据更新动画的行为,比如更改动画的速度或样式。VChart提供了灵活的方法来实现这一点。
// 更新某个系列的数据更新动画配置
chart.updateSeriesOptions(0, {
animationEnter: {
duration: 1000, // 更改淡入动画的持续时间
easing: 'linear' // 更改缓动函数
},
animationUpdate: {
duration: 700, // 更改缩放动画的持续时间
easing: 'easeInOutCubic' // 更改缓动函数
},
animationExit: {
duration: 900, // 更改淡出动画的持续时间
easing: 'easeInOutCubic' // 更改缓动函数
}
});
// 重新应用新的动画配置
chart.render();
7. 动画生命周期管理
事件监听与钩子
为了更好地管理动画的生命周期,VChart提供 了一系列事件监听器和钩子函数。例如,VGRAMMAR_HOOK_EVENT.AFTER_DO_RENDER事件可以在图表首次渲染完成后触发,而VGRAMMAR_HOOK_EVENT.ANIMATION_END则会在动画结束时触发。
this._event.on(VGRAMMAR_HOOK_EVENT.AFTER_DO_RENDER, () => {
// 图表首次渲染完成后的逻辑
});
this._event.on(VGRAMMAR_HOOK_EVENT.ANIMATION_END, ({ event }) => {
if (event.animationState === AnimationStateEnum.enter) {
// enter 动画结束后的逻辑
} else if (event.animationState === AnimationStateEnum.update) {
// update 动画结束后的逻辑
} else if (event.animationState === AnimationStateEnum.exit) {
// exit 动画结束后的逻辑
}
});
这段代码展示了如何在不同的动画阶段执行特定的逻辑,保证动画之间的平滑过渡,提升用户体验。
8. 差异检测与动画触发
差异检测
在数据更新过程中,VChart会自动进行差异检测,识别哪些数据点是新增的、更新的或移除的。基于这些信息,AnimateManager会触发相应的动画。
if (state === AnimationStateEnum.update) {
this.updateState(
{
animationState: {
callback: (datum: any, element: IElement) => element.diffState
}
},
noRender
);
}
这里的diffState属性表示元素的状态变化类型,如enter、update或exit。AnimateManager会根据这个属性来决定应用哪种类型的动画。
9. 动画的具体实现
具体动画函数
每个具体的动画函数(如Appear_FadeIn、ScaleInOutAnimation和Appear_FadeOut)定义了动画的具体行为。例如,Appear_FadeIn函数可能如下所示:
export const Appear_FadeIn: IAnimationTypeConfig = {
type: 'fadeIn',
duration: 800,
easing: 'easeInOutQuad',
channel: {
opacity: { from: 0, to: 1 }
}
};
这段代码定义了一个淡入动画,通过调整图形元素的opacity属性从0变到1来实现视觉上的淡入效果。
10. 动画状态管理
状态切换与更新
AnimateManager不仅管理normal动画,还负责处理其他状态下的 动画切换。例如,当有新数据加入时,enter状态的动画会被触发;当数据更新时,update状态的动画生效;而当数据被移除时,则是exit状态的动画起作用。
class AnimateManager extends StateManager implements IAnimate {
updateAnimateState(state: AnimationStateEnum, noRender?: boolean) {
if (state === AnimationStateEnum.update) {
this.updateState(
{
animationState: {
callback: (datum: any, element: IElement) => element.diffState
}
},
noRender
);
} else if (state === AnimationStateEnum.appear) {
// appear 状态下的动画逻辑
} else if (state === AnimationStateEnum.exit) {
// exit 状态下的动画逻辑
}
}
}
当图表元素进入update、appear或exit状态时,updateAnimateState方法会被调用,并将状态传递给内部的状态管理逻辑。这使得所有符合条件的元素都能够执行对应的动画。
总结
通过上述步骤,我们详细解读了VChart中数据更新动画的实现原理。VChart的数据更新动画系统设计巧妙地结合了工厂模式、状态管理器模式以及模块化的动画配置,不仅提供了丰富的内置动画效果,还支持高度定制化的需求。开发者可以根据实际应用场景灵活配置和组合不同的动画,创造出既美观又实用的可视化效果。具体来说:
-
**animationEnter**:适用于新数据点的入场动画,如淡入、生长等。 -
**animationUpdate**:适用于现有数据点的更新动画,如缩放、颜色渐变等。 -
**animationExit**:适用于旧数据点的退场动画,如淡出、缩小等。
这种设计确保了在数据变化时,图表能够以平滑且直观的方式呈现给用户,提升了交互体验和视觉吸引力。