!!!###!!!title=全局变形动画——VisActor/VChart 社区贡献者文档!!!###!!!!!!###!!!description=---title: 10.2 全局形变动画key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

10.2 全局形变动画

分数: 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

魔力之帧(上):前端图表库动画实现原理一幅生动的可视化作品往往少不了动画的参与。无论是各色各样的图表还是叙事作品,组织周 - 掘金

在 10.1 中初步了解了 VChart 对动画系统的设计和图表的创建示例,这一节继续介绍在使用 VChart 中,不同图表配置之间切换时的过渡设计。

定义

VChart 提供了各个系列间相关切换的形变动画,我们称全局形变动画

在通过 updateSpec 来更新图表配置时,VChart 会检测新旧图表的两个相关联的系列,是否符合形变动画的条件,从而执行一对一、一对多或多对一的图形之间的动态过渡。 全局形变动画能让用户在展示的图表类型发生变化时有更好的视觉体验,避免看上去是瞬间变化的感觉,毕竟,视觉舒适是我们在展示数据和分析数据的过程中所应当关注的一个重要因素。

https://visactor.com/vchart/api/API/vchart 参考接口文档

updateSpec
异步spec 更新,会自动渲染图表不需要再调用 renderAsync() 等渲染方法。
/**
 * spec 更新
 * @param spec
 * @param forceMerge 是否强制合并,默认为 false
 * @param morphConfig morph 动画配置
 * @returns
 */
updateSpec: (spec: ISpec, forceMerge?: boolean, morphConfig?: IMorphConfig) => Promise<IVChart>;

效果示例

下面通过两个示例配置说明这类过渡动画的效果:

一对一动画

一对一动画是指两个不同的图形之间的过渡动画。例如在下面这个例子中,展示了我们在饼图和柱状图之间切换时的全局动画:

/**
 * 自1.12.0后,全局形变动画需要手动注册才能生效
 *
 * import { registerMorph } from '@visactor/vchart';
 *
 * registerMorph();
 *
 * 自2.0.0开始,全局形变动画默认开启,不再需要手动注册
 */

VCHART_MODULE.registerMorph && VCHART_MODULE.registerMorph();

const pieSpec = {
  type: 'pie',
  data: [
    {
      values: [
        { type: '1', value: Math.random() },
        { type: '2', value: Math.random() },
        { type: '3', value: Math.random() }
      ]
    }
  ],
  outerRadius: 0.8,
  innerRadius: 0.6,
  valueField: 'value',
  categoryField: 'type',
  tooltip: false
};

const barSpec = Object.assign({}, pieSpec, {
  type: 'bar',
  xField: 'type',
  yField: 'value',
  seriesField: 'type'
});

const specs = [pieSpec, barSpec];

const vchart = new VChart(specs[0], { dom: CONTAINER_ID });

vchart.renderSync();
let count = 1;
setInterval(() => {
  vchart.updateSpec(specs[count % 2]);
  count++;
}, 2000);


一对多动画

一对多动画是指一个图形元素向多个图形元素过渡的动画。例如在下面这个例子中,展示了我们在柱状图和散点图之间切换时的全局动画,其中,将一个大的柱子拆分为多个散点的动画就是一对多动画。

/**
 * 自1.12.0后,全局形变动画需要手动注册才能生效
 *
 * import { registerMorph } from '@visactor/vchart';
 *
 * registerMorph();
 *
 * 自2.0.0开始,全局形变动画默认开启,不再需要手动注册
 */

VCHART_MODULE.registerMorph && VCHART_MODULE.registerMorph();

function calculateAverage(data, dim) {
  let total = 0;
  for (let i = 0; i < data.length; i++) {
    total += data[i][dim];
  }
  return (total /= data.length);
}

function generateData(type) {
  const data = [];
  for (let i = 0; i < 10; i++) {
    data.push({ x: i, y: Math.random(), type });
  }
  return data;
}
const DataA = generateData('A');

const DataB = generateData('B');

const barSpec = {
  type: 'common',
  series: [
    {
      type: 'bar',
      data: { values: [{ value: calculateAverage(DataA, 'y'), type: 'A' }] },
      xField: 'type',
      yField: 'value',
      morph: {
        morphKey: 'A'
      }
    },
    {
      type: 'bar',
      data: { values: [{ value: calculateAverage(DataB, 'y'), type: 'B' }] },
      xField: 'type',
      yField: 'value',
      morph: {
        morphKey: 'B'
      }
    }
  ],
  axes: [
    { orient: 'left', type: 'linear', max: 1 },
    { orient: 'bottom', type: 'band' }
  ]
};

