开发笔记-Insight的文本与曲线布局

需求

现有实现的源码阅读

方案设计

Insight 类型的,标签最多 3 个:最大、最小、异常值

如果是显示事件类的,数据量就可能很多,而且当前 F10 的事件没有权重的属性,无法根据权重排序筛选

流程

Model:把用户的配置项转换成 echarts 配置项

由于这次仍然是用的 dvMarker,因此 Model 不用修改,直接完全沿用之前的即可。

调用 flag 初始化图形元素

模仿 flag 新增一个 annotation,这个工作量比较大。

  1. 计算 flag 中 point 和 label 的相对位置

  2. 算出 flag 的包围盒矩形

  3. 算出 2 个曲线点的坐标

flag 的返回类型 markerRenderItemReturn ,就是 echarts 的自定义类型:

1
2
export type markerRenderItemReturn = CustomSeriesRenderItemReturn;
import { CustomSeriesRenderItemReturn } from 'echarts';

处理布局信息

只需要算 Y 轴的布局,直接全部沿用 flag 的布局,无需修改

这里有个问题:Y 轴移动后,整体的包围盒大小也是会变更的!

先不考虑这个,减少变量,不然碰撞无法静止下来。

算法

概念

concept

P: Point 数据点的坐标

L: Label 文本标签

C: Center 画布中心点

新增的配置项

xGap:point 和 label 在 x 上的空隙,单位像素

yGap:point 和 label 在 y 上的空隙,单位像素

linkDirection: 线和 label 的连接点方向,horizontal、vertical

限制条件

数据点的 X 是不能变动的,这意味着单个注解的一侧的 x 坐标肯定是固定的

程序流程

1、内存中绘制文本 Label,计算其高宽

2、根据数据点 Point 距离画布中心 C 的位置,确定该 Insight 的文本 Label 相对数据点的绘制方向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const distanceToCenterX = abs(Px - Cx);
const distanceToCenterY = abs(Py - Cy);
const W = 画布宽度;
const H = 画布高度;
let labelDirectionX, labelDirectionY;

if (distanceToCenterX > W / 4) {
labelDirectionX = 近C端;
} else {
labelDirectionX = 远C端;
}

if (distanceToCenterY > H / 4) {
labelDirectionY = 近C端;
} else {
labelDirectionY = 远C端;
}

3、确认每个 Insight 的包围盒(Pint + Label)的大小

4、因为限制条件的缘故,X 不能动,因此调整 Y,步进算法解决重叠问题,注意设置 step 次数上限

5、仍然排不下,则根据权重,隐藏不重要的 Insight

开发准备

定义数据结构

构建开发环境

把 VISALL 最终生成的数据拷贝一份过来

SD 里面搞个 Demo 页面

找到获取所需数据的各个 API

获取文本的内容

计算文本的尺寸

计算文本+数据点的包围盒

获取画布中心点的坐标

获取画布的左上和右下的坐标(是否可以先用相对位置来计算)

Takeaways

(精)通用程序设计套路:围绕生命周期进行

  • prepareXXX():数据预处理,将用户传入的数据和配置,转为程序内部的数据结构和存储方式

  • init():初始化,如果是 View 相关的代码,一般会包括绘图、事件监听等

  • update():数据驱动变更,注意这里一般只处理纯数据,不涉及绘图

  • destroy():销毁

(精)新增一个 Series 的流程

注意,代码全部都应该写在 extension 目录下。

  • 新增图元:

    • 通用图元在 src\extension\graphic 目录下,可以参考 CurveLine.ts;

    • 非通用图元放在具体的 series 目录下,比如 src\extension\series\dvTwoWayTree\shape.ts

  • 数据处理:参考 src\extension\series\dvTwoWayTree\TwoWayTreeSeries.ts

  • 布局计算:参考 src\extension\series\dvTwoWayTree\twoWayTreeLayout.ts

获取数据点的坐标

1
2
3
4
5
6
7
// canvas坐标
const coords = [api.value(0), api.value(1)];
// console.error(api, coords);

// 绝对坐标
const point = api.coord([coords[0], coords[1]]) as [number, number];
console.error(api, coords, point);

获取画布的包围盒

这是页面的像素坐标:

1
const gridRect = params.coordSys;

获取文本包围盒

1
2
3
4
5
6
7
8
9
10
11
12
function getTextRect(textStyle: CustomTextStyle) {
const content = new graphic.Text(textStyle);
const { x, y, width, height } = content.getBoundingRect();
const padding = content.style.padding || [0, 0, 0, 0];
return {
x,
y,
// 满足富文本内边距设置
width: width + padding[1] + padding[3],
height: height + padding[0] + padding[2],
};
}

如何新增图元

可以参考这次宇航实现的曲线:src\extension\graphic\CurveLine.ts

注意,新增的图元,需要先注册才能用:

https://echarts.apache.org/zh/api.html#echarts.graphic.registerShape

布局方法里面,获取的矩形框的宽度是负数,这是为什么?

宽度正负表示左右朝向,高度正负表示上下朝向

当前 dvMarker 的布局逻辑是什么样的?

  • 把所有要布局的元素放到一个数组里面

  • 根据设置的边距,确定文本标签可摆放的区域(一个矩形?)

  • 遍历这个数组,摆放元素

  • 如果放不下,则减小元素之间的间隙,然后再次摆放

  • 如果重复 N 次还是放不下,则隐藏权重低的元素

如何调试 ZRender 元素?

https://datav.iwencai.com/components/paradigm-chart/docs/docs/developer-guide/debug/zrender-inspector

经验教训

没弄清楚全流程,盲人摸象

一开始我看了\src\extension\series\dvMarker 目录下的代码,以为这就是全部逻辑了,结果后面才发现,这只是布局相关的代码,而数据处理的逻辑在\src\model\series\dvMarkerSeries\DvMarkerModel.ts 里面(把用户的配置项转换成 echarts 配置项),真正到了 extension 下面时,其实各个子元素的定位数据已经计算好了。这对于我理解整个流程和设计方案造成了困扰。

数据驱动、数据驱动,一定要先弄清楚数据流转的全过程,再去看代码。

定义好数据结构

数据结构如果没定义好,会导致概念多,增加了理解成本,有点乱。

技术储备不足导致的问题:方案质量 VS. DDL

这次本来有个从程序设计上来看更好的方案,就是扩展 ECharts,增加一个类似 markerArea 这种控件。但是这个涉及的概念和时间成本太多了,因此最终还是选择了根据现有的程序设计并不好的 dvMarker 进行修改。

TODO

  • 阅读 MarkArea 相关的源码,比如 MarkAreaModel.ts,参考数据处理的方案

  • 阅读 LabelLayoutHelper 相关的源码,参考碰撞检测方案(shiftLayout 方法)

ECharts 官网的动态排序折线图的案例,就应用了这个碰撞检测,可以用这个 Demo 调试下代码。

特别注意下这几个:
prepareLayoutList 方法:数据预处理
balanceShift 参数:是否在所有标签上平均分配偏移量
squeezeGaps 方法:上下留出边界,美观
delta 参数:可以往外延伸的像素值,大于 0 上边界,小于 0 下边界?

  • 参考 MarkArea 内容,扩展成一个新的 ECharts 控件。

资料

基于 dvMarker 的标签功能:

https://datav.iwencai.com/example.html#/static-page?id=60

Matter.js:一个 2D 物理引擎,可以用来做碰撞检测:

https://github.com/liabru/matter-js