TODO
mdc抽象
基于该项目的技术方案,反向抽象出各个需求点的解决方案和代码,形成为mdc文件
基本信息
Demo
该功能涉及了2个库的修改:
VISALL部分
组件概述
markerLine(大事标记折线图)是一个复合型数据可视化组件,结合了折线图、散点图和自定义标记图元,用于展示时间序列数据及其关键事件标记。
整体架构分析
组件结构概览
数据流处理
输入数据 → 去重排序 → 转换处理 → 分组编码 → 渲染输出
支持的编码字段:x(时间轴)、y(数值)、y2(事件类型)、subTitle、desc、link
核心技术特性分析
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
},
{
type: 'scatter',
yAxisIndex: 1,
symbolSize: scatterSize
},
{
type: 'dvMarker',
dvMarkType: 'flag'
}
]
|
技巧要点:
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 }
]);
let currPriority = 0;
let lastX = null;
originData.forEach((item, originIndex) => {
if (lastX !== item[x]) {
currPriority = 0;
lastX = item[x];
} else {
currPriority += 1;
}
item[priorityAttr] = currPriority;
item[scatterUID] = `${item[x]}${currPriority}`;
});
|
设计思路:
3. 动态Y轴范围计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
const scatterSize = 10;
const scatterSpace = 2;
const gridHeight = chartRect.height - heightOfSpaceOutsideGrid;
const y2AxisData = Array.from(
{ length: Math.floor(gridHeight / (scatterSize + scatterSpace)) },
(_, index) => index
);
|
巧妙设计:
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;
}
});
|
技术亮点:
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
}
};
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;
}
}
});
});
|
交互策略:
性能优化技巧
1. 数据结构优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
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 错误分析:
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. 复合图表设计思路
2. 动态布局计算
响应式设计:基于容器尺寸的自适应布局策略
精确计算:考虑所有UI组件占用空间的布局算法
灵活配置:可根据数据量和显示需求调整布局参数
3. 高级交互模式
联动交互:轴指针、折线点、散点的协同高亮
状态管理:Proxy模式实现的自动化状态切换
事件处理:多层级的事件监听和处理机制
4. 性能优化手段
数据结构:Map/Set等高效数据结构的应用
状态缓存:避免重复计算和DOM操作
事件防抖:减少不必要的重绘和重排
5. 文本处理技巧
智能截断:多维度的文本长度控制
富文本渲染:复杂样式的文本显示方案
国际化支持:灵活的文本处理框架
实际应用场景
这个组件特别适合以下场景:
金融时间序列分析:股价走势 + 重大事件标记
项目进度跟踪:进度曲线 + 里程碑事件
系统监控告警:性能指标 + 异常事件标记
用户行为分析:访问量趋势 + 运营活动标记
通过深入理解这个组件的实现,可以掌握复杂数据可视化组件的设计模式和最佳实践,为开发类似的高级图表组件提供参考。
StandardChart部分(dvMarker)
概述
DVMarker 是一个基于 ECharts 自定义系列(Custom Series) 的扩展组件,提供了多种标记类型(Flag、CurvedPointer、Pin)的智能布局和渲染功能。本文档详细分析了其实现逻辑、技术特性和程序设计技巧。
整体架构设计
DVMarker 采用分层架构设计,包含以下核心层次:
1 2 3 4 5 6 7
| 用户API层 ↓ 数据处理层 ↓ 渲染层 ↓ 布局调整层
|
架构特点
- 数据层:负责用户配置到 ECharts 配置的转换
- 渲染层:基于 Custom Series 的动态图形生成
- 布局层:智能防重叠算法和位置调整
- 扩展层:生命周期钩子和自定义图形注册
核心 ECharts 技术特性运用
1. Custom Series 的巧妙运用
1 2 3 4 5 6 7 8 9 10 11
| 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. 防重叠布局算法
算法流程:
- 优先级排序:根据
weight 和水平位置排序
- 区间分割:将可用空间分割成不重叠的区间
- 智能放置:根据排序优先级依次放置,找不到空间就隐藏
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) { rectBorderRadius[rectWidth > 0 ? 0 : 1] = 0; } else if (flagGroupY + Math.abs(boundHeight) > lineYStart) { if (rectWidth > 0) { rectBorderRadius[0] = rectBorderRadius[3] = 0; } else { rectBorderRadius[1] = rectBorderRadius[2] = 0; } } else { rectBorderRadius[rectWidth > 0 ? 3 : 2] = 0; } }
|
设计理念:
- 连接处圆角为0,营造自然的连接效果
- 根据旗杆方向调整不同的圆角
- 提升视觉体验的细节处理
性能优化技巧
1. 图形元素复用
1 2 3 4 5
| 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. 扩展性设计
- 策略模式:易于添加新的标记类型
- 自定义布局器:支持特殊场景的个性化需求
- 配置驱动:通过配置控制行为,降低使用复杂度
技术亮点
- 复杂图形的组合管理:通过嵌套 Group 结构管理复杂图形层次
- 智能空间分配算法:区间分割 + 优先级排序的高效布局算法
- 自适应UI设计:根据位置和空间自动调整显示效果
- 性能优化策略:延迟计算、批量处理、对象复用等优化手段
- 扩展性架构:支持自定义布局算法和新标记类型的添加
这个组件展现了如何在 ECharts 基础上构建复杂的自定义可视化组件,是学习图表库扩展开发的绝佳案例。通过深入理解其设计思路和实现技巧,可以掌握构建高质量可视化组件的核心能力。