排查动态柱状图的动画问题

背景

我们基于 ECharts 的竞赛图 封装了一个动态柱状图组件,用于对多周期数据进行动态展示。这是之前的同事封装的,现在业务上线之后,发现当本周期有新数据出现时,效果非常差:

  • 新出现的数据中,文字没有随着柱子定位,而是一开始就固定在最终的位置了
  • 新出现的柱子的定位是从下往上飞到目标位置,很乱
  • 新出现的柱子的高度是从 0 到目标高度,也导致视觉效果很差

线上的效果

业务方预期的效果是类似这样的:

期望的效果

程序流程

通过 console.log(console.trace())查看调用栈。

  • 定位到_renderNormal()方法
  • 定位到 diff
  • 定位到 diff 中的 update
  • 发现每次动画都会触发二次 update,第一次是更新数据,第二次排序
  • 定位到 updateRealtimeAnimation()
  • 定位到 updateProps()

两次渲染流程

  • 首次渲染:setOption,根据原始数据的顺序,计算 layout,创建初始元素

  • 第 2 次渲染:_updateSortWithinSameData,排序,计算实际定位

判断两次渲染的差异变量是isChangeOrder,总之就是 sort 的修改带来了 layout 的变更。
sort 没问题的,不用修改,要改的是sort 之前的逻辑,也就是 setOption 触发的 layout 计算逻辑,这个阶段决定了柱子初始的位置和形状,而我们目前的问题就是柱子初始的位置和形状不对

render1

完整流程图

render1

解决方案

核心:搞清楚柱子首次渲染的 y 定位和 height 怎么算的。

在 diff 的 update 的 realtimeSortCfg 逻辑中,添加如下代码即可:

1
2
3
4
5
6
7
8
9
10
// 新出现的元素:当前的位置在画布外,但是计算出的布局位置应该在画布内
if (
el.shape.y >= coordSysClipArea.y + coordSysClipArea.height &&
layout.y !== el.shape.y
) {
// y方向的定位
el.shape.y = layout.y;
// 柱子高度
el.shape.height = layout.height;
}

一些 Q&A

为什么首次的动画是没有上升的?

因为这个首次是真的没有,哪怕首屏没显示的 China,其实也是绘制了的,或者说数据是初始化了的。

初始化的柱子画了个高度=1 的,宽度=value 的,所以当起出现时,看起来高度会变大。

layout 的计算逻辑

1
var layout = getLayout[coord.type](data, newIndex, itemModel);

为什么手动设置的 y 的定位不对?

设置错了,不是 el.y,是 el.shape.y:

1
el.shape.y = 132;

经验教训

这个问题我从周四下午 3 点开始排查,直到周五晚上 9 点才搞定了,效率极低,主要原因是走了很多弯路,下面总结一下经验教训:

未明确需求

一开始没仔细看出问题的视频,导致我一直在排查文字动画未跟随柱子的问题,实际上业务方预期的效果根本不用改文字,而是应该改柱子动画。

这让我走错了路,浪费了不少时间。

未构建最小、最易排查的数据环境

我是直接在现有 Demo 的大量数据中进行排查的,这会带来一些冗余信息。
工欲善其事,必先利其器,因此第一步应该先精简数据,构建最小、最易排查的程序环境。
后面我改为只有 2 个周期的数据,且营造了大量飞入的数据,这样排查起来就目的性更强了。

输出调试信息时,未增加逻辑断点

当数据较多时,日志会很多,造成大量的干扰信息。

应该添加类似这样的断点:

1
2
3
4
5
if (['China'].includes(itemModel.option[3])) {
console.log('add itemModel', dataIndex, itemModel);
console.log('add layout', itemModel.option[3], JSON.stringify(layout));
console.log('el', el);
}

一开始就陷入细节

我一开始就去跟踪代码细节,加上自己不熟悉这部分的代码,导致前面很长时间连问题的关键代码都没定位到。

应该从大流程开始,都不用看代码,先在脑子里面思考下这个动画的流程是怎么样的(数据处理、渲染流程等等),然后再去看代码,别一开始就陷入细节。

没画流程图

当信息很多时(不熟悉的内容,包括流程、程序设计等,往往会给你带来很多信息和概念),我的思维往往很容易乱掉,图是理清楚思维的好办法。周五下午我画出流程图后,很快就理清楚思路,找到了问题的关键点。

没有用 debugger 跟踪代码,导致找漏了修改点

有 3 个地方的修改可能和本次相关:

  • src/helper/dynamicHistogram

  • patch-package 修改了 echarts 的源码

  • src/extension/series/bar/bar.ts 重写了_dataSort

下意识的猜想中,我只找到了第一个,剩下二个根本没想到,后面排查了很久才发现了这 2 个。

如果一开始就 debugger 调试程序,就可以快速定位到后面的 2 个点了。

没有通过注释代码的方式快速定位

后面通过这个方式,很快就定位到了横向生长动画的控制逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function updateRealtimeAnimation(
realtimeSortCfg,
seriesAnimationModel,
el,
layout,
newIndex,
isHorizontal,
isUpdate,
isChangeOrder
) {
var seriesTarget;
var axisTarget;
//isHorizontal = false
if (isHorizontal) {
axisTarget = {
x: layout.x,
width: layout.width,
};
seriesTarget = {
y: layout.y,
height: layout.height,
};
} else {
axisTarget = {
y: layout.y,
height: layout.height,
};
seriesTarget = {
x: layout.x,
width: layout.width,
};
}

// 横向的生长动画
if (!isChangeOrder) {
// Keep the original growth animation if only axis order changed.
// Not start a new animation.
(isUpdate ? updateProps : initProps)(
el,
{
shape: seriesTarget,
},
seriesAnimationModel,
newIndex,
null
);
}
// 纵向位移动画
var axisAnimationModel = seriesAnimationModel
? realtimeSortCfg.baseAxis.model
: null;
(isUpdate ? updateProps : initProps)(
el,
{
shape: axisTarget,
},
axisAnimationModel,
newIndex
);
}

确定了和这里修改的属性有关,后续只需要排查这个函数的几个参数(layout)是怎么计算得来的即可。

GPT 解释代码

对于不熟悉的逻辑,让 GPT 去解释,效率极高,而且往往能给我一些惊喜,把潜在的逻辑也解释出来。