/** * 事件标记工具函数 * * 用于在 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 ): 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; };