@363045841yyt/klinechart-core 0.9.0-alpha.2 → 0.9.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +9 -8
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/index.d.ts +1 -0
  5. package/dist/controllers/index.d.ts.map +1 -1
  6. package/dist/controllers/index.js.map +1 -1
  7. package/dist/controllers/types.d.ts +5 -0
  8. package/dist/controllers/types.d.ts.map +1 -1
  9. package/dist/data/binance.d.ts +1 -1
  10. package/dist/data/binance.d.ts.map +1 -1
  11. package/dist/data/binance.js +1 -1
  12. package/dist/data/binance.js.map +1 -1
  13. package/dist/data/timeShareBuffer.d.ts +3 -0
  14. package/dist/data/timeShareBuffer.d.ts.map +1 -1
  15. package/dist/data/timeShareBuffer.js +13 -0
  16. package/dist/data/timeShareBuffer.js.map +1 -1
  17. package/dist/engine/chart.d.ts +9 -9
  18. package/dist/engine/chart.d.ts.map +1 -1
  19. package/dist/engine/chart.js +87 -101
  20. package/dist/engine/chart.js.map +1 -1
  21. package/dist/engine/chartTypes.d.ts +5 -0
  22. package/dist/engine/chartTypes.d.ts.map +1 -1
  23. package/dist/engine/controller/interaction.d.ts +4 -2
  24. package/dist/engine/controller/interaction.d.ts.map +1 -1
  25. package/dist/engine/controller/interaction.js +18 -10
  26. package/dist/engine/controller/interaction.js.map +1 -1
  27. package/dist/engine/data/chartDataManager.d.ts +14 -18
  28. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  29. package/dist/engine/data/chartDataManager.js +55 -37
  30. package/dist/engine/data/chartDataManager.js.map +1 -1
  31. package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
  32. package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
  33. package/dist/engine/data/incrementalLoadHint.js +5 -2
  34. package/dist/engine/data/incrementalLoadHint.js.map +1 -1
  35. package/dist/engine/data/scrollCompensator.d.ts +3 -13
  36. package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
  37. package/dist/engine/data/scrollCompensator.js +13 -14
  38. package/dist/engine/data/scrollCompensator.js.map +1 -1
  39. package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
  40. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
  41. package/dist/engine/indicators/chartIndicatorManager.js +37 -37
  42. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
  43. package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
  44. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
  45. package/dist/engine/layout/chartPaneLayout.js +58 -16
  46. package/dist/engine/layout/chartPaneLayout.js.map +1 -1
  47. package/dist/engine/modes/timeShareMath.d.ts +56 -0
  48. package/dist/engine/modes/timeShareMath.d.ts.map +1 -0
  49. package/dist/engine/modes/timeShareMath.js +90 -0
  50. package/dist/engine/modes/timeShareMath.js.map +1 -0
  51. package/dist/engine/modes/timeShareMode.d.ts +5 -0
  52. package/dist/engine/modes/timeShareMode.d.ts.map +1 -1
  53. package/dist/engine/modes/timeShareMode.js +27 -27
  54. package/dist/engine/modes/timeShareMode.js.map +1 -1
  55. package/dist/engine/paneRenderer.d.ts +2 -0
  56. package/dist/engine/paneRenderer.d.ts.map +1 -1
  57. package/dist/engine/paneRenderer.js +15 -5
  58. package/dist/engine/paneRenderer.js.map +1 -1
  59. package/dist/engine/render/chartRenderer.d.ts +28 -22
  60. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  61. package/dist/engine/render/chartRenderer.js +143 -85
  62. package/dist/engine/render/chartRenderer.js.map +1 -1
  63. package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
  64. package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
  65. package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
  66. package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
  67. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
  68. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
  69. package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
  70. package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
  71. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
  72. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
  73. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
  74. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
  75. package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
  76. package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
  77. package/dist/engine/render/layers/yAxisLayer.js +11 -2
  78. package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
  79. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
  80. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  81. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
  82. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  83. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +73 -0
  84. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
  85. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +182 -0
  86. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
  87. package/dist/engine/renderers/leftYAxis.d.ts +15 -2
  88. package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
  89. package/dist/engine/renderers/leftYAxis.js +56 -16
  90. package/dist/engine/renderers/leftYAxis.js.map +1 -1
  91. package/dist/engine/renderers/timeShare.d.ts.map +1 -1
  92. package/dist/engine/renderers/timeShare.js +20 -9
  93. package/dist/engine/renderers/timeShare.js.map +1 -1
  94. package/dist/engine/renderers/yAxis.d.ts +16 -6
  95. package/dist/engine/renderers/yAxis.d.ts.map +1 -1
  96. package/dist/engine/renderers/yAxis.js +49 -16
  97. package/dist/engine/renderers/yAxis.js.map +1 -1
  98. package/dist/engine/state/viewportState.d.ts +4 -1
  99. package/dist/engine/state/viewportState.d.ts.map +1 -1
  100. package/dist/engine/state/viewportState.js +40 -8
  101. package/dist/engine/state/viewportState.js.map +1 -1
  102. package/dist/engine/subPaneManager.d.ts +5 -3
  103. package/dist/engine/subPaneManager.d.ts.map +1 -1
  104. package/dist/engine/subPaneManager.js +3 -2
  105. package/dist/engine/subPaneManager.js.map +1 -1
  106. package/dist/engine/utils/chartZoomController.d.ts +9 -7
  107. package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
  108. package/dist/engine/utils/chartZoomController.js +13 -12
  109. package/dist/engine/utils/chartZoomController.js.map +1 -1
  110. package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
  111. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
  112. package/dist/engine/viewport/chartViewportManager.js +7 -48
  113. package/dist/engine/viewport/chartViewportManager.js.map +1 -1
  114. package/dist/engine/viewport/viewport.d.ts +14 -0
  115. package/dist/engine/viewport/viewport.d.ts.map +1 -1
  116. package/dist/engine/viewport/viewport.js +10 -0
  117. package/dist/engine/viewport/viewport.js.map +1 -1
  118. package/dist/foundation/config/chartSettings.d.ts +0 -5
  119. package/dist/foundation/config/chartSettings.d.ts.map +1 -1
  120. package/dist/foundation/config/chartSettings.js +14 -0
  121. package/dist/foundation/config/chartSettings.js.map +1 -1
  122. package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
  123. package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
  124. package/dist/foundation/plugin/types.d.ts +3 -0
  125. package/dist/foundation/plugin/types.d.ts.map +1 -1
  126. package/dist/foundation/plugin/types.js.map +1 -1
  127. package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
  128. package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
  129. package/dist/foundation/reactivity/frameTransaction.js +17 -2
  130. package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
  131. package/dist/foundation/utils/kLineDraw/axis.d.ts +3 -0
  132. package/dist/foundation/utils/kLineDraw/axis.d.ts.map +1 -1
  133. package/dist/foundation/utils/kLineDraw/axis.js +40 -9
  134. package/dist/foundation/utils/kLineDraw/axis.js.map +1 -1
  135. package/dist/foundation/utils/sessionTimeLabels.d.ts +59 -0
  136. package/dist/foundation/utils/sessionTimeLabels.d.ts.map +1 -0
  137. package/dist/foundation/utils/sessionTimeLabels.js +170 -0
  138. package/dist/foundation/utils/sessionTimeLabels.js.map +1 -0
  139. package/dist/foundation/utils/timeShareAxisLabels.d.ts +27 -0
  140. package/dist/foundation/utils/timeShareAxisLabels.d.ts.map +1 -0
  141. package/dist/foundation/utils/timeShareAxisLabels.js +45 -0
  142. package/dist/foundation/utils/timeShareAxisLabels.js.map +1 -0
  143. package/dist/rendering/render/Renderer.d.ts +58 -74
  144. package/dist/rendering/render/Renderer.d.ts.map +1 -1
  145. package/dist/rendering/render/Renderer.js +8 -18
  146. package/dist/rendering/render/Renderer.js.map +1 -1
  147. package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
  148. package/dist/rendering/render/createWebGPURenderer.js +30 -17
  149. package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
  150. package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
  151. package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
  152. package/dist/rendering/render/physicalLine.d.ts +4 -0
  153. package/dist/rendering/render/physicalLine.d.ts.map +1 -0
  154. package/dist/rendering/render/physicalLine.js +37 -0
  155. package/dist/rendering/render/physicalLine.js.map +1 -0
  156. package/dist/rendering/render/physicalRegion.d.ts +14 -0
  157. package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
  158. package/dist/rendering/render/physicalRegion.js +15 -0
  159. package/dist/rendering/render/physicalRegion.js.map +1 -0
  160. package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
  161. package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
  162. package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
  163. package/dist/rendering/scene/createScene.d.ts.map +1 -1
  164. package/dist/rendering/scene/createScene.js +4 -3
  165. package/dist/rendering/scene/createScene.js.map +1 -1
  166. package/dist/version.d.ts +1 -1
  167. package/dist/version.js +1 -1
  168. package/package.json +1 -1
  169. package/src/__tests__/stateKernel.test.ts +1 -0
  170. package/src/controllers/createChartController.ts +9 -8
  171. package/src/controllers/index.ts +11 -0
  172. package/src/controllers/types.ts +7 -0
  173. package/src/data/__tests__/timeShareBuffer.test.ts +45 -0
  174. package/src/data/binance.ts +1 -1
  175. package/src/data/timeShareBuffer.ts +15 -0
  176. package/src/engine/__tests__/chart.dpr.test.ts +31 -2
  177. package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
  178. package/src/engine/__tests__/subPaneManager.test.ts +5 -3
  179. package/src/engine/chart.ts +105 -115
  180. package/src/engine/chartTypes.ts +5 -0
  181. package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
  182. package/src/engine/controller/interaction.ts +18 -10
  183. package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
  184. package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
  185. package/src/engine/data/chartDataManager.ts +64 -59
  186. package/src/engine/data/incrementalLoadHint.ts +8 -7
  187. package/src/engine/data/scrollCompensator.ts +16 -24
  188. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
  189. package/src/engine/indicators/chartIndicatorManager.ts +62 -62
  190. package/src/engine/layout/chartPaneLayout.ts +73 -21
  191. package/src/engine/modes/__tests__/timeShareMath.test.ts +185 -0
  192. package/src/engine/modes/__tests__/timeShareMode.test.ts +81 -0
  193. package/src/engine/modes/timeShareMath.ts +150 -0
  194. package/src/engine/modes/timeShareMode.ts +35 -28
  195. package/src/engine/paneRenderer.ts +17 -10
  196. package/src/engine/render/chartRenderer.ts +214 -145
  197. package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
  198. package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
  199. package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
  200. package/src/engine/render/layers/yAxisLayer.ts +29 -7
  201. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
  202. package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +290 -0
  203. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
  204. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
  205. package/src/engine/renderers/leftYAxis.ts +60 -18
  206. package/src/engine/renderers/timeShare.ts +25 -9
  207. package/src/engine/renderers/yAxis.ts +57 -22
  208. package/src/engine/state/__tests__/settingsState.test.ts +9 -0
  209. package/src/engine/state/viewportState.ts +44 -8
  210. package/src/engine/subPaneManager.ts +8 -5
  211. package/src/engine/utils/chartZoomController.ts +22 -19
  212. package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
  213. package/src/engine/viewport/chartViewportManager.ts +9 -60
  214. package/src/engine/viewport/viewport.ts +14 -0
  215. package/src/foundation/config/chartSettings.ts +13 -0
  216. package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
  217. package/src/foundation/plugin/types.ts +3 -0
  218. package/src/foundation/reactivity/frameTransaction.ts +20 -6
  219. package/src/foundation/utils/__tests__/sessionTimeLabels.test.ts +91 -0
  220. package/src/foundation/utils/kLineDraw/axis.ts +50 -9
  221. package/src/foundation/utils/sessionTimeLabels.ts +237 -0
  222. package/src/foundation/utils/timeShareAxisLabels.ts +94 -0
  223. package/src/rendering/render/Renderer.ts +69 -85
  224. package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
  225. package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
  226. package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
  227. package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
  228. package/src/rendering/render/createWebGPURenderer.ts +30 -17
  229. package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
  230. package/src/rendering/render/physicalLine.ts +45 -0
  231. package/src/rendering/render/physicalRegion.ts +32 -0
  232. package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
  233. package/src/rendering/scene/createScene.ts +4 -3
  234. package/src/version.ts +1 -1
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Chart — 图表顶层编排器。
3
+ *
4
+ * 职责范围:
5
+ * - 持有 StateKernel(所有配置与状态的唯一数据源)
6
+ * - 管理 Viewport / 布局 / 渲染器生命周期
7
+ * - 提供 scheduleDraw / draw 入口供外部(交互、数据、插件)触发重绘
8
+ * - 协调插件宿主、指标控制器、标注管理器等子系统的初始化与销毁
9
+ *
10
+ * 不是绘制管线的一部分。绘制由 ChartRenderer 完成,Chart 仅负责代理调用并维护 runtimeProjection(批量投影)屏障。
11
+ */
1
12
  import { createAlertController } from '../features/alerts/index'
