0%

G2源码解析之自适应应映射范围

G2源码解析之函数式API与规范式API

版本:V5.2.8

背景

首先介绍两个定义:

图表画布:整个用于绘制图表内容的区域,包括图例、轴、以及图表名称。

图表数据映射区域X轴与Y轴框定的,用于将柱子或者折线映射到像素的画布区域。

EChartsPadding仅解决图元向图表画布映射范围问题不同,G2的Padding解决的不是图元布局问题而是真正意义上的图表数据映射区域到图表的边界的距离。

我们先来看G2的解决方案:

G2当图例、Y轴名称、Y轴标签或其余图元位置或大小发生变化时会挤占图表空间

优点是:更加智能,且不会产生重叠,用户只需关心一个边距(Padding)的概念即可配置出相对好看的图表,更加易用

缺点是:当出现极端数据例如10个数量级或者跟大的数值,或者极长的数据项名称时,图表的空间会被过度挤压以至于无法查看。

我们再来看ECharts的解决方案:

ECharts当图例、Y轴名称、Y轴标签或其余图元位置或大小发生变化时会不会挤占图表空间,不会更改数据到图表像素映射,如果图例过长一般会发生遮挡,如果轴标签过长则会产生覆盖。

优点:不会破坏图表的布局方式,在图表包含异常值时优先保证图表的正常显示。

缺点:不够智能,具有异常值时会产生遮挡或截断。

本文不讨论这两种实现方案哪种更优,仅尝试从源码的角度分析G2是如何实现自适应映射范围的。

截图

G2截图来源于ChartCube - 在线图表制作工具 (alipay.com)

image-20230220182649979

G2正面案例:当图例居左时

image-20230220182724894

G2正面案例:当图例居下时

image-20230220192451326

G2反面案例:当图例名称过长时

image-20230220193328103

ECharts反面案例:图例位置单独布局

image-20230220193215573

ECharts正面案例:虽然包含异常值,但图元和图例均有正常显示

简单实践

image-20230220194701197

将chartcube的代码放到本地执行之后的效果

经简单测试发现对于图例的自适应应该是chartcube层面实现的功能,这里不再讨论,仅聚焦于Y轴标签过长时的自适应方案

核心代码

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
61
62
63
64
65
66
67
68
69
70
71
/**
* 递归计算每个 view 的 padding 值,coordinateBBox 和 coordinateInstance
* @param isUpdate
*/
protected renderPaddingRecursive(isUpdate: boolean) {
// 1. 子 view 大小相对 coordinateBBox,changeSize 的时候需要重新计算
this.calculateViewBBox();
// 2. 更新 coordinate
this.adjustCoordinate();
// 3. 初始化组件 component
this.initComponents(isUpdate);
// 4. 布局计算每隔 view 的 padding 值
// 4.1. 自动加 auto padding -> absolute padding,并且增加 appendPadding
this.autoPadding = calculatePadding(this).shrink(parsePadding(this.appendPadding));
// 4.2. 计算出新的 coordinateBBox,更新 Coordinate
// 这里必须保留,原因是后面子 view 的 viewBBox 或根据 parent 的 coordinateBBox
this.coordinateBBox = this.viewBBox.shrink(this.autoPadding.getPadding());
// 调整 coordinate 的坐标范围。
this.adjustCoordinate();

// 刷新 tooltip (tooltip crosshairs 依赖 coordinate 位置)
const tooltipController = this.controllers.find((c) => c.name === 'tooltip');
tooltipController.update();

// 同样递归处理子 views
const views = this.views;
for (let i = 0, len = views.length; i < len; i++) {
const view = views[i];
view.renderPaddingRecursive(isUpdate);
}
}


/**
* 调整 coordinate 的坐标范围。
*/
public adjustCoordinate() {
const { start: curStart, end: curEnd } = this.getCoordinate();
const start = this.coordinateBBox.bl;
const end = this.coordinateBBox.tr;

// 在 defaultLayoutFn 中只会在 coordinateBBox 发生变化的时候会调用 adjustCoordinate(),所以不用担心被置位
if (isEqual(curStart, start) && isEqual(curEnd, end)) {
this.isCoordinateChanged = false;
// 如果大小没有变化则不更新
return;
}
this.isCoordinateChanged = true;
this.coordinateInstance = this.coordinateController.adjust(start, end);
}

/**
* 更新坐标系对象
* @param start 起始位置
* @param end 结束位置
* @return 坐标系实例
*/
public adjust(start: Point, end: Point) {
this.coordinate.update({
start,
end,
});

// 更新坐标系大小的时候,需要:
// 1. 重置 matrix
// 2. 重新执行作用于 matrix 的 action
this.coordinate.resetMatrix();
this.execActions(['scale', 'rotate', 'translate']);

return this.coordinate;
}

总结

简单梳理下G2的布局逻辑:

  1. 首先给定图表的大小、比如宽高都为100,这个范围即为用于布局的坐标系范围。

  2. 之后会生成各个组件、例如X轴、Y轴。根据X轴的位置和Y轴的位置。在布局范围的外部(上下左右)添加坐标轴组件。

  3. 重新计算图表的真实大小(例如有一个在下方高为20的X轴现在图表的整体高度为120)。

  4. 重新计算出新的用于布局的坐标系范围并更新用于坐标计算的矩阵。

这种在布局范围之外添加组件之后反向更新布局范围的方式决定了其不会有重叠。