ECharts扩展配置项-dvNameLocation
需求
问题重现 Demo
详见这个示例。
问题描述
轴标题属于绝对定位,无法保证标题文本一定位于图表上方或下方
x 轴的轴标题在 x 轴右侧显示时,是跟随 x 轴轴线的,不是在图表右下角,0 轴情况会出现问题(dvNameLocation)
技术方案
相关配置项:axis.nameLocation
生命周期:位于component:beforeupdate和component:afterupdate之间,通过对应组件的CartesianAxisView.render()方法触发调用。
1 | |
关键问题:找到计算和渲染之间的空档,把我们的逻辑(修改 axis.name 的 x 属性)插入进去。
代码位置:axisBuilder.ts 中的 axisName()方法,核心是这几行:
1 | |
grid 的计算时机和逻辑
计算时机
在 ECharts 的生命周期component:beforeupdate中修改。
在component:beforeupdate之前,因为我在这个生命周期里面已经能取到 grid 的包围盒了。
计算逻辑
如何获取 grid 的包围盒
src/coord/cartesian/cartesianAxisHelper.ts
1 | |
grid 和 axis.name 的计算相互依赖的问题
尬住了…
grid 的边距依赖 axis.name 的 padding(流式布局)
axis.name 的 padding 依赖 grid 的包围盒(dvNameLocation)
因此 dvNameLocation 不能这样修改,得改现有的 0 轴 axis.name 的逻辑
不对,可以的,顺序问题。先计算 grid,再计算 axis.name 即可。
(精)流程
看 ECharts 源码
找到 ECharts 的源码,理解其逻辑
比如这次就是搜索 nameLocation 这个配置项,定位到 axisBuilder.ts 这个文件的源码,经过打印输出调试,发现核心是这一句:
1 | |
然后查看其相关的变量(labelOffset、nameDirection、gap),发现是通过/src/component/axis/CartesianAxisView.ts 调用传入的:
1 | |
通过 CartesianAxisView.ts 就可以知道我需要的几个变量是如何获取的了(layout 的计算逻辑)。
注意阅读注释,比如 labelOffset,注释里面就明确说明了其作用:
labelOffset means offset between label and axis, which is useful when ‘onZero’, where axisLabel is in the grid and label in outside grid.
参考其他的类似实现
参考这个 paradigm-chart/src/extension/component/axis/extendLabelWidth.ts,采用了 install 的方式。
注意扩展的配置项还需要在 paradigm-chart/src/core/option.ts 中进行声明:
1 | |
规范
关于 extension 下文件的命名问题
如果只修改一个配置项,则文件名和配置项保持一致即可,比如:dvSplitLineNumber.ts
如果修改了多个配置,则以 extend+配置分类命名,比如:extendLabelWidth.ts
registerPreprocessor 机制
echarts-5.3.3/src/core/echarts.ts
1 | |
问题
可视化的其他人写什么
https://datav.iwencai.com/example.html#/static-page?id=259
写这里的 Parser 函数即可,比如 myThemeParser。
业务方的开发写什么
1、不写 Parser,后续我们升级,他们只需要更新 Parser 函数即可
2、配置主题,即写 option 配置项
定位问题
弄清楚每个元素相对什么元素定位。
比如 axis.name 是相对 grid 定位的
xAxis.name
Y 轴的 0 刻度在画布内部:需要偏移 0 轴的 Y 坐标 + Label 的高度
因为 axis.name 是相对 grid 里面的 0 轴 定位的,因此无需偏移 gridRectBoundingRect.
1 | |
lineHeight 导致的坑
范式里面设置了 lineHeight 为 12(估计是为了行间距美观度,默认的 fontSize 是 12),但是如果用户设置了 fontSize 的情况下,这个 lineHeight 会导致计算相关的场景出错,比如流式布局中计算 axis.name 的高度,就错误了(应该按 fontSize 算,结果取了 lineHeight 的高度,导致纵向对不齐)。
解决方案是把范式中的 lineHeight 相关配置去掉了。因为这个不管怎么设置都会有问题:
如果动态设置为 lineHeight = fontSize + 2,那么字体大的时候,这个行间距就显得太小了。
TODO
动画问题
动画的问题,得找到一个时机,每次 AxisBuilder 之前执行我们的逻辑
不行还有终极方案:patch-package