2
13
  import {
3
14
  createVolumeLookbacks,
@@ -15,10 +26,13 @@ import {
15
26
  import { createLayerFromPlugin } from '../rendering/scene/createLayerFromPlugin'
16
27
  import {
17
28
  computed,
29
+ createSignal,
18
30
  type Computed,
19
31
  type ReadonlySignal,
20
32
  type Signal,
33
+ type WritableSignal,
21
34
  } from '../foundation/reactivity/signal'
35
+ import type { LegendTemplateContext } from './renderers/Indicator/mainIndicatorLegendContext'
22
36
 
23
37
  import { InteractionController, type InteractionSnapshot } from './controller/interaction'
24
38
 
@@ -46,10 +60,9 @@ import { UpdateLevel, type VisibleRange } from './layout/pane'
46
60
  import { KLineMode } from './modes/kLineMode'
47
61
  import { TimeShareMode } from './modes/timeShareMode'
48
62
  import { PaneRenderer } from './paneRenderer'
49
- import { ChartRenderer } from './render/chartRenderer'
63
+ import { ChartRenderer, mergeUpdateLevel } from './render/chartRenderer'
50
64
  import { ChartStateKernel } from './state/chartStateKernel'
51
65
  import { ChartViewportManager } from './viewport/chartViewportManager'
52
- import { getVisibleRange } from './viewport/viewport'
53
66
  import { ChartZoomController } from './utils/chartZoomController'
54
67
  import { getPhysicalKLineConfig } from './utils/klineConfig'
55
68
  import type {
@@ -117,8 +130,13 @@ export class Chart {
117
130
 
118
131
  /** 渲染器 */
119
132
  private renderer: ChartRenderer
133
+ /** runRuntimeProjection 嵌套深度,大于 0 时推迟 scheduleDraw */
120
134
  private runtimeProjectionDepth = 0
121
- private runtimeProjectionDrawPending = false
135
+ /** 被推迟的绘制级别,退出嵌套后统一 flush */
136
+ private pendingProjectionLevel: UpdateLevel | null = null
137
+ /** 主图图例模板上下文(每帧由 mainIndicatorLegend 发布) */
138
+ private readonly _legendTemplateContext: WritableSignal<LegendTemplateContext | null> =
139
+ createSignal<LegendTemplateContext | null>(null)
122
140
 
123
141
  /** 绘图交互会话(锚点/预览/拖拽);工具 id 在 kernel */
124
142
  private drawingSession: DrawingInteractionController | null = null
@@ -133,7 +151,13 @@ export class Chart {
133
151
  zoomLevel: number
134
152
  scaleTypes: Map<string, ScaleType>
135
153
  mainIndicators: Array<{ id: string; params: Record<string, number | boolean | string> }>
136
- subPanes: Array<{ id: string; params: Record<string, unknown> }>
154
+ subPanes: Array<{
155
+ paneId: string
156
+ indicatorId: string
157
+ params: Record<string, unknown>
158
+ }>
159
+ paneRatios: Record<string, number>
160
+ candleEnabled: boolean
137
161
  } | null = null
138
162
 
139
163
  /** 上次预警评估的最新 K 线时间戳(用于去重) */
@@ -275,17 +299,13 @@ export class Chart {
275
299
  defaultPaneMinHeightPx: o.defaultPaneMinHeightPx,
276
300
  }
277
301
  },
278
- getViewport: () => this.viewportManager.getViewport(),
302
+ viewport: this.kernel.viewport,
279
303
  setKnownPaneIds: (ids) => this.rendererPluginManager.setKnownPaneIds(ids),
280
304
  notifyPaneResize: (paneId, pane) =>
281
305
  this.rendererPluginManager.notifyResize(paneId, wrapPaneInfo(pane)),
282
306
  scheduleDraw: (level) => this.scheduleDraw(level),
283
- getPaneRatios: () => this.kernel.pane.readonly.paneRatios.peek(),
284
- getPaneScaleTypes: () => this.kernel.pane.readonly.paneScaleTypes.peek(),
285
- commitLayout: (ratios, specs) => {
286
- this.kernel.pane.actions.commitLayout(ratios, specs)
287
- this.ensurePaneScaleTypesFromSettings()
288
- },
307
+ pane: this.kernel.pane,
308
+ afterCommitLayout: () => this.ensurePaneScaleTypesFromSettings(),
289
309
  })
290
310
 
291
311
  this.alertController = createAlertController()
@@ -300,51 +320,25 @@ export class Chart {
300
320
  kGap: this.kernel.viewport.readonly.kGap(),
301
321
  }
302
322
  },
