技术拆解-散点图

  • 配置一个Demo,了解数据结构
  • 整体查看代码结构,了解程序设计
  • 把相关的代码文件独立出来,打包,让 GPT 分析调用关系(效果不佳)
  • 让杰哥讲解程序(ROI 最高的事情)
  • 画流程图、架构图
  • CodeReview,输出详细的优化方案
  • 查看设计文档(共享目录)
  • 启动程序,debugger 断点调试执行流程
  • 查看 github 的 commit 日志,了解历史需求和实现
  • 费曼学习法:做一个 PPT,给大家讲解

数据结构

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
[
// 2000年的数据
[
// 第一个元素
{
"id": "id-0",
"name": "name-0",
"value": [17, 65, 1]
},
{
"id": "id-1",
"name": "name-1",
"value": [75, 28, 70]
}
// ...
],
// 2001年的数据
[
{
"id": "id-0",
"name": "name-0",
"value": [58, 80, 26]
}
// ...
]
]

是个二维数组,每个数组对应一个时间周期的数据,按照数组下标和时间进行对应。

为什么要设置 name:在 ECharts 的设计中,只有同名的图形元素才会开启过渡动画,否则默认按照索引。

配置项结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export interface DvScatterOption {
type: 'dvScatter';
dvSymbol?: 'circle';
dvSymbolSize?: [number, number] | number;
dvItemStyle?: ItemStyle[];
dvItemConfig?: DvScatterItemConfig;
dvAnimationConfig?: AnimationConfigs;
dvRValueRange?: [number, number];
labelLayout?: {
dvAutoLayout?: boolean;
};
dvKeepHighlight?: boolean;
dvMultipleSelect?: boolean;
dvNearSearch?: {
enable: boolean;
// 哪些交互方式会触发临近搜索
trigger?: 'mousemove' | 'click' | 'all';
};
dvAutoHide?: {
enable: boolean;
delay?: number;
};
}

初始化代码

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
// 逻辑处理&图像绘制
const standardChart = window.ThsDataVStandardChart;

const { times, dataset } = getData();
console.log(times, dataset);
const START_PLAY_INDEX = dataset.length - 1;

const chartIns = standardChart.init('chart', 'mobile-app-light');

const timelineIns = new ThsDataVTimeline.Timeline('#timeline', {
theme: 'mobile-app-light',
data: times,
config: {
dataIndex: START_PLAY_INDEX,
animation: {
intervalTime: 2000,
},
},
});

function chartPlayByIndex(index) {
chartIns.play({
option: {
animationDurationUpdate: 2000,
animationEasingUpdate: 'linear',
// TODO:为什么每次都要传这个,不能自动根据上次的来么?
xAxis: {
min: -20,
max: 120,
type: 'value',
},
yAxis: {
min: -20,
max: 120,
type: 'value',
},
series: [
{
type: 'dvScatter',
data: dataset[index],
},
],
},
});
}

timelineIns.on('change', (event) => {
chartPlayByIndex(event.index);
});

chartPlayByIndex(START_PLAY_INDEX);

程序流程

helper->setOption->play->model->extension

技术细节

代码分析

散点图-动态

helper

职责:**消化图表的业务(设计师的需求)**:帮业务方处理 play 之前的事情-生成 option+事件交互

执行时机:在图表初始化 Option(即处理 model)之前,由用户手动调用 API 触发(helper 都是给外部用户用的,程序流程不会自动调用)。

待优化:

  • 自适应等,扩展为 ECharts 的功能(现在的方案是每次重新 setOption)
  • 抽取时间轴和图表的交互,现在每个 dynamicHelper 中都有,但是又有细微差别
  • (存疑)历史轨迹,属于纯业务,应该抽出来,放 demo 里面
  • (待定)图表是单周期的,但是现在全部数据都传进去了

dvScatter.ts

提供计算函数,供其他程序调用,或者在 play 之前,由用户自己主动显示调用执行。

getOverlapDistance(内部调用)

containBubble(用户调用)

findNearestPointIndex(临近搜索,内部调用)

dynamicDvScatter

(*)dynamicDvScatterHelper.ts

旨在聚合【时间轴】与【气泡图】组件,处理时间轴与图表的交互,并在此基础上扩展【历史轨迹】【自适应】等功能,用于帮助业务方介绍复杂业务场景下的接入成本。

具体说明参考这个文档

