Files
vf_react/src/components/StockChart/utils/eventMarkerUtils.ts

306 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 事件标记工具函数
*
* 用于在 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';
/**
* 创建事件标记 OverlayKLineChart 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;
};