303
- getEffectiveDpr: () => this.viewportManager.getEffectiveDpr(),
304
- getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
305
- getCachedScrollLeft: () => this.viewportManager.getCachedScrollLeft(),
306
- setScrollLeft: (v) => {
307
- this.viewportManager.setScrollLeft(v)
308
- },
309
323
  getDom: () => this.dom,
310
- getObservedSize: () => this.viewportManager.getObservedSize(),
311
- getViewport: () => this.viewportManager.getViewport(),
312
- getVisibleRange: () => {
313
- const vp = this.viewportManager.computeViewport()
314
- if (!vp) return null
315
- const dataLen = this.dataManager?.getData().length ?? 0
316
- if (dataLen === 0) return null
317
- return getVisibleRange(
318
- vp.scrollLeft,
319
- vp.plotWidth,
320
- this.kernel.zoom.readonly.kWidth(),
321
- this.kernel.viewport.readonly.kGap(),
322
- dataLen,
323
- vp.dpr,
324
- )
325
- },
326
- getLeftLoadBufferWidth: () => this.kernel.viewport.readonly.leftLoadBufferWidth.peek(),
327
- getContentWidth: () => this.kernel.viewport.readonly.contentWidth.peek(),
324
+ viewport: this.kernel.viewport,
325
+ comparison: this.kernel.comparison,
328
326
  scheduleDraw: (level) => this.scheduleDraw(level),
