技术拆解-大事标记折线图
TODO
mdc抽象
基于该项目的技术方案,反向抽象出各个需求点的解决方案和代码,形成为mdc文件
- 数据处理
- 布局算法(碰撞处理)
- 坐标转换
- CustomSeries
- 文本处理
- 交互处理
- 性能优化
- 其他
基本信息
该功能涉及了2个库的修改:
- SD主要是修改了dvMarker的内容:StandardChart的源码
- VISALL做了数据处理和配置处理:VISALL的源码
VISALL部分
组件概述
markerLine(大事标记折线图)是一个复合型数据可视化组件,结合了折线图、散点图和自定义标记图元,用于展示时间序列数据及其关键事件标记。
整体架构分析
组件结构概览
// 复合图表组件包含:
// - 主折线图(y轴数据)
// - 散点图(标记点,使用独立Y轴)
// - 自定义标记图元(dvMarker,显示事件详情)
数据流处理
-
输入数据 → 去重排序 → 转换处理 → 分组编码 → 渲染输出
-
支持的编码字段:
x(时间轴)、y(数值)、y2(事件类型)、subTitle、desc、link
核心技术特性分析
1. ECharts 多系列组合技巧
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. 数据预处理和转换策略
// 核心数据处理流程
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轴范围计算
// 根据图表高度动态计算散点可用的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. 智能文本处理和截断策略
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. 轴指针联动高亮机制
// 核心交互控制函数
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 模式的状态管理
// 高亮状态存储
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. 自定义图例交互逻辑
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. 数据结构优化
// 使用 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. 事件防抖和状态缓存
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. 内存管理
// 数据浅拷贝,避免意外修改原始数据
const originData = data.values.slice();
const transformData = util.dataTransform.transform(data.values.slice(), [/*...*/]);
布局计算策略
1. 响应式网格计算
// 精确的空间计算
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. 自适应分割线数量
const dvSplitLineNumber = hook.getSplitLineNumber(
gridHeight - grid.top - grid.bottom
);
优势:
-
根据实际绘图区域高度计算合适的分割线数量
-
避免分割线过密或过疏的视觉问题
样式和主题系统
1. 富文本样式配置
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. 主题色彩系统
// 颜色计算和混合
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. 建议的重构方向
// 职责分离的函数设计
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. 文本处理技巧
-
智能截断:多维度的文本长度控制
-
富文本渲染:复杂样式的文本显示方案
-
国际化支持:灵活的文本处理框架
实际应用场景
这个组件特别适合以下场景:
-
金融时间序列分析:股价走势 + 重大事件标记
-
项目进度跟踪:进度曲线 + 里程碑事件
-
系统监控告警:性能指标 + 异常事件标记
-
用户行为分析:访问量趋势 + 运营活动标记
通过深入理解这个组件的实现,可以掌握复杂数据可视化组件的设计模式和最佳实践,为开发类似的高级图表组件提供参考。
StandardChart部分(dvMarker)
概述
DVMarker 是一个基于 ECharts 自定义系列(Custom Series) 的扩展组件,提供了多种标记类型(Flag、CurvedPointer、Pin)的智能布局和渲染功能。本文档详细分析了其实现逻辑、技术特性和程序设计技巧。
整体架构设计
DVMarker 采用分层架构设计,包含以下核心层次:
用户API层 (DvMarkerModel)
↓
数据处理层 (数据转换与预处理)
↓
渲染层 (Custom Series renderItem)
↓
布局调整层 (Extension 插件)
架构特点
- 数据层:负责用户配置到 ECharts 配置的转换
- 渲染层:基于 Custom Series 的动态图形生成
- 布局层:智能防重叠算法和位置调整
- 扩展层:生命周期钩子和自定义图形注册
核心 ECharts 技术特性运用
1. Custom Series 的巧妙运用
// 核心:通过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. 扩展图形元素注册
// 注册自定义曲线图形
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. 生命周期钩子的运用
// 注册更新生命周期钩子
registers.registerUpdateLifecycle('series:afterupdate', (_, chart) => {
adjustFlagPosition(chart);
adjustCurvedPointerPosition(chart);
});
技巧点:
- 利用
series:afterupdate生命周期进行后处理 - 在图表更新后自动调整标记位置,防止重叠
- 实现了非侵入式的布局优化
程序设计技巧与模式
1. 策略模式的运用
// 图形渲染策略
const graphicHandle = {
flag: flagRenderer,
curvedPointer: curvedPointerRenderer,
pin: pinRenderer
};
// 动态选择渲染策略
return graphicHandle[markerType](data, this._series, api, params, dataIndex);
优势:
- 易于扩展新的标记类型
- 代码结构清晰,职责分离
- 支持运行时策略选择
2. 工厂模式 + 建造者模式
// 构建复杂的图形元素结构
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. 函数式编程技巧
// 使用闭包和函数式方法处理布局
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. 区间分割算法
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和水平位置排序 - 区间分割:将可用空间分割成不重叠的区间
- 智能放置:根据排序优先级依次放置,找不到空间就隐藏
// 根据优先级从大到小排序标记,优先级相同则按照水平位置从小到大排序
list.sort((a, b) => {
if (a.weight === b.weight) {
return xx(a)[0] - xx(b)[0];
}
return b.weight - a.weight;
});
核心特性:
- 高优先级标记优先显示
- 水平位置作为次要排序条件
- 支持自定义布局处理器
2. 自适应定位算法
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. 圆角自适应算法
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. 图形元素复用
// 通过 util.merge() 深度合并样式,避免重复创建对象
util.merge(circle, circleStyle);
util.merge(line, lineStyle);
util.merge(rect, elRectStyle);
util.merge(text, elTextStyle, true);
2. 延迟计算和缓存
// 只有在需要时才计算文本包围盒
if (typeof rWidth === 'undefined' || typeof rectHeight === 'undefined') {
const { width: tWidth, height: tHeight } = getTextRect(textStyle);
rWidth = tWidth;
rectHeight = tHeight;
}
3. 批量处理
// 按网格分组批量处理标记
gridFlagMarkerMap.forEach(({ flagList, needHideOverlap, customLayoutHandler }, gridModel) => {
const gridRect = gridModel.getRect();
shiftFlagLayoutOnY(flagList, gridRect.y, gridRect.y + gridRect.height, needHideOverlap, customLayoutHandler);
});
扩展机制设计
1. 自定义布局处理器
// 支持用户自定义布局算法
const flagLayoutHandler = markModel.get('flagLayoutHandler');
if (customLayout) {
customLayout(list, topBound, bottomBound, handlers, !needHideOverlap);
} else {
shiftLayout(list, topBound, bottomBound, handlers, !needHideOverlap);
}
扩展性:
- 用户可以提供自定义布局算法
- 保持向后兼容的默认实现
- 支持复杂场景的个性化需求
2. 流水线动画支持
// 根据数据索引设置延迟动画,形成流水线效果
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 基础上构建复杂的自定义可视化组件,是学习图表库扩展开发的绝佳案例。通过深入理解其设计思路和实现技巧,可以掌握构建高质量可视化组件的核心能力。