解决 play 的管理问题:做代理,实现图表和 timeline 的联动,这样用户就不用调用 play()方法了(猜测是自动生成每一个周期的历史轨迹配置项)。

就是组件特定的业务功能(设计需求(交互+UI)、潜规则等等),我们帮业务方写掉了。

我们提供默认的一套,用户也可以传入进行自定义,比如这个:

1
2
3
4
5
6
7
8
9
// [API]
// getter&setter
getHistorySeriesPlugin() {
return this._historySeriesPlugin;
}

setHistorySeriesPluginById(id, fn: (...args: unknown[]) => unknown) {
this._historySeriesPlugin[id] = fn;
}
util.ts

工具函数,比如:

getFlatData

getItemGraphicEl(获取单点的 Zrender 实例)

optionDelayHandle

axisRangeHandle

model

职责:加工 Option(多次 setOption+加工 Option)

执行时机:进入 ECharts 的整体流程之前处理,初始化 play 的地方调用,可以搜索这几行代码进行定位:

1
2
3
4
5
6
7
8
this._model.update(option);

export default [
barModelParser,
xAxisModelParser,
dvScatterModelParser,
dvMarkerModelParser,
];

具体说明参考这个文档

待优化:

  • 应该新做一个 extension 下的 series,参考词云的方式

series

dvScatter
graphic

Circle.ts

基础的历史轨迹(线…),图元抽象

很强大的图元,插件,扩展等,详细看下

defaultOption.ts
DvScatterModel.ts

就是加工 Option,自定义系列

应该将自定义系列改为扩展一个 series

_logAxisHandle 还没写完,没用上,待完善

helper.ts
type.ts

extension

职责:重写交互、扩展渲染和功能(我们自己的范式需求都应该放 extension 里面),逻辑集中在 action 中。

执行时机:ECharts 的渲染流程之中处理

series

dvScatter
action.ts

解决比如 ECharts 同时选中一个和 hover 一个时,状态无法共存的问题

主要是 ECharts 的的状态管理的重写:

https://datav.iwencai.com/components/paradigm-chart/docs/apis/series/dvScatter#%E4%BA%A4%E4%BA%92%E9%87%8D%E5%86%99

抽象了通用机制:

1
2
3
4
5
import {
type ActionPayloadApplication,
stateHandle,
getItemsInfo,
} from '../../../util/action';

待优化:

  • ECharts 支持多选,之前写的时候不知道,自己去实现了一遍,这块得优化。
xAxis.ts
yAxis.ts

常见问题

历史轨迹的状态是什么?

切换两种模式。

自适应是什么?

这个属于命名有误,实际是当用户执行一些交互后,我们提供其一个功能,可以将当前的焦点数据居中显示。

遗留问题

气泡图是第一批范式组件,由于当时对 ECharts 的不了解,目前存在以下问题

不应该存在的 model 层

为了扩展气泡的表现,使其可以满足历史轨迹的动效,新增了 model 的中间层
实际上应该转为重写 ECharts 中 scatter 相关的 Model 和 View 文件(类似桑基图)来实现表现形式的扩展
model 文件夹是技术债,初期没有用 API,而是加工 option

冗余的交互重写逻辑

为了满足【多选】、【highlight(hover)和 selected(click)状态共存】这俩个功能而完全重写代理了底层的交互。实际上多选是 ECharts 本来就存在的功能,而 hover 态和 click 态共存则应该通过 zrender 的状态机制来解决。
【注】:ECharts 原本的交互在 A 气泡已选中的情况下 hoverB 气泡。仅 B 气泡高亮,A 气泡丢失选中态样式。

庞杂的 helper

为了实现数据处理(多周期数据取极值,避免气泡超出画布)、临近搜索(ECharts 后来的版本推出了该功能,即在小屏幕上,在气泡周围的一定阈值内就可选中气泡)、与时间轴的联动(尤其是历史轨迹、交互与时间轴的状态)等多项功能,实现了一个统一的 helper 代理层。实际上需要将该 helper 中的功能拆分出来(有些应该直接扩展到 ECharts 中,比如自适应、临近搜索,都应该做成扩展,类似漫游组件/配置项)。

(TODO)频繁 setOption 导致的异步机制问题

比如历史轨迹状态下,返回上一步,会出现灰色小球。

PS:我记得 ECharts 有 API 可以清空异步队列的,找找,这个是关键内容。

还有个,如何获取上一个渲染完成的回调,有个 finish 的 API?