329
327
  resetInteraction: () => this.interaction.reset(),
330
328
  getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
331
329
  isPointerDown: () => this.interaction.isPointerDown(),
332
330
  onTimeShareDataReady: (dataLength) => {
333
- const vp = this.viewportManager.computeViewport()
331
+ const vp = this.getViewport()
334
332
  if (!vp || vp.plotWidth <= 0) return
335
333
  const result = this._activeMode.computeKWidth(dataLength, vp.plotWidth, vp.dpr)
336
334
  if (result) {
337
335
  this.applyRenderState(result.kWidth, result.kGap)
338
336
  const leftBuffer = this.getLeftLoadBufferWidth()
339
- this.viewportManager.setScrollLeft(leftBuffer)
337
+ this.kernel.viewport.actions.scrollTo(leftBuffer)
340
338
  }
341
339
  },
342
340
  onDataProcessed: (data, range) => this.evaluateAlerts(data, range),
343
341
  setSymbols: (symbols) => this.kernel.actions.setSymbols(symbols),
344
- setComparisonLoading: (loading) => this.kernel.comparison.actions.setLoading(loading),
345
- comparisonSpecs$: this.kernel.comparison.readonly.specs,
346
- comparisonColors$: this.kernel.comparison.readonly.colors,
347
- comparisonLoading$: this.kernel.comparison.readonly.loading,
348
342
  },
349
343
  this.kernel.data,
350
344
  this.kernel.dataManager,
