交互状态
InteractionController 是图表的交互中枢,将 DOM 事件转化为业务操作。其内部状态分为以下几类:
一、拖拽状态
| 状态 | 类型 | 用途 |
|---|---|---|
isDragging | boolean | 标记是否正在拖拽,决定 mousemove 时执行平移还是 hover 逻辑 |
dragStartX | number | 拖拽起点 X,用于计算水平平移距离 |
dragStartY | number | 拖拽起点 Y,用于计算垂直平移增量 |
scrollStartX | number | 拖拽起始的 scrollLeft,平移时以此为基准加减偏移 |
activePaneIdOnDrag | string | null | 拖拽发生时鼠标所在面板,垂直拖拽时只平移该面板的价格轴 |
二、触摸状态
| 状态 | 类型 | 用途 |
|---|---|---|
isTouchSession | boolean | 标记当前为触摸会话,阻止触摸触发的模拟 mouse 事件干扰鼠标处理器 |
三、十字线与悬浮状态
| 状态 | 类型 | 用途 |
|---|---|---|
crosshairPos | {x,y} | null | 十字线绘制坐标(像素),传给渲染层画十字光标 |
crosshairIndex | number | null | 十字线指向的 K 线在数据中的绝对索引,用于显示价格、时间等 |
hoveredIndex | number | null | 鼠标实际命中 candle 的 K 线索引,用于显示 tooltip |
activePaneId | string | null | 当前鼠标所在面板 ID,十字线只显示在该面板**(@Todo:疑似无用状态)** |
tooltipPos | {x,y} | tooltip 最终显示位置,经过防溢出计算 |
tooltipSize | {width,height} | tooltip 尺寸,用于防溢出定位 |
四、标记(Marker)状态
| 状态 | 类型 | 用途 |
|---|---|---|
hoveredMarkerId | string | null | 当前悬浮的 marker ID,用于高亮渲染 |
hoveredMarkerData | MarkerEntity | null | 悬浮 marker 的完整数据,传给外部回调 |
clickedMarkerId | string | null | 当前点击的 marker ID |
clickedMarkerData | MarkerEntity | null | 点击 marker 的完整数据,传给外部回调 |
hoveredCustomMarker | CustomMarkerEntity | null | 悬浮的自定义标记数据 |
五、帧缓存
| 状态 | 类型 | 用途 |
|---|---|---|
kLinePositions | number[] | null | 当前帧每根 K 线起始 X 坐标,用于二分查找鼠标指向的 K 线 |
visibleRange | {start,end} | null | 当前帧可视 K 线范围,将局部索引转为绝对索引 |
kWidthPx | number | null | K 线物理宽度,用于计算十字线吸附位置 |
六、回调函数
| 状态 | 类型 | 用途 |
|---|---|---|
onMarkerHoverCallback | Function | 量价标记悬浮时通知外部(如显示 tooltip) |
onMarkerClickCallback | Function | 量价标记点击时通知外部 |
onCustomMarkerHoverCallback | Function | 自定义标记悬浮时通知外部 |
onCustomMarkerClickCallback | Function | 自定义标记点击时通知外部 |
状态流转简图
用户操作 → 事件处理器 → 更新状态 → scheduleDraw → 渲染层读取状态绘制
↑ ↓
reset() ←——— updateData() ←——— 外部数据变更数据更新时调用 reset(),将拖拽、触摸、十字线、标记、帧缓存五类状态全部清零,避免基于旧数据的脏状态导致渲染异常。
交互事件
InteractionController 通过以下事件处理器承接 DOM 事件,转化为状态变更和图表操作。
1. 滚轮缩放
| 方法 | 触发 | 逻辑 |
|---|---|---|
onWheel(e) | wheel | 计算鼠标在容器内的 X 坐标 → 清除 hover 状态 → 调用 chart.zoomAt() 以鼠标位置为中心缩放 |
2. 鼠标事件(非触摸)
| 方法 | 触发 | 逻辑 |
|---|---|---|
onMouseDown(e) | mousedown | 触摸会话中忽略;优先做 marker 命中测试,命中则触发 click 回调并返回;未命中则进入拖拽模式,记录起点坐标和 scrollLeft |
onMouseMove(e) | mousemove | 拖拽中 → 水平更新 scrollLeft,垂直调用 translatePrice 平移价格轴;非拖拽 → 调用 updateHover 更新十字线和悬浮状态 |
onMouseUp() | mouseup | 退出拖拽模式 |
onMouseLeave() | mouseleave | 退出拖拽模式 + 清除 hover 状态 + 重绘 |
3. 指针事件(统一触屏和鼠标)
| 方法 | 触发 | 逻辑 |
|---|---|---|
onPointerDown(e) | pointerdown | 只处理主指针;标记 isTouchSession;优先 marker 命中测试,命中则触发 click 回调;未命中则进入拖拽模式 |
onPointerMove(e) | pointermove | 只处理主指针;拖拽中处理水平和垂直平移;非拖拽中调用 updateHoverFromPoint 更新悬浮状态 |
onPointerUp(e) | pointerup | 退出拖拽模式 |
onPointerLeave(e) | pointerleave | 退出拖拽模式 + 清除触摸会话标记 + 清除 hover 状态 + 重绘 |
双事件体系说明:
onPointer*覆盖所有指针设备,onMouse*专门处理鼠标。通过isTouchSession标记防止触屏事件同时触发两套处理器。
4. 滚动事件
| 方法 | 触发 | 逻辑 |
|---|---|---|
onScroll() | scroll | 清空 kLinePositions 和 visibleRange 缓存 → 清除 hover → 请求重绘 |
滚动时 K 线坐标缓存失效,清空后下一帧
draw()会重新计算并注入。
5. Hover 更新流程
updateHover / updateHoverFromPoint 的优先级链:
1. 边界检查(鼠标在绘图区域内?)
↓
2. 量价 marker 命中测试 → 命中则更新 marker hover,跳过十字线
↓
3. 自定义标记命中测试 → 命中则更新 custom marker hover,跳过十字线
↓
4. 二分查找鼠标对应的 K 线索引
↓
5. 确定鼠标所在 pane → 更新 activePaneId
↓
6. 计算十字线吸附位置(吸附到 K 线中心)
↓
7. Candle 命中测试(body / wick)→ 更新 hoveredIndex 和 tooltip 防溢出位置6. 外部回调
| 回调 | 触发时机 | 用途 |
|---|---|---|
onMarkerHoverCallback | marker 悬浮/离开 | 通知外部显示/隐藏 marker tooltip |
onMarkerClickCallback | marker 被点击 | 通知外部处理 marker 点击 |
onCustomMarkerHoverCallback | 自定义标记悬浮/离开 | 通知外部显示/隐藏自定义标记 tooltip |
onCustomMarkerClickCallback | 自定义标记被点击 | 通知外部处理自定义标记点击 |
scheduleDraw 重绘
定位
scheduleDraw 是图表重绘的唯一入口,负责以 RAF 合并多次重绘请求,避免同一帧内重复绘制。
实现
typescript
scheduleDraw() {
if (this.raf != null) cancelAnimationFrame(this.raf) // 取消未执行的旧请求
this.raf = requestAnimationFrame(() => {
this.raf = null
this.draw() // 在下一帧执行真正的绘制
})
}核心机制
RAF 合并:短时间内多次调用 scheduleDraw,只有最后一次生效。
例如用户在拖拽时连续触发 mousemove(每帧多次),每次都会取消上一帧的请求,最终只在下一帧执行一次 draw(),避免无效绘制。
调用来源
| 来源 | 场景 |
|---|---|
| 交互事件 | 缩放、平移、十字线移动、滚动、拖拽 |
| 数据变更 | updateData、updateOptions、updatePaneLayout |
| 外部操作 | 标记更新、副图增删、价格轴平移 |
| 尺寸变化 | resize |
设计要点
- 不直接调用
draw():所有触发方都通过scheduleDraw间接请求,由 RAF 统一调度 - 幂等安全:重复调用不会堆积多个
draw,已排队的请求会被新请求覆盖 - 异步执行:在当前帧的状态变更全部完成后,下一帧才开始绘制,保证状态一致性