技术拆解-大事标记折线图

TODO

mdc抽象

基于该项目的技术方案,反向抽象出各个需求点的解决方案和代码,形成为mdc文件

  • 数据处理
  • 布局算法(碰撞处理)
  • 坐标转换
  • CustomSeries
  • 文本处理
  • 交互处理
  • 性能优化
  • 其他

基本信息

Demo

该功能涉及了2个库的修改:

VISALL部分

组件概述

markerLine(大事标记折线图)是一个复合型数据可视化组件,结合了折线图、散点图和自定义标记图元,用于展示时间序列数据及其关键事件标记。

整体架构分析

组件结构概览

1
2
3
4
5
6
7
// 复合图表组件包含:

// - 主折线图(y轴数据)

// - 散点图(标记点,使用独立Y轴)

// - 自定义标记图元(dvMarker,显示事件详情)

数据流处理

  1. 输入数据 → 去重排序 → 转换处理 → 分组编码 → 渲染输出

  2. 支持的编码字段:x(时间轴)、y(数值)、y2(事件类型)、subTitledesclink

核心技术特性分析

1. ECharts 多系列组合技巧

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
series: [

// 主折线图

{

type: 'line',

name: y,

data: yAxisData,

yAxisIndex: 0 // 使用主Y轴

},

// 散点图系列(动态生成)

{

type: 'scatter',

yAxisIndex: 1, // 使用辅助Y轴

symbolSize: scatterSize

},

// 自定义标记系列

{

type: 'dvMarker',

dvMarkType: 'flag'

}

]

技巧要点:

  • 使用不同的 yAxisIndex 实现双Y轴布局

  • 散点图和折线图共享X轴,但使用独立的Y轴系统

  • 通过 dvMarker 自定义图元实现复杂的标记展示

2. 数据预处理和转换策略

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
// 核心数据处理流程

const transformData = util.dataTransform.transform(data.values.slice(), [

{ type: 'sort', field: x, order: 'ascending' }, // 按时间排序

{ type: 'deDuplication', field: x } // X轴去重

]);



// 标记数据的优先级计算算法

let currPriority = 0;

let lastX = null;



originData.forEach((item, originIndex) => {

if (lastX !== item[x]) {

// 新的X轴位置,重置优先级

currPriority = 0;

lastX = item[x];

} else {

// 同一X轴位置,优先级递增

currPriority += 1;

}

// 记录每个数据点的堆叠位置

item[priorityAttr] = currPriority;

item[scatterUID] = `${item[x]}${currPriority}`;

});

设计思路:

  • 同一X轴位置可能有多个标记点,通过 currPriority 实现垂直堆叠

  • 使用 Map 结构记录每个X轴位置的最顶层标记,用于交互高亮

  • 为每个标记点生成唯一ID,便于后续事件处理

3. 动态Y轴范围计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根据图表高度动态计算散点可用的Y轴刻度数

const scatterSize = 10; // 散点大小

const scatterSpace = 2; // 散点间距

const gridHeight = chartRect.height - heightOfSpaceOutsideGrid;



const y2AxisData = Array.from(

{ length: Math.floor(gridHeight / (scatterSize + scatterSpace)) },

(_, index) => index

);

巧妙设计:

  • 不使用固定Y轴范围,而是根据容器高度和散点大小动态计算

  • 确保标记点在可视范围内合理分布

  • 自适应不同屏幕尺寸和容器大小

4. 智能文本处理和截断策略

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
function processString(inputStr: string, maxCharNum: number, maxLineNum: number): string {

// 按换行符分割文本

const segments = inputStr.split('\n');

const processedSegments = [];

segments.forEach(segment => {

// 超长文本智能截断

if (segment.length > maxCharNum) {

const numSubSegments = Math.ceil(segment.length / maxCharNum);

for (let i = 0; i < numSubSegments; i++) {

const subSegment = segment.substring(i * maxCharNum, (i + 1) * maxCharNum);

processedSegments.push(subSegment);

}

} else {

processedSegments.push(segment);

}

});

// 行数限制和省略号处理

if (processedSegments.length > maxLineNum) {

processedSegments.splice(maxLineNum);

processedSegments[maxLineNum - 1] =

`${processedSegments[maxLineNum - 1].substring(0,

processedSegments[maxLineNum - 1].length - 3)}...`;

}

return processedSegments.join('\n');

}