const scatterSpec = {
  type: 'common',
  series: [
    {
      type: 'scatter',
      data: { values: DataA },
      xField: 'x',
      yField: 'y',
      seriesField: 'type',
      morph: {
        morphKey: 'A',
        morphElementKey: 'type'
      }
    },
    {
      type: 'scatter',
      data: { values: DataB },
      xField: 'x',
      yField: 'y',
      seriesField: 'type',
      morph: {
        morphKey: 'B',
        morphElementKey: 'type'
      }
    }
  ],
  axes: [
    { orient: 'left', type: 'linear', zero: false, max: 1 },
    { orient: 'bottom', type: 'band' }
  ]
};

const specs = [barSpec, scatterSpec];

const vchart = new VChart(specs[0], { dom: CONTAINER_ID });

vchart.renderSync();
let count = 1;
setInterval(() => {
  vchart.updateSpec(specs[count % 2]);
  count++;
}, 3000);

多对一动画

多对一动画是指多个图形元素过渡到一个元素。例如,在上面的例子中,我们可以让散点系列的多个点合并为一个大的柱子。

效果实现的源码执行过程解读

通过配置可以说明不同图表的切换是通过更新配置来实现的,并且在形变动画开启的情况下会自动识别系列图元的切换过渡效果,下面对默认效果的设置做一个说明。

草稿 全局动画的实现解读 x

全局动画是指那些作用于整个图表级别的动画效果,它们可以应用于图表加载时的整体入场动画、数据更新时的统一变化动画,以及图表销毁前的整体退场动画。在 VChart 中,全局动画的设计和实现依赖于几个核心组件和机制,包括Factory类、AnimateManager类、IAnimationSpec接口等。

1. 动画注册与管理

Factory 类

Factory类是动画系统中的一个关键角色,它负责管理和注册各种类型的动画。通过静态方法registerAnimation,我们可以将特定的动画逻辑与名称关联起来,以便后续使用。

class Factory {
  static registerAnimation(key: string, animation: (params?: any, preset?: any) => MarkAnimationSpec) {
    Factory._animations[key] = animation;
  }
}

当需要为某个图表元素添加动画时,可以通过Factory.getAnimationInKey获取已注册的动画,并将其应用到对应的图元或图形元素上。

2. 动画配置结构

IAnimationSpec 接口

IAnimationSpec接口定义了动画配置的基本结构,涵盖了从入场(animationAppear)到退场(animationDisappear)的各种状态。每个状态都可以接受布尔值(启用/禁用)、预设配置对象或自定义配置对象作为参数。

interface IAnimationSpec<MarkName extends string, Preset extends string> {
  animationAppear?: boolean | IStateAnimateSpec<Preset> | IMarkAnimateSpec<MarkName>;
  animationEnter?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>;
  animationUpdate?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>;
  animationExit?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>;
  animationDisappear?: boolean | ICommonStateAnimateSpec | IMarkAnimateSpec<MarkName>;
  animationState?: boolean | IStateAnimationConfig;
  animationNormal?: IMarkAnimateSpec<MarkName>;
}

这些配置项允许开发者灵活地控制不同状态下动画的行为,例如设置持续时间、缓动函数、动画类型等。

3. 动画状态管理

AnimateManager 类

AnimateManager继承自StateManager并实现了IAnimate接口,用于管理动画的状态。它提供了方法来更新动画状态,并根据当前状态触发相应的动画逻辑。

class AnimateManager extends StateManager implements IAnimate {
  updateAnimateState(state: AnimationStateEnum, noRender?: boolean) {
    if (state === AnimationStateEnum.update) {
      // 更新状态下的动画逻辑
    } else if (state === AnimationStateEnum.appear) {
      // 出现状态下的动画逻辑
    } else {
      // 其他状态下的动画逻辑
    }
  }
}

此外,AnimateManager还负责生成唯一的标识符(ID)和信号名称,确保每个动画实例都能被正确识别和管理。

4. 动画配置生成

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 === 'normal') {
      userStateConfig && (config.normal = userStateConfig as IAnimationTypeConfig);
      continue;
    }

    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;
}