@@ -352,22 +346,16 @@ export class Chart {
352
346
 
353
347
  this.zoomController = new ChartZoomController(
354
348
  {
355
- getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
356
- getCurrentDpr: () => this.viewportManager.getEffectiveDpr(),
349
+ viewport: this.kernel.viewport,
350
+ options: this.kernel.options,
351
+ period$: this.kernel.dataManager.readonly.currentPeriod,
357
352
  getClientWidth: () =>
358
- this.viewportManager.getViewport()?.viewWidth ?? this.dom.container?.clientWidth ?? 0,
353
+ this.getViewport()?.viewWidth ?? this.dom.container?.clientWidth ?? 0,
359
354
  getDataLength: () => this.dataManager.getData().length,
360
355
  getPlotWidth: () => this.getLeftLoadBufferWidth(),
361
- setScrollLeft: (v) => {
362
- this.viewportManager.setScrollLeft(v)
363
- },
364
356
  onChange: () => {
365
357
  this.scheduleDraw()
366
358
  },
367
- getMinKWidth: () => this.kernel.options.readonly.options.peek().minKWidth,
368
- getMaxKWidth: () => this.kernel.options.readonly.options.peek().maxKWidth,
369
- getPeriod: () => this.kernel.dataManager.readonly.currentPeriod.peek(),
370
- zoomLevelCount,
371
359
  },
372
360
  this.kernel.zoom,
373
361
  )
@@ -387,8 +375,9 @@ export class Chart {
387
375
  removeRenderer: (name) => this.removeRenderer(name),
388
376
  updateRendererConfig: (name, config) => this.updateRendererConfig(name, config),
389
377
  setRendererEnabled: (name, enabled) => this.setRendererEnabled(name, enabled),
390
- getPaneRatiosSignal: () =>
391
- this.kernel.pane.readonly.paneRatios as ReadonlySignal<Readonly<Record<string, number>>>,
378
+ paneRatios$: this.kernel.pane.readonly.paneRatios as ReadonlySignal<
379
+ Readonly<Record<string, number>>
380
+ >,
392
381
  paneSpecs$: this.kernel.pane.readonly.paneSpecs,
393
382
  projectPaneLayout: (specs, ratios) => {
394
383
  this.layoutManager.projectState(specs, ratios)
@@ -405,24 +394,17 @@ export class Chart {
405
394
  getLayer: (id) => this.renderer?.getScene()?.getLayer(id) ?? null,
406
395
  setLayerVisibility: (id, visible) =>
407
396
  this.renderer?.getScene()?.setLayerVisibility(id, visible),
408
- getRightAxisWidth: () => this.kernel.options.readonly.options.peek().rightAxisWidth,
409
- getPriceLabelWidth: () => this.kernel.options.readonly.options.peek().priceLabelWidth ?? 60,
410
- getYPaddingPx: () => this.kernel.options.readonly.options.peek().yPaddingPx,
411
- mainIndicators$: this.kernel.indicator.readonly.mainIndicators,
412
- upsertMainIndicator: (id, params) => this.kernel.indicator.actions.upsert(id, params),
413
- removeMainIndicator: (id) => this.kernel.indicator.actions.remove(id),
414
- setMainIndicatorParams: (id, params) => this.kernel.indicator.actions.setParams(id, params),
415
- replaceMainIndicators: (entries) => this.kernel.indicator.actions.replaceAll(entries),
416
- clearMainIndicators: () => this.kernel.indicator.actions.clear(),
417
- subPanes$: this.kernel.subPane.readonly.entries,
418
- createSubPaneState: (paneId, indicatorId, params) =>
419
- this.kernel.actions.createSubPane(paneId, indicatorId, params),
420
- removeSubPaneState: (paneId) => this.kernel.actions.removeSubPane(paneId),
421
- replaceSubPaneState: (paneId, indicatorId, params) =>
422
- this.kernel.actions.replaceSubPane(paneId, indicatorId, params),
423
- updateSubPaneStateParams: (paneId, params) =>
424
- this.kernel.actions.updateSubPaneParams(paneId, params),
425
- clearSubPaneState: () => this.kernel.actions.clearSubPanes(),
397
+ indicator: this.kernel.indicator,
398
+ subPaneOps: {
399
+ entries: this.kernel.subPane.readonly.entries,
400
+ create: (paneId, indicatorId, params) =>
401
+ this.kernel.actions.createSubPane(paneId, indicatorId, params),
402
+ remove: (paneId) => this.kernel.actions.removeSubPane(paneId),
403
+ replace: (paneId, indicatorId, params) =>
404
+ this.kernel.actions.replaceSubPane(paneId, indicatorId, params),
405
+ setParams: (paneId, params) => this.kernel.actions.updateSubPaneParams(paneId, params),
406
+ clear: () => this.kernel.actions.clearSubPanes(),
407
+ },
426
408
  runRendererTransaction: (run) => this.runRuntimeProjection(run),
427
409
  })
428
410
 
@@ -456,10 +438,10 @@ export class Chart {
456
438
  getSceneRenderer: () => this.rendererHost.renderer,
457
439
  getPluginHost: () => this.pluginHost,
458
440
  getRendererPluginManager: () => this.rendererPluginManager,
459
- getTheme: () => this.kernel.effectiveTheme$.peek(),
460
- getCurrentZoomLevel: () => this.kernel.zoom.readonly.zoomLevel.peek(),
461
- getZoomLevelCount: () => this.kernel.options.readonly.options.peek().zoomLevelCount,
462
- getViewportManager: () => this.viewportManager,
441
+ theme$: this.kernel.effectiveTheme$,
442
+ zoom: this.kernel.zoom,
443
+ options: this.kernel.options,
444
+ viewport: this.kernel.viewport,
463
445
  getDataManager: () => this.dataManager,
464
446
  getIndicatorManager: () => this.indicatorManager,
465
447
  getActiveMode: () => this._activeMode,
@@ -468,6 +450,9 @@ export class Chart {
468
450
  drawings$: this.kernel.drawing.readonly.drawings,
469
451
  selectedDrawingId$: this.kernel.drawing.readonly.selectedDrawingId,
470
452
  getOverlay: () => this.drawingSession?.getPaintOverlay() ?? [],
453
+ onLegendContext: (ctx) => {
454
+ this._legendTemplateContext.set(ctx)
455
+ },
471
456
  })
472
457
  this.renderer.registerDrawingPlugins()
473
458
  this.renderer.initCoreRenderers()
@@ -476,7 +461,8 @@ export class Chart {
476
461
  }
477
462
 
478
463
  getViewport(): Viewport | null {
479
- return this.viewportManager.getViewport()
464
+ if (this.kernel.viewport.readonly.viewWidth.peek() === 0) return null
465
+ return this.kernel.viewport.readonly.viewport.peek()
480
466
  }
481
467
 
482
468
  /** 获取当前活跃的模式处理器 */
@@ -490,17 +476,21 @@ export class Chart {
490
476
  const prev = this._activeMode
491
477
 
492
478
  if (mode === this._timeShareMode) {
479
+ const candlePlugin = this.rendererPluginManager.getPlugin('candle')
493
480
  this._savedTimeShareState = {
494
481
  zoomLevel: this.kernel.zoom.readonly.zoomLevel.peek(),
495
482
  scaleTypes: new Map(this.kernel.pane.readonly.paneScaleTypes.peek()),
496
483
  mainIndicators: [],
497
484
  subPanes: [],
485
+ paneRatios: { ...this.kernel.pane.readonly.paneRatios.peek() },
486
+ candleEnabled: candlePlugin?.enabled !== false,
498
487
  }
499
488
  for (const [id, entry] of this.kernel.indicator.readonly.mainIndicators.peek()) {
500
489
  this._savedTimeShareState.mainIndicators.push({ id, params: { ...entry.params } })
501
490
  }
502
491
  this._savedTimeShareState.subPanes = this.indicatorManager.getSubPaneEntries().map((e) => ({
503
- id: e.indicatorId,
492
+ paneId: e.paneId,
493
+ indicatorId: e.indicatorId,
504
494
  params: { ...e.params },
505
495
  }))
506
496
  // 分时强制 percent 进 kernel,再投影(不再由 updatePaneRange 每帧旁路写)
@@ -546,7 +536,7 @@ export class Chart {
546
536
 
547
537
  if (mode === this._timeShareMode) {
548
538
  for (const { id } of this._savedTimeShareState!.mainIndicators) {
549
- if (id !== 'TIMESHARE') {
539
+ if (id !== 'TIMESHARE' && id !== 'timeShare') {
550
540
  this.indicatorManager.disableMainIndicator(id)
551
541
  }
552
542
  }
@@ -555,13 +545,26 @@ export class Chart {
555
545
  const saved = this._savedTimeShareState
556
546
  if (saved) {
557
547
  for (const { id, params } of saved.mainIndicators) {
558
- if (id !== 'TIMESHARE') {
548
+ if (id !== 'TIMESHARE' && id !== 'timeShare') {
559
549
  this.indicatorManager.enableMainIndicator(id, params)
560
550
  }
561
551
  }
562
- for (const { id, params } of saved.subPanes) {
563
- this.indicatorManager.addIndicator(id, 'sub', params)
552
+ for (const { paneId, indicatorId, params } of saved.subPanes) {
553
+ this.indicatorManager.createSubPane(
554
+ paneId,
555
+ indicatorId as SubIndicatorType,
556
+ params as Record<string, number | boolean | string>,
557
+ )
558
+ }
559
+ // 恢复进入分时前的 pane 比例(createSubPane 会重算 3:1 分配)
560
+ if (Object.keys(saved.paneRatios).length > 0) {
561
+ const specs = this.kernel.pane.readonly.paneSpecs.peek().map((pane) => ({
562
+ ...pane,
563
+ ratio: saved.paneRatios[pane.id] ?? pane.ratio,
564
+ }))
565
+ this.kernel.pane.actions.commitLayout(saved.paneRatios, specs)
564
566
  }
567
+ this.setRendererEnabled('candle', saved.candleEnabled)
565
568
  }
566
569
  this._savedTimeShareState = null
567
570
  }
@@ -572,7 +575,7 @@ export class Chart {
572
575
  }
573
576
 
574
577
  getCurrentDpr(): number {
575
- return this.viewportManager.getEffectiveDpr()
578
+ return this.kernel.viewport.readonly.dpr.peek()
576
579
  }
577
580
 
578
581
  /** 获取当前周期 */
@@ -580,25 +583,6 @@ export class Chart {
580
583
  return this.dataManager.currentPeriod
581
584
  }
582
585
 
583
- /** 获取缓存的 scrollLeft(避免读取 DOM 触发强制回流) */
584
- getCachedScrollLeft(): number {
585
- return this.viewportManager.getCachedScrollLeft()
586
- }
587
-
588
- /** 同步程序性 scrollLeft 写入后的缓存,避免等待异步 scroll 事件 */
589
- syncScrollLeft(scrollLeft: number): void {
590
- this.viewportManager.setScrollLeft(scrollLeft)
591
- }
592
-
593
- setScrollLeft(v: number): void {
594
- this.viewportManager.setScrollLeft(v)
595
- }
596
-
597
- /** 获取逻辑 scrollLeft(减去左侧加载缓冲宽度,可为负值) */
598
- getLogicalScrollLeft(): number {
599
- return this.viewportManager.getLogicalScrollLeft()
600
- }
601
-
602
586
  /** 获取插件宿主 */
603
587
  get plugin(): PluginHostImpl {
604
588
  return this.pluginHost
@@ -1211,14 +1195,14 @@ export class Chart {
1211
1195
  resize() {
1212
1196
  if (this._activeMode === this._timeShareMode) {
1213
1197
  const tsData = this.dataManager.getTimeShareData()
1214
- const vp = this.viewportManager.computeViewport()
1198
+ const vp = this.getViewport()
1215
1199
  if (!vp || vp.plotWidth <= 0) return
1216
1200
  if (tsData.length > 0) {
1217
1201
  const result = this._activeMode.computeKWidth(tsData.length, vp.plotWidth, vp.dpr)
1218
1202
  if (result) {
1219
1203
  this.applyRenderState(result.kWidth, result.kGap)
1220
1204
  const leftBuffer = this.getLeftLoadBufferWidth()
1221
- this.viewportManager.setScrollLeft(leftBuffer)
1205
+ this.kernel.viewport.actions.scrollTo(leftBuffer)
1222
1206
  }
1223
1207
  }
1224
1208
  this.renderer.clearCachedFrame()
@@ -1226,7 +1210,7 @@ export class Chart {
1226
1210
  this.scheduleDraw()
1227
1211
  return
1228
1212
  }
1229
- const vp = this.viewportManager.computeViewport()
1213
+ const vp = this.getViewport()
1230
1214
  // 防御性检查:容器尺寸无效时跳过布局
1231
1215
  if (!vp || vp.viewWidth < 10 || vp.viewHeight < 10) {
1232
1216
  return
@@ -1241,22 +1225,27 @@ export class Chart {
1241
1225
  * @param level 更新级别,默认为 All
1242
1226
  */
1243
1227
  scheduleDraw(level: UpdateLevel = UpdateLevel.All): void {
1228
+ // 正在批量安装/卸载 renderer(pane 重排、指标切换、子图增删),等全部完成再 flush
1244
1229
  if (this.runtimeProjectionDepth > 0) {
1245
- this.runtimeProjectionDrawPending = true
1230
+ this.pendingProjectionLevel = this.pendingProjectionLevel
1231
+ ? mergeUpdateLevel(this.pendingProjectionLevel, level)
1232
+ : level
1246
1233
  return
1247
1234
  }
1248
1235
  this.renderer.scheduleDraw(level)
1249
1236
  }
1250
1237
 
1238
+ /** 在 RendererPluginManager 事务内执行 run,推迟期间的所有 scheduleDraw 到最外层统一 flush */
1251
1239
  private runRuntimeProjection(run: () => void): void {
1252
1240
  this.runtimeProjectionDepth++
1253
1241
  try {
1254
1242
  this.rendererPluginManager.transaction(run)
1255
1243
  } finally {
1256
1244
  this.runtimeProjectionDepth--
1257
- if (this.runtimeProjectionDepth === 0 && this.runtimeProjectionDrawPending) {
1258
- this.runtimeProjectionDrawPending = false
1259
- this.renderer?.scheduleDraw(UpdateLevel.All)
1245
+ if (this.runtimeProjectionDepth === 0 && this.pendingProjectionLevel !== null) {
1246
+ const level = this.pendingProjectionLevel
1247
+ this.pendingProjectionLevel = null
1248
+ this.renderer?.scheduleDraw(level)
1260
1249
  }
1261
1250
  }
1262
1251
  }
@@ -1310,10 +1299,6 @@ export class Chart {
1310
1299
  await this.pluginHost.destroy()
1311
1300
  }
1312
1301
 
1313
- private computeViewport(): Viewport | null {
1314
- return this.viewportManager.computeViewport()
1315
- }
1316
-
1317
1302
  // ==================== Facade API (High-level interface for adapters) ====================
1318
1303
 
1319
1304
  /** interactionSnapshot lazy computed for the createChartController facade */
@@ -1329,7 +1314,7 @@ export class Chart {
1329
1314
 
1330
1315
  /** 视口状态信号 */
1331
1316
  get viewport(): ReadonlySignal<ViewportState> {
1332
- return this.viewportManager.viewportSignal
1317
+ return this.kernel.viewport.readonly.viewportState
1333
1318
  }
1334
1319
 
1335
1320
  /** 数据信号 */
@@ -1359,12 +1344,12 @@ export class Chart {
1359
1344
 
1360
1345
  /** 比较商品颜色信号 */
1361
1346
  get comparisonColors(): ReadonlySignal<ReadonlyMap<string, string>> {
1362
- return this.dataManager.comparisonColors
1347
+ return this.kernel.comparison.readonly.colors
1363
1348
  }
1364
1349
 
1365
1350
  /** 比较商品加载信号 */
1366
1351
  get comparisonLoading(): ReadonlySignal<boolean> {
1367
- return this.dataManager.comparisonLoading
1352
+ return this.kernel.comparison.readonly.loading
1368
1353
  }
1369
1354
 
1370
1355
  /** 生效主题信号(settings.theme + systemTheme 推导) */
@@ -1414,6 +1399,11 @@ export class Chart {
1414
1399
  return this._interactionSnapshot
1415
1400
  }
1416
1401
 
1402
+ /** 主图左上角图例模板上下文(null 表示无数据) */
1403
+ get legendTemplateContext(): ReadonlySignal<LegendTemplateContext | null> {
1404
+ return this._legendTemplateContext
1405
+ }
1406
+
1417
1407
  // ---------- Data ----------
1418
1408
 
1419
1409
  setData(data: KLineData[]): void {
@@ -22,7 +22,10 @@ export type PaneRendererDom = {
22
22
  mainCanvas: HTMLCanvasElement
23
23
  overlayCanvas: HTMLCanvasElement
24
24
  yAxisCanvas: HTMLCanvasElement
25
+ /** 轴区动态层(最新价标签、十字线价签),叠在 yAxisCanvas 上 */
26
+ yAxisOverlayCanvas: HTMLCanvasElement
25
27
  leftYAxisCanvas?: HTMLCanvasElement
28
+ leftYAxisOverlayCanvas?: HTMLCanvasElement
26
29
  }
27
30
 
28
31
  export type ChartOptions = {
@@ -58,7 +61,9 @@ export type ViewportState = {
58
61
  plotWidth: number
59
62
  plotHeight: number
60
63
  dpr: number
64
+ /** 可索引可见起点(已 clamp start>=0) */
61
65
  visibleFrom: number
66
+ /** 可索引可见终点(开区间) */
62
67
  visibleTo: number
63
68
  kWidth: number
64
69
  kGap: number
@@ -223,8 +223,22 @@ function createChartStub(args: {
223
223
  dpr: args.dpr,
224
224
  }),
225
225
  getCurrentDpr: () => args.dpr,
226
- getCachedScrollLeft: () => 0,
227
- getLogicalScrollLeft: () => 0,
226
+ kernel: {
227
+ viewport: {
228
+ readonly: {
229
+ scrollLeft: { peek: () => 0 },
230
+ scrollLeftLogical: { peek: () => 0 },
231
+ },
232
+ actions: {
233
+ scrollTo: () => undefined,
234
+ },
235
+ },
236
+ settings: {
237
+ readonly: {
238
+ settings: { peek: () => ({}) },
239
+ },
240
+ },
241
+ },
228
242
  getMarkerManager: () => markerManager,
229
243
  getPaneRenderers: () => paneRenderers,
230
244
  getData: () => data,
@@ -302,6 +316,29 @@ describe('InteractionController DPR consumption', () => {
302
316
  expect(interaction.crosshairPos).toBeNull()
303
317
  expect(interaction.crosshairIndex).toBeNull()
304
318
  })
319
+
320
+ it('indexes bars from sealed frameVisibleRange, not stale viewport.visibleFrom', () => {
321
+ // hit-test 与 positions 必须同帧同源;即使 viewport 快照与 seal range 不一致,也以 seal 为准
322
+ const chart = createChartStub({ dpr: 1, plotWidth: 300, plotHeight: 160 })
323
+ chart.viewport.peek = () => ({
324
+ zoomLevel: 1,
325
+ plotWidth: 300,
326
+ plotHeight: 160,
327
+ dpr: 1,
328
+ visibleFrom: 99,
329
+ visibleTo: 101,
330
+ kWidth: 6,
331
+ kGap: 2,
332
+ })
333
+ const interaction = new InteractionController(chart as never, createMockInteractionState())
334
+ interaction.setKLinePositions([0, 10], { start: 0, end: 2 }, 10)
335
+
336
+ interaction.onPointerMove({ clientX: 5, clientY: 40, isPrimary: true } as PointerEvent)
337
+ interaction.flushPendingHover()
338
+
339
+ expect(interaction.crosshairIndex).toBe(0)
340
+ expect(interaction.crosshairPos).not.toBeNull()
341
+ })
305
342
  })
306
343
 
307
344
  describe('InteractionController pane capability gating', () => {
@@ -89,6 +89,8 @@ export class InteractionController {
89
89
  private framePositions: number[] | null = null
90
90
  private frameCenters: number[] | null = null
91
91
  private frameKWidthPx: number | null = null
92
+ /** 与 framePositions 同代的可见区间(已 clamp start>=0,与 calcKLinePositions 一致) */
93
+ private frameVisibleRange: { start: number; end: number } | null = null
92
94
  private markerState = new MarkerInteractionState()
93
95
  private lastHoverRenderKey = ''
94
96
  private useTooltipAnchorPositioning = false
@@ -211,7 +213,7 @@ export class InteractionController {
211
213
  if (!location) return
212
214
 
213
215
  const { mouseX, mouseY } = location
214
- const scrollLeft = this.chart.getLogicalScrollLeft()
216
+ const scrollLeft = this.chart.kernel.viewport.readonly.scrollLeftLogical.peek()
215
217
 
216
218
  const markerManager = this.chart.getMarkerManager()
217
219
  const worldX = scrollLeft + mouseX
@@ -248,7 +250,7 @@ export class InteractionController {
248
250
  // 触屏始终以 pan 模式开始,长按后才切换为 explore
249
251
  this.dragStartX = e.clientX
250
252
  this.dragStartY = e.clientY
251
- this.scrollStartX = this.chart.getCachedScrollLeft()
253
+ this.scrollStartX = this.chart.kernel.viewport.readonly.scrollLeft.peek()
252
254
  this._cachedMaxScrollLeft = -1
253
255
  const captureContainer = this.chart.getDom().container
254
256
  captureContainer?.setPointerCapture(e.pointerId)
@@ -348,6 +350,7 @@ export class InteractionController {
348
350
  this.framePositions = null
349
351
  this.frameCenters = null
350
352
  this.frameKWidthPx = null
353
+ this.frameVisibleRange = null
351
354
  this.hoverFlushPending = false
352
355
  this.clearHover()
353
356
  if (options.scheduleDraw !== false) {
@@ -419,7 +422,7 @@ export class InteractionController {
419
422
  }
420
423
  const clamped = Math.min(Math.max(0, this.scrollStartX + deltaX), this._cachedMaxScrollLeft)
421
424
  const dpr = this.chart.getCurrentDpr()
422
- this.chart.syncScrollLeft(Math.round(clamped * dpr) / dpr)
425
+ this.chart.kernel.viewport.actions.scrollTo(Math.round(clamped * dpr) / dpr)
423
426
 
424
427
  const deltaY = e.clientY - this.dragStartY
425
428
  this.dragStartY = e.clientY
@@ -467,26 +470,30 @@ export class InteractionController {
467
470
  * ChartRenderer 在 paint 前调用;引用未变则跳过 hover 重算。
468
471
  *
469
472
  * @param positions K 线起始 x 坐标数组
470
- * @param visibleRange 保留兼容签名(hover viewport
473
+ * @param visibleRange positions 同代的可见区间(start 已 clamp 到 >=0
471
474
  * @param kWidthPx K 线宽度(物理像素)
472
475
  * @param centers K 线中心 x 坐标数组
473
476
  */
474
477
  setKLinePositions(
475
478
  positions: number[] | null,
476
- _visibleRange: { start: number; end: number } | null,
479
+ visibleRange: { start: number; end: number } | null,
477
480
  kWidthPx?: number,
478
481
  centers?: number[] | null,
479
482
  ) {
480
483
  const nextWidth = kWidthPx ?? null
481
484
  const nextCenters = centers ?? null
485
+ const nextRange = visibleRange
482
486
  const unchanged =
483
487
  this.framePositions === positions &&
484
488
  this.frameCenters === nextCenters &&
485
- this.frameKWidthPx === nextWidth
489
+ this.frameKWidthPx === nextWidth &&
490
+ this.frameVisibleRange?.start === nextRange?.start &&
491
+ this.frameVisibleRange?.end === nextRange?.end
486
492
 
487
493
  this.framePositions = positions
488
494
  this.frameCenters = nextCenters
489
495
  this.frameKWidthPx = nextWidth
496
+ this.frameVisibleRange = nextRange
490
497
 
491
498
  // 几何变化时标记 hover 待刷新;由 flushPendingHover 与 paint 同帧完成
492
499
  if (!unchanged && this.lastClientPos && !this._state.readonly.isDragging.peek()) {
@@ -726,7 +733,7 @@ if (this.tooltipPositionMode === 'adaptive') {
726
733
  return null
727
734
  }
728
735
 
729
- const scrollLeft = this.chart.getLogicalScrollLeft()
736
+ const scrollLeft = this.chart.kernel.viewport.readonly.scrollLeftLogical.peek()
730
737
  const dpr = this.chart.getCurrentDpr()
731
738
 
732
739
  return {
@@ -785,11 +792,11 @@ if (this.tooltipPositionMode === 'adaptive') {
785
792
  private findNearestBar(ctx: HoverContext): NearestBar | null {
786
793
  const kLinePositions = this.framePositions
787
794
  const kWidthPx = this.frameKWidthPx
788
- const vp = this.chart.viewport.peek()
789
- const visibleRange = vp ? { start: vp.visibleFrom, end: vp.visibleTo } : null
795
+ // framePositions 同代的 seal range(viewport 已 clamp,此处仍用 seal 保证同帧一致)
796
+ const visibleRange = this.frameVisibleRange
790
797
  if (!kLinePositions || !visibleRange || !kWidthPx) return null
791
798
 
792
- const { worldX, mouseY, dpr, scrollLeft, plotWidth } = ctx
799
+ const { worldX, dpr, scrollLeft, plotWidth } = ctx
793
800
  const kWidthLogical = kWidthPx / dpr
794
801
  const positions = kLinePositions
795
802
 
@@ -993,6 +1000,7 @@ if (this.tooltipPositionMode === 'adaptive') {
993
1000
  this.framePositions = null
994
1001
  this.frameCenters = null
995
1002
  this.frameKWidthPx = null
1003
+ this.frameVisibleRange = null
996
1004
  this.lastHoverRenderKey = ''
997
1005
  }
998
1006