技术拆解-动态关系可视化组件

我们做了什么,解决什么问题?

解决业务方必须用命令式调用 ZRender 的问题。

业务方可以通过声明式调用我们的库,给其屏蔽底层 ZRender 的命令式调用,降低业务方成本和心智负担。

这个和 AIGC 组件库本质上是一样的,都是向下封装底层,向上提供声明式配置

(TODO)这个需要抽象出一套通用的程序设计方案。

库的定位

纯绘图库。

节点的绘制、布局、动画都是业务方计算好的。

环境搭建

1
npm run dev-serve

业务方的难点

节点的绘制,目前是 formatter 的方式

因为内部节点的时间依赖于外部的整体时间,这样一旦某个节点的前置时间变更了,那这个节点就得跟着变更动画时间。

目前已经改为了组件的形式,类似 vue

formatter=>vue 组件

方案设计

方案设计文档

原则:数据驱动

ZRender 已经实现了帧循环调度,我们只需要关注如何将用户配置转为 ZRender 的帧配置即可。

1
2
// 解析用户配置为zrender配置
elementGroupAnimationAction(chart._group, 'start', easing);

文档

docs 下的文档分类做得很好。

https://diataxis.fr/

文件结构

src 下其实分为了 2 个具有上下级关系的独立的库:

外层库:DynamicRelationGraphs.ts,封装了不同周期数据的动画切换逻辑

底层库:libs 下面,这部分原则上应该独立出去,以第三方包的形式引入

核心代码

重点看这 2 部分即可:

  • core:机制

  • helper/animation:将用户配置转为 ZRender 的动画配置

动画操控

libs/relation-graphs/RelationGraph.ts

数据更新

libs/relation-graphs/core/RelationGraph.ts 中的updateDataset(dataset: Data)方法。

动画

猜测是直接用 ZRender 的帧动画切换?

Clip

ZRender 的 Clip 的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class Clip {
private _life;
private _delay;
private _inited;
private _startTime;
private _pausedTime;
private _paused;
animation: Animation;
loop: boolean;
easing: AnimationEasing;
easingFunc: (p: number) => number;
next: Clip;
prev: Clip;
onframe: OnframeCallback;
ondestroy: ondestroyCallback;
onrestart: onrestartCallback;
constructor(opts: ClipProps);
step(globalTime: number, deltaTime: number): boolean;
pause(): void;
resume(): void;
setEasing(easing: AnimationEasing): void;
}

在 ZRender 中,Clip 类通常用于定义动画剪辑,它是一个封装了动画生命周期和行为的类。根据你提供的代码片段,这个 Clip 类包含以下属性和方法:

  • _life: 动画剪辑的总寿命,即动画从开始到结束的总时间。
  • _delay: 动画剪辑的延迟时间,即从动画创建到实际开始播放的时间。
  • _inited: 表示动画剪辑是否已经初始化的标志。
  • _startTime: 动画剪辑的开始时间。
  • _pausedTime: 动画剪辑暂停时的时间。
  • _paused: 表示动画剪辑是否处于暂停状态的标志。
  • animation: 动画对象,可能包含具体的动画行为和参数。
  • loop: 布尔值,表示动画是否循环播放。
  • easing: 动画的缓动函数类型,用于控制动画的速度变化。
  • easingFunc: 缓动函数,根据给定的比例返回一个介于 0 到 1 之间的值,用于调整动画的播放速度。
  • next: 指向下一个动画剪辑的引用。
  • prev: 指向前一个动画剪辑的引用,用于形成动画剪辑链。
  • onframe: 每帧动画执行时调用的回调函数。
  • ondestroy: 动画剪辑销毁时调用的回调函数。
  • onrestart: 动画剪辑重新开始时调用的回调函数。
  • constructor(opts: ClipProps): 构造函数,用于创建一个新的  Clip  实例,opts  是一个包含初始化参数的对象。
  • step(globalTime: number, deltaTime: number): 动画剪辑的步骤函数,用于更新动画状态。globalTime  是全局时间,deltaTime  是自上次调用以来的时间差。
  • pause(): 暂停动画剪辑的方法。
  • resume(): 恢复动画剪辑的方法。
  • setEasing(easing: AnimationEasing): 设置动画剪辑的缓动函数的方法。

这个 Clip 类是 ZRender 动画系统中的一部分,用于控制和组织动画的播放。开发者可以通过创建 Clip 对象并设置相应的属性来定义动画的行为,然后将其添加到 ZRender 的动画队列中进行播放。

组件库的局限性

只适用于纯展示的场景。

如果涉及交互修改数据和状态,就会有问题。比如用户点击后节点颜色变红。因为这涉及修改帧数据了。

问题案例

Rect 中的文字绘制不出来

ZRender5 的 API 有变更:https://github.com/ecomfe/zrender/issues/1074

另外组件库本身有问题,这个拷贝,把 ZRText 对象拷贝成 JSON 了:

1
2
3
4
5
6
7
8
9
10
/**
* 获取布局后的数据
*/
private __getLayoutData(datasets: Datasets): Datasets {
const layoutOptions = this._option.layout;
// eslint-disable-next-line
// return zrender.util.clone(layout(zrender.util.clone(datasets), layoutOptions));
// eslint-disable-next-line
return zrender.util.clone(zrender.util.clone(datasets));
}

解决方法有 3 个:

修复组件库

1
2
3
4
5
6
7
/**
* 获取布局后的数据
*/
private __getLayoutData(datasets: Datasets): Datasets {
const layoutOptions = this._option.layout;
return layout(zrender.util.clone(datasets), layoutOptions);
}

扩展绘图功能

修改 Node.ts 的__updateBaseShape方法,根据传入的参数实例化 ZRText 对象,类似这样:

1
baseShape._textContent = new zrender.Text(props.textContent);

通过 customRender 实现

修改初始化 DynamicRelationGraphs 的第三个参数 option,类似这样(注意:这个是伪代码,待验证):

1
2
3
4
5
const option.nodes.customRender = function (relationGraphs, node, option) {
const text = new zrender.Text(node._option.render.opts.textContent);
node.group.add(text);
}