需求
ECharts 的设计是每个元素在画布上绝对定位的,这样元素多了可能出现重叠问题。
设计师为了让业务方的不同场景都可以自动解决这些布局问题,希望我们能支持类似 DOM 的流式布局。
方案设计
Plan1:二次绘制(在组件层解决)
在各个组件绘制后,获取其包围盒信息,计算布局,然后设置 grid,二次绘制。
这个方案存在很多问题:
因为存在两次绘制,图表初始化会出现 2 次绘制结果的切换效果
(待确认)多周期数据的时间轴的场景,因为上面这个问题,画面各种抖动
严重的性能问题
Double 性能消耗,在数据多、动画多的情况下,尤为严重,导致帧数肉眼可见的下降。
和之前改 axisName 的定位同样的问题:有动画的情况下会出现异常
针对一些我们修改过渲染逻辑的配置(比如 dvNameLocation),无法获取其正确的渲染大小
1 2 3 4 5 6 7
| const axisView = api.getViewOfComponentModel(axis); setTimeout(() => { console.error('axisView', axisView.group.getBoundingRect()); }, 2000);
console.error('axisView', axisView.group.getBoundingRect());
|
这是因为 dvBoxLayout 进行了二次渲染的缘故。
时间轴组件会有问题
每一个周期的 Y 轴 label 的最大值是不相同的,我们没有每个周期都重新计算。
dataZoom 导致 grid 变更的问题
最终该方案被舍弃了。
Plan2:重写 ECharts 的布局逻辑(patch-package)
成本太高,且扩展性和适应性太差,很容易今后来个布局相关的其他需求就又解决不了了,Pass。
Plan3:组件预绘制(在配置层解决)
相当于增加了一个预渲染层。
流程:
1、整合用户配置+默认配置,得到一个最终传入组件的 option
2、获取这个 option 中和布局相关的几个组件的配置信息,用 ZRender 在内存中绘制这些组件
3、根据这些组件的包围盒信息,计算布局,然后设置 grid,进行绘制。
布局计算逻辑
顺序:
grid.left 影响 Y 轴轴线,所以 grid.left> Y 轴轴线
Y 轴轴线影响 axisName,所以 Y 轴轴线 > axisName
axisName:
左侧位置
这个是根据 Y 轴轴线居中对齐的。
外部接口(用到的 API)
获取元素对象
ExtensionAPI.getViewOfComponentModel()
1 2 3 4 5
| declare abstract class ExtensionAPI { abstract getViewOfComponentModel( componentModel: ComponentModel ): ComponentView; }
|
ExtensionAPI 提供的各个 API 方法都可以重点记忆下,我们写扩展的时候经常会用到。
以获取轴名称为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
export function getAxisNameEls(compView: CartesianAxisView) { const nameEls = [] as ZRText[];
compView.group.traverse((childEl) => { if (childEl.anid === 'name') { nameEls.push(childEl as ZRText); } });
return nameEls; }
const compView = api.getViewOfComponentModel(axisModel) as CartesianAxisView; const nameEls = getAxisNameEls(compView);
|
获取组件
GlobalModel.eachComponent()
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
| declare class GlobalModel extends Model<ECUnitOption> {
eachComponent<T>(cb: EachComponentAllCallback, context?: T): void; eachComponent<T>( mainType: string, cb: EachComponentInMainTypeCallback, context?: T ): void; eachComponent<T>( mainType: QueryConditionKindA, cb: EachComponentInMainTypeCallback, context?: T ): void; }
|
以获取所以 grid 组件为例:
1 2 3 4 5 6 7 8 9 10
| ecModel.eachComponent('grid', (gridModel: GridModel) => { enable = !!gridModel.get('dvBoxLayout.enable');
gridModel.__$$cacheDvBoxLayout = { leftValues: [], rightValues: [], topValues: [], bottomValues: [], }; });
|
获取配置项
model.get()
1 2 3 4 5 6 7 8 9 10 11 12
| declare class Model<Opt = ModelOption> { get<R extends keyof Opt>(path: R, ignoreParent?: boolean): Opt[R]; get<R extends keyof Opt>(path: readonly [R], ignoreParent?: boolean): Opt[R]; get<R extends keyof Opt, S extends keyof Opt[R]>( path: readonly [R, S], ignoreParent?: boolean ): Opt[R][S]; get<R extends keyof Opt, S extends keyof Opt[R], T extends keyof Opt[R][S]>( path: readonly [R, S, T], ignoreParent?: boolean ): Opt[R][S][T]; }
|
1 2 3 4 5 6 7 8
| gridModel.get('dvBoxLayout');
gridModel.get('dvBoxLayout.enable');
gridModel.get(['dvBoxLayout', 'enable']);
|
对外接口(对外暴露的 API)
内存绘制
用 ZRender 绘制。
https://ecomfe.github.io/zrender-doc/public/api.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const echarts = window.ThsDataVStandardChart.echarts;
const rect = new echarts.graphic.Text({ style: { text: '{a|afadsafasdfad}\n{b|sdfnjsdanvjksadnjv}', rich: { a: { width: 50, height: 20, padding: [1, 2, 3, 4], }, b: { width: 50, height: 20, padding: [1, 2, 3, 4], }, }, }, });
console.error('绘制的矩形的包围盒信息', rect.getBoundingRect());
|
属性未设置怎么画?
比如 axis.nameTextStyle,默认情况是没有 fontSize 等属性值的,类似这样:
1 2 3 4 5 6 7 8 9
| { "verticalAlign": "top", "padding": [ -20, 0, 20, 113.31640625 ] }
|
因为这是用户配置,需要拿到预处理的 option,就有值了。
比如 parseVennOption 这里的 option 就有了。
如何控制 ThemeParser 的执行顺序(解决依赖问题)?
因为 grid 这种,是依赖于 axis、dataZoom 等的大小和定位的。
之前主题中的 grid 逻辑
1、设置特殊的边距
2、合并默认主题
1
| util.merge(target, source);
|
PC
src/theme/pc/component/grid.ts
对普通图表、DynamicHistogram 分别设置了不同的边距。
mobile
src/theme/mobile/component/grid.ts
设置边距
ainvest
src/theme/ainvest/component/grid.ts
额外处理了 AttchedGrid 的情况。
GridParser 程序设计
1 2 3 4 5 6 7 8
| function preRenderComponents(option): Map<string, zrender.Displayable>;
computeLayout(componentMap);
apply();
|
(TODO)多个 grid 的场景如何处理?
axisView
只需要绘制 label 即可
1 2 3 4
| const gridRect = grid.coordinateSystem.getRect(); const axisView = api.getViewOfComponentModel(axis); const axisViewRect = axisView.group.getBoundingRect().clone(); axisViewRect.applyTransform(axisView.group.transform);
|
搞清楚 AxisView extends ComponentView 的渲染逻辑。
AxisBuilder.ts 吧?
经验
抓住本质:修改 option & 重绘
StandardChart、AIGC 可视化库,都是这个逻辑。
配置项预处理
registerPreprocessor
1 2 3 4 5 6 7 8 9 10
|
export function registerPreprocessor( preprocessorFunc: OptionPreprocessor ): void { if (indexOf(optionPreprocessorFuncs, preprocessorFunc) < 0) { optionPreprocessorFuncs.push(preprocessorFunc); } }
|
忽略了多个元素的情况
多个 grid、xAxix、yAxis、series 等等
挂载缓存数据
1
| gridModel.__$$cacheDvBoxLayout = { leftValues: [], rightValues: [] };
|
生命周期的执行顺序
生命周期的执行顺序
资料
dvBoxLayout
实现网格布局的自适应,根据左右两侧 Y 轴的元素内容宽度动态计算网格的左右两侧留白距离。