实用技巧:

  • 多维度文本控制:字符数限制 + 行数限制

  • 优雅降级:超出部分用省略号表示

  • 保持原有换行格式的同时进行智能截断

高级交互实现

1. 轴指针联动高亮机制

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
// 核心交互控制函数

const toggleTo = (

event: 'highlight' | 'downplay',

seriesIndex: number | string | null,

dataIdx: number | string | null

) => {

if (dataIdx == null || seriesIndex == null) return;

if (typeof seriesIndex === 'string') {

ecIns.dispatchAction({

type: event,

seriesName: seriesIndex, // 按系列名称定位

name: dataIdx

});

} else {

ecIns.dispatchAction({

type: event,

seriesIndex, // 按系列索引定位

dataIndex: dataIdx

});

}

};



// 轴指针事件监听

ecIns.on('updateaxispointer', e => {

const dataIdx = e.dataIndex;

const topScatterInfo = xAxisToTopScatter.get(dataIdx);

if (dataIdx !== lastDataIndex) {

// 高亮新位置的折线点和散点

toggleTo('highlight', 0, dataIdx);

toggleTo('highlight', topScatterInfo?.seriesName, topScatterInfo?.dataName);

// 取消上一个位置的高亮

toggleTo('downplay', 0, lastDataIndex);

lastDataIndex = dataIdx;

}

});

技术亮点:

  • 使用 updateaxispointer 事件实现鼠标悬停联动

  • 通过 Map 结构快速定位对应的散点数据

  • 统一的高亮/取消高亮控制函数,支持多种定位方式

2. Proxy 模式的状态管理

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
// 高亮状态存储

const highlightStorage = {

lastHighlight: {

topScatterSeriesName: null,

xAxisDataName: null

}

};



// Proxy 代理实现自动状态管理

const handler = {

set(target, property, value) {

const oldValue = target[property];

// 检查状态是否真正发生变化

if (!(value.topScatterSeriesName === oldValue.topScatterSeriesName &&

value.xAxisDataName === oldValue.xAxisDataName)) {

// 自动执行状态切换

toggleTo('downplay', oldValue?.topScatterSeriesName, oldValue?.xAxisDataName);

toggleTo('highlight', value?.topScatterSeriesName, value?.xAxisDataName);

}

target[property] = value;

return true;

}

};



const highlightProxy = new Proxy(highlightStorage, handler);



// 使用示例

highlightProxy.lastHighlight = { topScatterSeriesName, xAxisDataName };

设计模式优势:

  • 自动化状态管理,减少手动调用

  • 避免重复的高亮/取消高亮逻辑

  • 状态变化时自动触发视觉更新

3. 自定义图例交互逻辑

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
legendIns.on('legendClick', e => {

const ecIns = chartIns.getECharts();

const global = legendIns.__config;

const clickedStatus = e.data.selected;

global.data.forEach(item => {

if (!item.disableSelect) {

if (item.name !== e.data.name) {

// 实现类似单选模式:点击一个图例时,其他图例状态取反

const status = clickedStatus ? false : !item.selected;

ecIns.dispatchAction({

type: status ? 'legendSelect' : 'legendUnSelect',

name: item.name

});

item.selected = status;

legendIns.__updateLegendDom(item, item.dom);

} else {

// 被点击的图例始终保持选中状态

ecIns.dispatchAction({

type: 'legendSelect',

name: e.data.name

});

item.selected = true;

}

}

});

});

