306 lines
7.9 KiB
TypeScript
306 lines
7.9 KiB
TypeScript
/**
|
||
* 事件标记工具函数
|
||
*
|
||
* 用于在 K 线图上创建、管理事件标记(Overlay)
|
||
*/
|
||
|
||
import dayjs from 'dayjs';
|
||
import type { OverlayCreate } from 'klinecharts';
|
||
import type { EventMarker, KLineDataPoint } from '../types';
|
||
import { EVENT_MARKER_CONFIG } from '../config';
|
||
import { findClosestDataPoint } from './dataAdapter';
|
||
import { logger } from '@utils/logger';
|
||
|
||
/**
|
||
* 创建事件标记 Overlay(KLineChart 10.0 格式)
|
||
*
|
||
* @param marker 事件标记配置
|
||
* @param data K线数据(用于定位标记位置)
|
||
* @returns OverlayCreate | null Overlay 配置对象
|
||
*/
|
||
export const createEventMarkerOverlay = (
|
||
marker: EventMarker,
|
||
data: KLineDataPoint[]
|
||
): OverlayCreate | null => {
|
||
try {
|
||
// 查找最接近事件时间的数据点
|
||
const closestPoint = findClosestDataPoint(data, marker.timestamp);
|
||
|
||
if (!closestPoint) {
|
||
logger.warn('eventMarkerUtils', 'createEventMarkerOverlay', '未找到匹配的数据点', {
|
||
markerId: marker.id,
|
||
timestamp: marker.timestamp,
|
||
});
|
||
return null;
|
||
}
|
||
|
||
// 根据位置计算 Y 坐标
|
||
const yValue = calculateMarkerYPosition(closestPoint, marker.position);
|
||
|
||
// 创建 Overlay 配置(KLineChart 10.0 规范)
|
||
const overlay: OverlayCreate = {
|
||
name: 'simpleAnnotation', // 使用内置的简单标注类型
|
||
id: marker.id,
|
||
points: [
|
||
{
|
||
timestamp: closestPoint.timestamp,
|
||
value: yValue,
|
||
},
|
||
],
|
||
styles: {
|
||
point: {
|
||
color: marker.color,
|
||
borderColor: marker.color,
|
||
borderSize: 2,
|
||
radius: EVENT_MARKER_CONFIG.size.point,
|
||
},
|
||
text: {
|
||
color: EVENT_MARKER_CONFIG.text.color,
|
||
size: EVENT_MARKER_CONFIG.text.fontSize,
|
||
family: EVENT_MARKER_CONFIG.text.fontFamily,
|
||
weight: 'bold',
|
||
},
|
||
rect: {
|
||
style: 'fill',
|
||
color: marker.color,
|
||
borderRadius: EVENT_MARKER_CONFIG.text.borderRadius,
|
||
paddingLeft: EVENT_MARKER_CONFIG.text.padding,
|
||
paddingRight: EVENT_MARKER_CONFIG.text.padding,
|
||
paddingTop: EVENT_MARKER_CONFIG.text.padding,
|
||
paddingBottom: EVENT_MARKER_CONFIG.text.padding,
|
||
},
|
||
},
|
||
// 标记文本内容
|
||
extendData: {
|
||
label: marker.label,
|
||
icon: marker.icon,
|
||
},
|
||
};
|
||
|
||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlay', '创建事件标记', {
|
||
markerId: marker.id,
|
||
timestamp: closestPoint.timestamp,
|
||
label: marker.label,
|
||
});
|
||
|
||
return overlay;
|
||
} catch (error) {
|
||
logger.error('eventMarkerUtils', 'createEventMarkerOverlay', error as Error, {
|
||
markerId: marker.id,
|
||
});
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 计算标记的 Y 轴位置
|
||
*
|
||
* @param dataPoint K线数据点
|
||
* @param position 标记位置(top/middle/bottom)
|
||
* @returns number Y轴数值
|
||
*/
|
||
const calculateMarkerYPosition = (
|
||
dataPoint: KLineDataPoint,
|
||
position: 'top' | 'middle' | 'bottom'
|
||
): number => {
|
||
switch (position) {
|
||
case 'top':
|
||
return dataPoint.high * 1.02; // 在最高价上方 2%
|
||
case 'bottom':
|
||
return dataPoint.low * 0.98; // 在最低价下方 2%
|
||
case 'middle':
|
||
default:
|
||
return (dataPoint.high + dataPoint.low) / 2; // 中间位置
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 从事件时间创建标记配置
|
||
*
|
||
* @param eventTime 事件时间字符串(ISO 格式)
|
||
* @param label 标记标签(可选,默认为"事件发生")
|
||
* @param color 标记颜色(可选,使用默认颜色)
|
||
* @returns EventMarker 事件标记配置
|
||
*/
|
||
export const createEventMarkerFromTime = (
|
||
eventTime: string,
|
||
label: string = '事件发生',
|
||
color: string = EVENT_MARKER_CONFIG.defaultColor
|
||
): EventMarker => {
|
||
const timestamp = dayjs(eventTime).valueOf();
|
||
|
||
return {
|
||
id: `event-${timestamp}`,
|
||
timestamp,
|
||
label,
|
||
position: EVENT_MARKER_CONFIG.defaultPosition,
|
||
color,
|
||
icon: EVENT_MARKER_CONFIG.defaultIcon,
|
||
draggable: false,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 批量创建事件标记 Overlays
|
||
*
|
||
* @param markers 事件标记配置数组
|
||
* @param data K线数据
|
||
* @returns OverlayCreate[] Overlay 配置数组
|
||
*/
|
||
export const createEventMarkerOverlays = (
|
||
markers: EventMarker[],
|
||
data: KLineDataPoint[]
|
||
): OverlayCreate[] => {
|
||
if (!markers || markers.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const overlays: OverlayCreate[] = [];
|
||
|
||
markers.forEach((marker) => {
|
||
const overlay = createEventMarkerOverlay(marker, data);
|
||
if (overlay) {
|
||
overlays.push(overlay);
|
||
}
|
||
});
|
||
|
||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlays', '批量创建事件标记', {
|
||
totalMarkers: markers.length,
|
||
createdOverlays: overlays.length,
|
||
});
|
||
|
||
return overlays;
|
||
};
|
||
|
||
/**
|
||
* 移除事件标记
|
||
*
|
||
* @param chart KLineChart 实例
|
||
* @param markerId 标记 ID
|
||
*/
|
||
export const removeEventMarker = (chart: any, markerId: string): void => {
|
||
try {
|
||
chart.removeOverlay(markerId);
|
||
logger.debug('eventMarkerUtils', 'removeEventMarker', '移除事件标记', { markerId });
|
||
} catch (error) {
|
||
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 移除所有事件标记
|
||
*
|
||
* @param chart KLineChart 实例
|
||
*/
|
||
export const removeAllEventMarkers = (chart: any): void => {
|
||
try {
|
||
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
|
||
chart.removeOverlay();
|
||
logger.debug('eventMarkerUtils', 'removeAllEventMarkers', '移除所有事件标记');
|
||
} catch (error) {
|
||
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 更新事件标记
|
||
*
|
||
* @param chart KLineChart 实例
|
||
* @param markerId 标记 ID
|
||
* @param updates 更新内容(部分字段)
|
||
*/
|
||
export const updateEventMarker = (
|
||
chart: any,
|
||
markerId: string,
|
||
updates: Partial<EventMarker>
|
||
): void => {
|
||
try {
|
||
// 先移除旧标记
|
||
removeEventMarker(chart, markerId);
|
||
|
||
// 重新创建标记(KLineChart 10.0 不支持直接更新 overlay)
|
||
// 注意:需要在调用方重新创建并添加 overlay
|
||
|
||
logger.debug('eventMarkerUtils', 'updateEventMarker', '更新事件标记', {
|
||
markerId,
|
||
updates,
|
||
});
|
||
} catch (error) {
|
||
logger.error('eventMarkerUtils', 'updateEventMarker', error as Error, { markerId });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 高亮事件标记(改变样式)
|
||
*
|
||
* @param chart KLineChart 实例
|
||
* @param markerId 标记 ID
|
||
* @param highlight 是否高亮
|
||
*/
|
||
export const highlightEventMarker = (
|
||
chart: any,
|
||
markerId: string,
|
||
highlight: boolean
|
||
): void => {
|
||
try {
|
||
// KLineChart 10.0: 通过 overrideOverlay 修改样式
|
||
chart.overrideOverlay({
|
||
id: markerId,
|
||
styles: {
|
||
point: {
|
||
activeRadius: highlight ? 10 : EVENT_MARKER_CONFIG.size.point,
|
||
activeBorderSize: highlight ? 3 : 2,
|
||
},
|
||
},
|
||
});
|
||
|
||
logger.debug('eventMarkerUtils', 'highlightEventMarker', '高亮事件标记', {
|
||
markerId,
|
||
highlight,
|
||
});
|
||
} catch (error) {
|
||
logger.error('eventMarkerUtils', 'highlightEventMarker', error as Error, { markerId });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化事件标记标签
|
||
*
|
||
* @param eventTitle 事件标题
|
||
* @param maxLength 最大长度(默认 10)
|
||
* @returns string 格式化后的标签
|
||
*/
|
||
export const formatEventMarkerLabel = (eventTitle: string, maxLength: number = 10): string => {
|
||
if (!eventTitle) {
|
||
return '事件';
|
||
}
|
||
|
||
if (eventTitle.length <= maxLength) {
|
||
return eventTitle;
|
||
}
|
||
|
||
return `${eventTitle.substring(0, maxLength)}...`;
|
||
};
|
||
|
||
/**
|
||
* 判断事件时间是否在数据范围内
|
||
*
|
||
* @param eventTime 事件时间戳
|
||
* @param data K线数据
|
||
* @returns boolean 是否在范围内
|
||
*/
|
||
export const isEventTimeInDataRange = (
|
||
eventTime: number,
|
||
data: KLineDataPoint[]
|
||
): boolean => {
|
||
if (!data || data.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
const timestamps = data.map((item) => item.timestamp);
|
||
const minTime = Math.min(...timestamps);
|
||
const maxTime = Math.max(...timestamps);
|
||
|
||
return eventTime >= minTime && eventTime <= maxTime;
|
||
};
|