搜索文章

输入关键词开始搜索

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

思考#ECharts#数据可视化#AI#程序设计#未来的路

背景

我们基于 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 逻辑中,添加如下代码即可:

// 新出现的元素:当前的位置在画布外,但是计算出的布局位置应该在画布内
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 的计算逻辑

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

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

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

el.shape.y = 132;

经验教训

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

未明确需求

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

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

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

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

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

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

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

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 个点了。

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

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

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 去解释,效率极高,而且往往能给我一些惊喜,把潜在的逻辑也解释出来。