单次渲染没问题,多次渲染会出问题。
没问题:
1 | |
有问题:
1 | |
第一次渲染:正确
第二次渲染:从正确到错误
打印的执行顺序:
那么应该是:
第一次渲染:
- AxisBuilder-axisName
- adjustNameLocation
第二次渲染:
- AxisBuilder-axisName
- 动画开始(获取到错误的定位信息)
- adjustNameLocation(动画不走这个流程了,导致其不生效?)
通过打印 trace,二者都是在 render 中触发的:
1 | |
结论:
1、component的动画应该是在 series:beforeupdate 之前就开始了,这样才会取到错误的位置数据;
2、component的动画和series的动画执行时机是不一样的。
有个问题说不通:为什么一定是同侧同时有 xAxis.name 和 yAxis.name 才出现这个问题?
============
看起来是这样的顺序:
1、adjustNameLocation
2、setTimeout
3、adjustNameLocation
4、执行动画
也就是在动画的第一帧,这个 adjustNameLocation 生效了,然后在后续的帧里面,则没有生效,而是走的默认的 axisName 的逻辑。
结合 ECharts 的 lifecycle 来分析:
1 | |
这个扩展是写在series:beforeupdate里面的。
感觉还是时机的问题,动画执行时机比我们自定义的 adjustNameLocation 更靠前,此时获取的是 AxisBuilder-axisName 算出来的位置信息,因此出现这种情况。
而且有动画的情况下,adjustNameLocation 算出来的这个数值不会被使用。
禁用动画果然就没这个问题了。
另外发现仅在有双 Y 轴名称的情况下才会出现这个问题,这是为什么?
解决方案:
1、(TODO)在 AxisBuilder-axisName 和动画执行之间,插入我们的逻辑。
2、禁用 axis 的动画:animation: false
3、别同时设置 xAxis.name 和 yAxis.name 在同一侧
axisName 的动画在哪里触发的、时长如何控制的?
ECharts 注释掉了series:beforeeachupdate这个生命周期,为什么?
1 | |
如何禁用掉 AxisBuilder.ts 的 axisName 的计算?
这个不禁用掉,一旦切换数据,Y 轴从有 0 轴到无 0 轴,axis.name 的位置就会出现问题(出现 2 帧的插帧动画)。
方法一:用装饰器模式重写 AxisBuilder.ts 的 isNameLocationCenter()方法:
1 | |
opt.labelOffset是怎么计算的
关键看这个opt.labelOffset是怎么计算的,改为根据这个定位即可!
cartesianAxisHelper.ts 的 layout()方法。
1 | |
0 轴的逻辑是怎么计算的?
TODO
记得把 settings.json 中的 node_modules 的注释去掉。
经验教训
没搞懂原理,黑盒式开发
比如 grid、axisLabel、axisName、0 轴线等等这几个元素在布局时的关联关系。如果没搞懂,那么很容易变成打地鼠式的调试,因为每个元素都是一个变量(自变量),当有 n 个元素时,相互之间的影响复杂度就是(O)n^2 了。即时你临时调试出一个算法,很可能也是碰巧,稍微换下场景和配置,就会出现问题。
y=f1(a)+f2(b)+f3(c)
实际上后面我真正弄懂ECharts的布局与定位关联关系后,发现这个计算规则是非常简单的。
控制变量至关重要!
TDD 测试先行
贯彻这个理念,可以有效减少调试时间。
不一定是真的写单元测试,比如写好足够的 Demo,也算是一种 TDD 的思维。
像我后面,直接把 mobile-单 y 轴、pc-单 y 轴、pc-双 y 轴这几个场景的 Demo 都写好了,这样改动后马上可以验证各个场景的正确性,发现问题的效率得到了极大的提升。
多个组件(比如双 Y 轴)
同一个页面渲染多个图表
每个图表都要用到原始的 nameTextStyle.padding,但是我在每次渲染中都会去修改这个 nameTextStyle.padding。因此必须要有个策略,把原始的 nameTextStyle.padding 存储起来,跨不同的渲染周期复用,且要考虑重新 setOption 时这个 nameTextStyle.padding 的更新问题。
最终我用 WeakMap 来存储这个原始的 nameTextStyle.padding,每次渲染时,先从这个 WeakMap 中取出原始的 nameTextStyle.padding,然后再进行修改。
将每个图表实例的 axisModel 作为 WeakMap 的 key,这样就能解决同一个页面渲染多个图表的问题了。
1 | |
Debug 模式
每次调试 console.log()和 console.error()混用,很麻烦。干扰信息很多,不利于定位输出信息。
Canvas 绘制中英文的问题
结论:Canvas 绘制的英文,获取的包围盒宽度值,会大于其实际占据的宽度值。
同样的配置,KAmis 中画出来 yAxis.name 和 yAxis.axisLabel 对不齐,VISALL 画出来却是对齐的。
经过对比观察,发现 yAxis.axisLabel 的内容,一个是纯数字(KAmis),一个是单位做了格式化(VIALL)。
将 KAmis 的加上格式化,发现果然整齐些了。
Option 对比工具-Partial Diff
这是个 VSC 的扩展。
写代码的套路
我的adjustXAxisNameLocation()写得一塌糊涂,究其原因,是因为我写的时候根本没一个流程思路,就是按照流水账随性的写。
后面总结,发现其实可以按照这个思路写:
1、画图,方案设计
2、参数预处理(比如checkPrerequisitesForX(axisModel))
3、计算出后续所需的变量(比如 labelHeight、nameGap)
4、判断逻辑和布局计算(这部分应该是各种 if/else)
5、赋值 or 返回
这样有 2 个很大的好处:
1、思路清晰
2、写完后,可以很方便的把这个函数拆分成多个函数,每个函数只做一件事,这样代码更加清晰,可读性和可维护性大大提升
多线并行的问题
同时在处理几个事情:
- 熊鹏的接入问题
- StandardChart 的 CDN 访问失败的问题
- 股权图谱的开发
一个打断,半天接不起来,效率低下。
代码案例
组件的数据结构和最小循环多 Y 轴判断:
1 | |