交互策略:

  • 主折线图图例不可取消选择(disableSelect: true

  • 标记类型图例实现智能切换:点击时其他图例状态取反

  • 提供视觉反馈和DOM更新

性能优化技巧

1. 数据结构优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 Map 提高查找效率 O(1) vs O(n)

const xAxisToTopScatter = new Map<number, {

seriesName: string;

dataName: string;

}>();



// 预计算常量,避免重复计算

const maxCharNum = 14; // 每个标记单行最多字数

const maxLineNum = 3; // 每个标记描述的最大行数

const scatterSize = 10; // 散点大小

const scatterSpace = 2; // 散点间距

const markerTopPadding = 5; // 标记顶部边距

2. 事件防抖和状态缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let lastDataIndex: number | null = null;



ecIns.on('updateaxispointer', e => {

const dataIdx = e.dataIndex;

// 只有索引真正变化时才执行高亮操作

if (dataIdx !== lastDataIndex) {

toggleTo('highlight', 0, dataIdx);

toggleTo('downplay', 0, lastDataIndex);

lastDataIndex = dataIdx;

}

});

3. 内存管理

1
2
3
4
5
// 数据浅拷贝,避免意外修改原始数据

const originData = data.values.slice();

const transformData = util.dataTransform.transform(data.values.slice(), [/*...*/]);

布局计算策略

1. 响应式网格计算

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
// 精确的空间计算

const chartRect = $dom.getBoundingClientRect();

const dataZoomHeight = 16; // 数据缩放组件高度

const gridMargin = 12 + 10 * 2; // 网格边距

const heightOfSpaceOutsideGrid =

dataZoomHeight + 8 + gridMargin + 14 + 8; // 非绘图区域总高度



const gridHeight = chartRect.height - heightOfSpaceOutsideGrid;



// 动态网格配置

const grid = {

...baseGridOption,

height: gridHeight

};

计算逻辑:

  • 从容器总高度中精确减去各组件占用的固定高度

  • 剩余空间作为图表绘制区域

  • 确保在不同容器尺寸下都有合适的绘制空间

2. 自适应分割线数量

1
2
3
4
5
const dvSplitLineNumber = hook.getSplitLineNumber(

gridHeight - grid.top - grid.bottom

);

优势:

  • 根据实际绘图区域高度计算合适的分割线数量

  • 避免分割线过密或过疏的视觉问题

样式和主题系统

1. 富文本样式配置

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
const richTextConfig = {

logo: {

width: 10,

height: 10,

borderRadius: 10,

backgroundColor: color,

lineHeight: 16

},

margin10: {

width: 6,

backgroundColor: 'transparent'

},

title: {

fontSize: 12,

fontWeight: 550,

lineHeight: 16,

fill: markerLineToken?.titleColor ?? '#000'

},

desc: {

fontSize: 12,

lineHeight: 16,

fill: markerLineToken?.descColor ?? 'rgba(84, 94, 113, 1)'

}

};

2. 主题色彩系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 颜色计算和混合

const lineColor = util.style.colorOnBackground(

color,

0.5,

markerLineToken?.markerBackground

);



const fillColor = util.style.colorOnBackground(

color,

0.2,

markerLineToken?.markerBackground

);

潜在问题和改进建议

1. 代码质量问题

从 ESLint 错误分析:

  • 认知复杂度过高:主渲染函数复杂度89,远超建议的15

  • 类型安全问题:多处使用 any 类型和隐式类型

  • 对象变更问题:直接修改函数参数对象

  • 模板字符串嵌套:影响代码可读性

2. 建议的重构方向

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
// 职责分离的函数设计

interface MarkerLineHooks {

processMarkerData: (data: any[]) => ProcessedMarkerData;

calculateLayout: (containerRect: DOMRect) => LayoutConfig;

setupInteractions: (chartInstance: any) => void;

handleLegendEvents: (legendInstance: Legend) => void;

}



// 类型安全的接口定义

interface MarkerDataItem {

value: [string | number, number, string];

name: string;

__name: string | number;

currPriority: number;

title: string;

desc: string;

link: string;

}

3. 性能优化建议

  • 使用 useMemo 或类似机制缓存计算结果

  • 实现虚拟滚动处理大数据集

  • 优化事件监听器的添加和移除

学习要点总结

1. 复合图表设计思路

  • 多系列组合:折线图 + 散点图 + 自定义标记的有机结合

  • 双轴系统:主Y轴显示数值,辅助Y轴控制标记位置

  • 数据映射:同一份数据在不同系列中的巧妙复用

2. 动态布局计算

  • 响应式设计:基于容器尺寸的自适应布局策略

  • 精确计算:考虑所有UI组件占用空间的布局算法

  • 灵活配置:可根据数据量和显示需求调整布局参数

3. 高级交互模式

  • 联动交互:轴指针、折线点、散点的协同高亮

  • 状态管理:Proxy模式实现的自动化状态切换

  • 事件处理:多层级的事件监听和处理机制

4. 性能优化手段

  • 数据结构:Map/Set等高效数据结构的应用

  • 状态缓存:避免重复计算和DOM操作

  • 事件防抖:减少不必要的重绘和重排

5. 文本处理技巧

  • 智能截断:多维度的文本长度控制

  • 富文本渲染:复杂样式的文本显示方案

  • 国际化支持:灵活的文本处理框架

实际应用场景

这个组件特别适合以下场景:

  1. 金融时间序列分析:股价走势 + 重大事件标记

  2. 项目进度跟踪:进度曲线 + 里程碑事件

  3. 系统监控告警:性能指标 + 异常事件标记

  4. 用户行为分析:访问量趋势 + 运营活动标记

通过深入理解这个组件的实现,可以掌握复杂数据可视化组件的设计模式和最佳实践,为开发类似的高级图表组件提供参考。

StandardChart部分(dvMarker)

概述

DVMarker 是一个基于 ECharts 自定义系列(Custom Series) 的扩展组件,提供了多种标记类型(Flag、CurvedPointer、Pin)的智能布局和渲染功能。本文档详细分析了其实现逻辑、技术特性和程序设计技巧。

整体架构设计

DVMarker 采用分层架构设计,包含以下核心层次:

1
2
3
4
5
6
7
用户API层 (DvMarkerModel)

数据处理层 (数据转换与预处理)

渲染层 (Custom Series renderItem)

布局调整层 (Extension 插件)

架构特点

  1. 数据层:负责用户配置到 ECharts 配置的转换
  2. 渲染层:基于 Custom Series 的动态图形生成
  3. 布局层:智能防重叠算法和位置调整
  4. 扩展层:生命周期钩子和自定义图形注册

核心 ECharts 技术特性运用

1. Custom Series 的巧妙运用

1
2
3
4
5
6
7
8
9
10
11
// 核心:通过Custom Series的renderItem动态生成复杂图形
protected _renderItemHandle(): void {
this._series.renderItem = (params: CustomSeriesRenderItemParams, api) => {
const { dataIndex } = params;
const data = this._data[dataIndex];
const { markerType } = data;

// 根据标记类型选择不同的渲染函数
return graphicHandle[markerType](data, this._series, api, params, dataIndex);
};
}

技巧点

  • 利用 Custom Series 的灵活性实现复杂的自定义图形
  • 通过 renderItem 函数动态选择渲染策略
  • 使用 api.coord() 进行坐标系转换
  • 支持多种标记类型的统一管理

2. 扩展图形元素注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 注册自定义曲线图形
const CurveLine = graphic.extendShape({
type: 'curveLine',
shape: {
sx: 0, sy: 0, ex: 0, ey: 0,
clockwise: true,
showArrow: true
},
buildPath(path, shape) {
// 绘制贝塞尔曲线和箭头
// 复杂的路径计算逻辑...
}
});
graphic.registerShape('curveLine', CurveLine);

技巧点

  • 通过 graphic.extendShape 扩展自定义图形
  • 实现了带箭头的贝塞尔曲线
  • 支持顺时针/逆时针方向控制
  • 使用 buildPath 方法自定义路径绘制

3. 生命周期钩子的运用

1
2
3
4
5
// 注册更新生命周期钩子
registers.registerUpdateLifecycle('series:afterupdate', (_, chart) => {
adjustFlagPosition(chart);
adjustCurvedPointerPosition(chart);
});

技巧点

  • 利用 series:afterupdate 生命周期进行后处理
  • 在图表更新后自动调整标记位置,防止重叠
  • 实现了非侵入式的布局优化

程序设计技巧与模式

1. 策略模式的运用

1
2
3
4
5
6
7
8
9
// 图形渲染策略
const graphicHandle = {
flag: flagRenderer,
curvedPointer: curvedPointerRenderer,
pin: pinRenderer
};

// 动态选择渲染策略
return graphicHandle[markerType](data, this._series, api, params, dataIndex);

优势

  • 易于扩展新的标记类型
  • 代码结构清晰,职责分离
  • 支持运行时策略选择

2. 工厂模式 + 建造者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构建复杂的图形元素结构
return {
$mergeChildren: false,
type: 'group',
extra: { dvid: `flag_group_outside_${dataIndex}` },
children: [
circle,
{
type: 'group',
extra: { dvid: `flag_group_middle_${dataIndex}` },
children: [line, insideGroup]
}
]
};

技巧点

  • 使用嵌套 Group 结构组织复杂图形
  • 通过 extra.dvid 为每个图形元素添加唯一标识
  • $mergeChildren: false 控制子元素的合并行为
  • 层次化的元素组织便于后续查找和操作

3. 函数式编程技巧

1
2
3
4
5
6
7
8
9
// 使用闭包和函数式方法处理布局
function useMap<K, V>() {
const map = new Map<K, V>();
const get = (key: K) => map.get(key);
const set = (key: K, value: V) => map.set(key, value);
return [get, set] as const;
}

const [y, setY] = useMap<FlagInfo, number>();

优势

  • 提供类似 React Hooks 的使用体验
  • 封装复杂的状态管理逻辑
  • 支持类型安全的操作

4. 区间分割算法

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
class SegmentSplitter {
private segments: Interval[] = [];

// 智能分割可用空间,避免重叠
addInterval(i: number, j: number): void {
const newSegments: Interval[] = [];
this.segments.forEach(([start, end]) => {
if (!isSegmentIntersect([start, end], [i, j])) {
newSegments.push([start, end]);
} else {
if (start < i) {
newSegments.push([start, i]);
}
if (j < end) {
newSegments.push([j, end]);
}
}
});
this.segments = newSegments;
}

// 根据长度和方向找到合适的位置
getSegmentByLength(start: number, end: number, excludePoint: number, direction: -1 | 1) {
// 智能位置查找算法
}
}

核心特性

  • 动态分割可用空间
  • 支持双向搜索最优位置
  • 避免与关键点重叠

核心算法和规则逻辑

1. 防重叠布局算法

算法流程:

  1. 优先级排序:根据 weight 和水平位置排序
  2. 区间分割:将可用空间分割成不重叠的区间
  3. 智能放置:根据排序优先级依次放置,找不到空间就隐藏
1
2
3
4
5
6
7
// 根据优先级从大到小排序标记,优先级相同则按照水平位置从小到大排序
list.sort((a, b) => {
if (a.weight === b.weight) {
return xx(a)[0] - xx(b)[0];
}
return b.weight - a.weight;
});

核心特性:

  • 高优先级标记优先显示
  • 水平位置作为次要排序条件
  • 支持自定义布局处理器

2. 自适应定位算法

1
2
3
4
5
6
7
8
9
10
11
12
13
const handleFlagPosition = (
rectHeight: number, rWidth: number, point: [number, number],
gridRect: BoundingRect
) => {
// 根据点在网格中的位置自动调整标记朝向
const horizontal = point[0] > gridWidth / 2 + gridX ? -1 : 1;
const vertical = point[1] > gridHeight / 2 + gridY ? 1 : -1;

return {
lineHeight: lHeight * vertical,
rectWidth: rWidth * horizontal
};
};

定位规则

  • 水平方向:点在左半部分向右展开,右半部分向左展开
  • 垂直方向:点在上半部分向下展开,下半部分向上展开
  • 自动避免标记超出可视区域

3. 圆角自适应算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function shiftStyles(flagGroupY: number, lineYStart: number, boundHeight: number, rect) {
// 根据旗杆连接位置智能调整圆角
if (flagGroupY > lineYStart) {
// 连接在上方,上方圆角为0
rectBorderRadius[rectWidth > 0 ? 0 : 1] = 0;
} else if (flagGroupY + Math.abs(boundHeight) > lineYStart) {
// 连接在中间,两侧圆角为0
if (rectWidth > 0) {
rectBorderRadius[0] = rectBorderRadius[3] = 0;
} else {
rectBorderRadius[1] = rectBorderRadius[2] = 0;
}
} else {
// 连接在下方,下方圆角为0
rectBorderRadius[rectWidth > 0 ? 3 : 2] = 0;
}
}

设计理念

  • 连接处圆角为0,营造自然的连接效果
  • 根据旗杆方向调整不同的圆角
  • 提升视觉体验的细节处理

性能优化技巧

1. 图形元素复用

1
2
3
4
5
// 通过 util.merge() 深度合并样式,避免重复创建对象
util.merge(circle, circleStyle);
util.merge(line, lineStyle);
util.merge(rect, elRectStyle);
util.merge(text, elTextStyle, true);

2. 延迟计算和缓存

1
2
3
4
5
6
// 只有在需要时才计算文本包围盒
if (typeof rWidth === 'undefined' || typeof rectHeight === 'undefined') {
const { width: tWidth, height: tHeight } = getTextRect(textStyle);
rWidth = tWidth;
rectHeight = tHeight;
}

3. 批量处理

1
2
3
4
5
// 按网格分组批量处理标记
gridFlagMarkerMap.forEach(({ flagList, needHideOverlap, customLayoutHandler }, gridModel) => {
const gridRect = gridModel.getRect();
shiftFlagLayoutOnY(flagList, gridRect.y, gridRect.y + gridRect.height, needHideOverlap, customLayoutHandler);
});

扩展机制设计

1. 自定义布局处理器

1
2
3
4
5
6
7
// 支持用户自定义布局算法
const flagLayoutHandler = markModel.get('flagLayoutHandler');
if (customLayout) {
customLayout(list, topBound, bottomBound, handlers, !needHideOverlap);
} else {
shiftLayout(list, topBound, bottomBound, handlers, !needHideOverlap);
}

扩展性

  • 用户可以提供自定义布局算法
  • 保持向后兼容的默认实现
  • 支持复杂场景的个性化需求

2. 流水线动画支持

1
2
3
4
5
6
// 根据数据索引设置延迟动画,形成流水线效果
if (dvAnimationConfig.dvStreamAnimation) {
const categoryIndex = coords[0];
const streamDelay = duration * categoryIndex;
dvAnimationConfig.enterAnimation.delay = streamDelay;
}

动画特性

  • 支持流水线式的依次显示动画
  • 基于数据索引计算延迟时间
  • 提供丰富的视觉效果

标记类型详解

1. Flag 标记

  • 组成:圆点 + 旗杆 + 旗帜(矩形 + 文本)
  • 特性:自适应方向、圆角连接、防重叠布局
  • 用途:重要节点标注、数据点说明

2. CurvedPointer 标记

  • 组成:圆点 + 曲线 + 标签框
  • 特性:贝塞尔曲线连接、可选箭头、顺/逆时针
  • 用途:优雅的指向性标注

3. Pin 标记

  • 组成:pin 形状图标
  • 特性:简洁的位置标记
  • 用途:地图标记、位置指示

关键学习点总结

1. 架构设计

  • 分层架构:数据层、渲染层、布局层清晰分离
  • 插件机制:通过生命周期钩子实现非侵入式扩展
  • 策略模式:支持多种标记类型的统一管理

2. 算法应用

  • 区间分割算法:智能空间分配,避免重叠
  • 优先级排序:确保重要信息优先显示
  • 自适应定位:根据位置自动调整显示方向

3. ECharts 深度集成

  • Custom Series:充分利用自定义系列的灵活性
  • 生命周期钩子:在合适的时机进行布局调整
  • 自定义图形:扩展 ECharts 的图形能力
  • 坐标转换:正确处理数据坐标到像素坐标的转换

4. 用户体验优化

  • 自适应定位:智能调整标记位置,避免遮挡
  • 防重叠算法:确保信息清晰可读
  • 流水线动画:提供优雅的视觉效果
  • 细节处理:圆角连接、箭头方向等细节优化

5. 扩展性设计

  • 策略模式:易于添加新的标记类型
  • 自定义布局器:支持特殊场景的个性化需求
  • 配置驱动:通过配置控制行为,降低使用复杂度

技术亮点

  1. 复杂图形的组合管理:通过嵌套 Group 结构管理复杂图形层次
  2. 智能空间分配算法:区间分割 + 优先级排序的高效布局算法
  3. 自适应UI设计:根据位置和空间自动调整显示效果
  4. 性能优化策略:延迟计算、批量处理、对象复用等优化手段
  5. 扩展性架构:支持自定义布局算法和新标记类型的添加

这个组件展现了如何在 ECharts 基础上构建复杂的自定义可视化组件,是学习图表库扩展开发的绝佳案例。通过深入理解其设计思路和实现技巧,可以掌握构建高质量可视化组件的核心能力。