此函数会处理默认配置和用户配置的合并,并且考虑到了某些状态(如normal)可以直接使用用户提供的配置,而不需要额外处理。

5. 全局动画的具体实现

全局动画的注册

以折线图或区域图为例,registerVGrammarLineOrAreaAnimation函数展示了如何批量注册一系列动画方法。这些动画涵盖了点增长、点移动、裁剪等效果,并适用于 X 轴和 Y 轴方向。

const registerVGrammarLineOrAreaAnimation = () => {
  View.useRegisters([
    registerGrowPointsInAnimation,
    registerGrowPointsOutAnimation,
    registerGrowPointsXInAnimation,
    registerGrowPointsXOutAnimation,
    registerGrowPointsYInAnimation,
    registerGrowPointsYOutAnimation,
    registerClipInAnimation,
    registerClipOutAnimation
  ]);
};

全局动画的初始化

在具体系列的实现文件中(如柱状图、饼图等),通常会在初始化阶段调用initAnimation方法来设置动画配置。这个方法会结合用户提供的配置和默认配置,生成最终的动画配置,并将其应用到相应的图元或图形元素上。

initAnimation(): void {
  const animationParams = getGroupAnimationParams(this);
  const appearPreset = (this._spec?.animationAppear as IStateAnimateSpec<ScatterAppearPreset>)?.preset;
  this._symbolMark.setAnimationConfig(
    animationConfig(
      Factory.getAnimationInKey('scatter')?.({}, appearPreset),
      userAnimationConfig(SeriesMarkNameEnum.point, this._spec, this._markAttributeContext),
      animationParams
    )
  );
}

这里,animationConfig函数用于合并默认配置和用户配置,而userAnimationConfig则负责提取用户提供的动画配置信息。最后,通过setAnimationConfig方法将生成的配置应用到具体的图元上。

6. 动画任务的执行

IAnimationTask 接口

对于复杂的动画序列,VChart 引入了IAnimationTask接口来描述动画任务的数据结构。每个任务包含时间偏移、动作队列和后继任务列表,形成了一种链式动画执行机制。

interface IAnimationTask {
  timeOffset: number;
  actionList: Action[];
  nextTaskList: IAnimationTask[];
}

这种设计使得多个动画任务可以按顺序或并发执行,从而实现更加复杂和细腻的动画效果。

7. 示例:创建全局入场动画

假设我们要为一个新创建的柱状图添加一个全局的淡入入场动画,以下是详细的实现步骤:

  • 定义动画配置:首先,在图表配置中指定animationAppeartrue,表示启用入场动画。同时,可以进一步定制动画的具体行为,比如选择淡入效果、设置持续时间和缓动函数。
const chartSpec = {
  // ... 其他配置 ...
  animationAppear: {
    type: 'fadeIn',
    duration: 1000,
    easing: 'easeInOutQuad'
  },
  series: [
    {
      type: 'bar',
      data: [/* 数据数组 */]
    }
  ]
};

  • 注册淡入动画:接着,我们需要确保淡入动画已经被正确注册到系统中。这一步骤通常在项目启动时完成,或者在需要的地方显式调用。
import { Factory } from '@visactor/vchart';
import { Appear_FadeIn } from './series/bar/animation';

Factory.registerAnimation('fadeIn', Appear_FadeIn);

  • 初始化图表实例:有了上述配置之后,我们可以初始化一个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'   // 使用浅色主题
  }
});

  • 触发动画:一旦图表被渲染出来,任何数据的变化都会自动触发动画。例如,当页面首次加载时,所有柱子将以淡入的方式逐渐显现;当有新的数据加入时,新柱子也会以同样的方式进入。

  • 手动控制动画:如果需要对动画进行更精细的控制,比如暂停或恢复动画,可以使用VChart实例提供的相关方法。

// 暂停所有正在进行的动画
chart.pauseAnimation();

// 恢复之前暂停的动画
chart.resumeAnimation();

总结

通过以上步骤,我们详细解读了 VChart 中全局动画的实现原理。VChart 的动画系统设计巧妙地结合了工厂模式、状态管理器模式以及模块化的动画配置,不仅提供了丰富的内置动画效果,还支持高度定制化的需求。开发者可以根据实际应用场景灵活配置和组合不同的动画,创造出既美观又实用的可视化效果。

本文档由以下人员修正整理

玄魂