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

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 (190) 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/engine/chart.d.ts +9 -9
  14. package/dist/engine/chart.d.ts.map +1 -1
  15. package/dist/engine/chart.js +69 -96
  16. package/dist/engine/chart.js.map +1 -1
  17. package/dist/engine/chartTypes.d.ts +5 -0
  18. package/dist/engine/chartTypes.d.ts.map +1 -1
  19. package/dist/engine/controller/interaction.d.ts +4 -2
  20. package/dist/engine/controller/interaction.d.ts.map +1 -1
  21. package/dist/engine/controller/interaction.js +18 -10
  22. package/dist/engine/controller/interaction.js.map +1 -1
  23. package/dist/engine/data/chartDataManager.d.ts +12 -18
  24. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  25. package/dist/engine/data/chartDataManager.js +46 -37
  26. package/dist/engine/data/chartDataManager.js.map +1 -1
  27. package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
  28. package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
  29. package/dist/engine/data/incrementalLoadHint.js +5 -2
  30. package/dist/engine/data/incrementalLoadHint.js.map +1 -1
  31. package/dist/engine/data/scrollCompensator.d.ts +3 -13
  32. package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
  33. package/dist/engine/data/scrollCompensator.js +13 -14
  34. package/dist/engine/data/scrollCompensator.js.map +1 -1
  35. package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
  36. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
  37. package/dist/engine/indicators/chartIndicatorManager.js +37 -37
  38. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
  39. package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
  40. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
  41. package/dist/engine/layout/chartPaneLayout.js +58 -16
  42. package/dist/engine/layout/chartPaneLayout.js.map +1 -1
  43. package/dist/engine/paneRenderer.d.ts +2 -0
  44. package/dist/engine/paneRenderer.d.ts.map +1 -1
  45. package/dist/engine/paneRenderer.js +15 -5
  46. package/dist/engine/paneRenderer.js.map +1 -1
  47. package/dist/engine/render/chartRenderer.d.ts +28 -22
  48. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  49. package/dist/engine/render/chartRenderer.js +117 -69
  50. package/dist/engine/render/chartRenderer.js.map +1 -1
  51. package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
  52. package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
  53. package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
  54. package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
  55. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
  56. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
  57. package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
  58. package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
  59. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
  60. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
  61. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
  62. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
  63. package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
  64. package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
  65. package/dist/engine/render/layers/yAxisLayer.js +11 -2
  66. package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
  67. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
  68. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  69. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
  70. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  71. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +72 -0
  72. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
  73. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +177 -0
  74. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
  75. package/dist/engine/renderers/leftYAxis.d.ts +15 -2
  76. package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
  77. package/dist/engine/renderers/leftYAxis.js +56 -16
  78. package/dist/engine/renderers/leftYAxis.js.map +1 -1
  79. package/dist/engine/renderers/yAxis.d.ts +16 -6
  80. package/dist/engine/renderers/yAxis.d.ts.map +1 -1
  81. package/dist/engine/renderers/yAxis.js +49 -16
  82. package/dist/engine/renderers/yAxis.js.map +1 -1
  83. package/dist/engine/state/viewportState.d.ts +4 -1
  84. package/dist/engine/state/viewportState.d.ts.map +1 -1
  85. package/dist/engine/state/viewportState.js +40 -8
  86. package/dist/engine/state/viewportState.js.map +1 -1
  87. package/dist/engine/subPaneManager.d.ts +5 -3
  88. package/dist/engine/subPaneManager.d.ts.map +1 -1
  89. package/dist/engine/subPaneManager.js +3 -2
  90. package/dist/engine/subPaneManager.js.map +1 -1
  91. package/dist/engine/utils/chartZoomController.d.ts +9 -7
  92. package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
  93. package/dist/engine/utils/chartZoomController.js +13 -12
  94. package/dist/engine/utils/chartZoomController.js.map +1 -1
  95. package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
  96. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
  97. package/dist/engine/viewport/chartViewportManager.js +7 -48
  98. package/dist/engine/viewport/chartViewportManager.js.map +1 -1
  99. package/dist/engine/viewport/viewport.d.ts +14 -0
  100. package/dist/engine/viewport/viewport.d.ts.map +1 -1
  101. package/dist/engine/viewport/viewport.js +10 -0
  102. package/dist/engine/viewport/viewport.js.map +1 -1
  103. package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
  104. package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
  105. package/dist/foundation/plugin/types.d.ts +3 -0
  106. package/dist/foundation/plugin/types.d.ts.map +1 -1
  107. package/dist/foundation/plugin/types.js.map +1 -1
  108. package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
  109. package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
  110. package/dist/foundation/reactivity/frameTransaction.js +17 -2
  111. package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
  112. package/dist/rendering/render/Renderer.d.ts +58 -74
  113. package/dist/rendering/render/Renderer.d.ts.map +1 -1
  114. package/dist/rendering/render/Renderer.js +8 -18
  115. package/dist/rendering/render/Renderer.js.map +1 -1
  116. package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
  117. package/dist/rendering/render/createWebGPURenderer.js +30 -17
  118. package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
  119. package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
  120. package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
  121. package/dist/rendering/render/physicalLine.d.ts +4 -0
  122. package/dist/rendering/render/physicalLine.d.ts.map +1 -0
  123. package/dist/rendering/render/physicalLine.js +37 -0
  124. package/dist/rendering/render/physicalLine.js.map +1 -0
  125. package/dist/rendering/render/physicalRegion.d.ts +14 -0
  126. package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
  127. package/dist/rendering/render/physicalRegion.js +15 -0
  128. package/dist/rendering/render/physicalRegion.js.map +1 -0
  129. package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
  130. package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
  131. package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
  132. package/dist/rendering/scene/createScene.d.ts.map +1 -1
  133. package/dist/rendering/scene/createScene.js +4 -3
  134. package/dist/rendering/scene/createScene.js.map +1 -1
  135. package/dist/version.d.ts +1 -1
  136. package/dist/version.js +1 -1
  137. package/package.json +1 -1
  138. package/src/__tests__/stateKernel.test.ts +1 -0
  139. package/src/controllers/createChartController.ts +9 -8
  140. package/src/controllers/index.ts +11 -0
  141. package/src/controllers/types.ts +7 -0
  142. package/src/data/binance.ts +1 -1
  143. package/src/engine/__tests__/chart.dpr.test.ts +3 -2
  144. package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
  145. package/src/engine/__tests__/subPaneManager.test.ts +5 -3
  146. package/src/engine/chart.ts +76 -109
  147. package/src/engine/chartTypes.ts +5 -0
  148. package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
  149. package/src/engine/controller/interaction.ts +18 -10
  150. package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
  151. package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
  152. package/src/engine/data/chartDataManager.ts +54 -59
  153. package/src/engine/data/incrementalLoadHint.ts +8 -7
  154. package/src/engine/data/scrollCompensator.ts +16 -24
  155. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
  156. package/src/engine/indicators/chartIndicatorManager.ts +62 -62
  157. package/src/engine/layout/chartPaneLayout.ts +73 -21
  158. package/src/engine/paneRenderer.ts +17 -10
  159. package/src/engine/render/chartRenderer.ts +187 -129
  160. package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
  161. package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
  162. package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
  163. package/src/engine/render/layers/yAxisLayer.ts +29 -7
  164. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
  165. package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +282 -0
  166. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
  167. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
  168. package/src/engine/renderers/leftYAxis.ts +60 -18
  169. package/src/engine/renderers/yAxis.ts +57 -22
  170. package/src/engine/state/viewportState.ts +44 -8
  171. package/src/engine/subPaneManager.ts +8 -5
  172. package/src/engine/utils/chartZoomController.ts +22 -19
  173. package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
  174. package/src/engine/viewport/chartViewportManager.ts +9 -60
  175. package/src/engine/viewport/viewport.ts +14 -0
  176. package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
  177. package/src/foundation/plugin/types.ts +3 -0
  178. package/src/foundation/reactivity/frameTransaction.ts +20 -6
  179. package/src/rendering/render/Renderer.ts +69 -85
  180. package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
  181. package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
  182. package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
  183. package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
  184. package/src/rendering/render/createWebGPURenderer.ts +30 -17
  185. package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
  186. package/src/rendering/render/physicalLine.ts +45 -0
  187. package/src/rendering/render/physicalRegion.ts +32 -0
  188. package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
  189. package/src/rendering/scene/createScene.ts +4 -3
  190. package/src/version.ts +1 -1
@@ -7,7 +7,7 @@ import {
7
7
  } from '../../foundation/reactivity/signal'
8
8
  import type { Viewport, ViewportState } from '../chartTypes'
9
9
  import type { VisibleRange } from '../layout/pane'
10
- import { getVisibleRange } from '../viewport/viewport'
10
+ import { clampVisibleRange, getVisibleRange } from '../viewport/viewport'
11
11
  import {
12
12
  computeLeftLoadBufferWidth as pureLeftBuffer,
13
13
  computeContentWidth as pureContentWidth,
@@ -197,8 +197,8 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
197
197
  return vp
198
198
  })
199
199
 
200
- let _cachedVisibleRange: VisibleRange | null = null
201
- const cachedVisibleRange = computed<VisibleRange>(() => {
200
+ let _cachedRawVisibleRange: VisibleRange | null = null
201
+ const cachedRawVisibleRange = computed<VisibleRange>(() => {
202
202
  const vp = cachedViewport()
203
203
  const opts = signalDeps.options$()
204
204
  const vr = getVisibleRange(
@@ -209,13 +209,32 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
209
209
  signalDeps.dataLength$(),
210
210
  vp.dpr,
211
211
  )
212
- if (_cachedVisibleRange && _cachedVisibleRange.start === vr.start && _cachedVisibleRange.end === vr.end) {
213
- return _cachedVisibleRange
212
+ if (
213
+ _cachedRawVisibleRange &&
214
+ _cachedRawVisibleRange.start === vr.start &&
215
+ _cachedRawVisibleRange.end === vr.end
216
+ ) {
217
+ return _cachedRawVisibleRange
214
218
  }
215
- _cachedVisibleRange = vr
219
+ _cachedRawVisibleRange = vr
216
220
  return vr
217
221
  })
218
222
 
223
+ // 可索引可见区间(start>=0)— 绘制 / hit-test / 指标 / ViewportState 的 SSOT
224
+ let _cachedVisibleRange: VisibleRange | null = null
225
+ const cachedVisibleRange = computed<VisibleRange>(() => {
226
+ const clamped = clampVisibleRange(cachedRawVisibleRange())
227
+ if (
228
+ _cachedVisibleRange &&
229
+ _cachedVisibleRange.start === clamped.start &&
230
+ _cachedVisibleRange.end === clamped.end
231
+ ) {
232
+ return _cachedVisibleRange
233
+ }
234
+ _cachedVisibleRange = clamped
235
+ return clamped
236
+ })
237
+
219
238
  let _cachedViewportState: ViewportState | null = null
220
239
  const cachedViewportState = computed<ViewportState>(() => {
221
240
  const vp = cachedViewport()
@@ -355,6 +374,9 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
355
374
  scrollLeftLogical,
356
375
  kGap,
357
376
  viewport: cachedViewport,
377
+ /** raw:含左右扩窗,start 可能为 -1(增量加载左缘检测) */
378
+ rawVisibleRange: cachedRawVisibleRange,
379
+ /** clamped:start>=0,绘制 / hit-test / 指标 / ViewportState 的 SSOT */
358
380
  visibleRange: cachedVisibleRange,
359
381
  viewportState: cachedViewportState,
360
382
  }
@@ -414,7 +436,7 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
414
436
  /**
415
437
  * 初始化视口状态。
416
438
  *
417
- * @remarks 从 DOM 容器读取当前 scrollLeft,并挂载 canvas 尺寸同步 effect。
439
+ * @remarks 从 DOM 容器读取首帧尺寸与 scrollLeft,并挂载 canvas 尺寸同步 effect。
418
440
  * 重复调用安全(仅首次生效)。
419
441
  */
420
442
  init() {
@@ -423,7 +445,21 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
423
445
  const container = _getDom().container
424
446
  if (!container) return
425
447
  signals.initialized.set(true)
426
- setRequestedScrollLeft(container.scrollLeft)
448
+ // 首帧尺寸写入 kernel,避免 paint 路径读 DOM fallback
449
+ const w = Math.max(0, Math.round(container.clientWidth))
450
+ const h = Math.max(0, Math.round(container.clientHeight))
451
+ batch(() => {
452
+ if (w > 0 && h > 0) {
453
+ signals.viewWidth.set(w)
454
+ signals.viewHeight.set(h)
455
+ }
456
+ // 与 resize 一致:首帧 scrollLeft 为 0 时落到 viewWidth
457
+ if (container.scrollLeft === 0 && w > 0) {
458
+ signals.requestedScrollLeft.set(w)
459
+ } else {
460
+ setRequestedScrollLeft(container.scrollLeft)
461
+ }
462
+ })
427
463
  setupCanvasSync()
428
464
  },
429
465
  },
@@ -44,9 +44,11 @@ export interface SubPaneContext {
44
44
  removeRenderer: (name: string) => void
45
45
  setRendererEnabled: (name: string, enabled: boolean) => void
46
46
  updateRendererConfig: (name: string, config: Record<string, unknown>) => void
47
- getRightAxisWidth: () => number
48
- getPriceLabelWidth: () => number
49
- getYPaddingPx: () => number
47
+ getOption: () => {
48
+ rightAxisWidth: number
49
+ priceLabelWidth?: number
50
+ yPaddingPx: number
51
+ }
50
52
  getCrosshairPos: () => { x: number; y: number } | null
51
53
  getCrosshairPrice: () => number | null
52
54
  getActivePaneId: () => string | null
@@ -203,7 +205,8 @@ export class SubPaneManager {
203
205
  return
204
206
  }
205
207
  const definition = ctx.getIndicatorScheduler().getIndicatorMetadata(entry.indicatorId)
206
- const axisWidth = ctx.getRightAxisWidth() + ctx.getPriceLabelWidth()
208
+ const opt = ctx.getOption()
209
+ const axisWidth = opt.rightAxisWidth + (opt.priceLabelWidth ?? 60)
207
210
  const getCrosshair = () => {
208
211
  const pos = ctx.getCrosshairPos()
209
212
  const price = ctx.getCrosshairPrice()
@@ -213,7 +216,7 @@ export class SubPaneManager {
213
216
  const options = {
214
217
  axisWidth,
215
218
  paneId: entry.paneId,
216
- yPaddingPx: ctx.getYPaddingPx(),
219
+ yPaddingPx: opt.yPaddingPx,
217
220
  getCrosshair,
218
221
  }
219
222
  const plugin = definition?.scaleRendererFactory
@@ -1,18 +1,20 @@
1
1
  import { computeZoom, deriveKGap } from './zoom'
2
+ import type { ReadonlySignal } from '../../foundation/reactivity/signal'
3
+ import type { OptionsStateModule } from '../state/optionsState'
2
4
  import type { ZoomStateModule } from '../state/zoomState'
5
+ import type { ViewportStateModule } from '../state/viewportState'
3
6
 
4
7
  export interface ZoomDependencies {
5
- getLogicalScrollLeft: () => number
6
- getCurrentDpr: () => number
8
+ /** scroll / dpr 几何,读写走 kernel.viewport */
9
+ viewport: ViewportStateModule
10
+ /** min/max kWidth / zoomLevelCount SSOT */
11
+ options: OptionsStateModule
12
+ /** 当前周期(kGap 推导) */
13
+ period$: ReadonlySignal<string>
7
14
  getClientWidth: () => number
8
15
  getDataLength: () => number
9
16
  getPlotWidth: () => number
10
- setScrollLeft: (v: number) => void
11
17
  onChange?: () => void
12
- getMinKWidth: () => number
13
- getMaxKWidth: () => number
14
- getPeriod: () => string
15
- zoomLevelCount: number
16
18
  }
17
19
 
18
20
  /**
@@ -39,17 +41,17 @@ export class ChartZoomController {
39
41
  get currentKGap(): number {
40
42
  return deriveKGap({
41
43
  kWidth: this.currentKWidth,
42
- dpr: this.deps.getCurrentDpr(),
43
- period: this.deps.getPeriod(),
44
+ dpr: this.deps.viewport.readonly.dpr.peek(),
45
+ period: this.deps.period$.peek(),
44
46
  })
45
47
  }
46
48
 
47
49
  get zoomLevelCount(): number {
48
- return this.deps.zoomLevelCount
50
+ return this.deps.options.readonly.options.peek().zoomLevelCount
49
51
  }
50
52
 
51
53
  zoomToLevel(level: number, anchorX?: number): void {
52
- const clamped = Math.max(1, Math.min(this.deps.zoomLevelCount, Math.round(level)))
54
+ const clamped = Math.max(1, Math.min(this.zoomLevelCount, Math.round(level)))
53
55
  this.applyZoom(clamped, anchorX)
54
56
  }
55
57
 
@@ -65,7 +67,7 @@ export class ChartZoomController {
65
67
  const delta = deltaY > 0 ? -1 : 1
66
68
  const targetLevel = Math.max(
67
69
  1,
68
- Math.min(this.deps.zoomLevelCount, this.currentZoomLevel + delta),
70
+ Math.min(this.zoomLevelCount, this.currentZoomLevel + delta),
69
71
  )
70
72
  if (targetLevel === this.currentZoomLevel) return
71
73
  this.applyZoom(targetLevel, viewportX)
@@ -74,7 +76,7 @@ export class ChartZoomController {
74
76
  handlePinch(delta: number, centerClientX: number): void {
75
77
  const targetLevel = Math.max(
76
78
  1,
77
- Math.min(this.deps.zoomLevelCount, this.currentZoomLevel + delta),
79
+ Math.min(this.zoomLevelCount, this.currentZoomLevel + delta),
78
80
  )
79
81
  if (targetLevel === this.currentZoomLevel) return
80
82
  this.applyZoom(targetLevel, centerClientX)
@@ -84,8 +86,9 @@ export class ChartZoomController {
84
86
  if (targetLevel === this.currentZoomLevel) return
85
87
 
86
88
  const delta = targetLevel - this.currentZoomLevel
87
- const logicalScrollLeft = this.deps.getLogicalScrollLeft()
88
- const dpr = this.deps.getCurrentDpr()
89
+ const logicalScrollLeft = this.deps.viewport.readonly.scrollLeftLogical.peek()
90
+ const dpr = this.deps.viewport.readonly.dpr.peek()
91
+ const opt = this.deps.options.readonly.options.peek()
89
92
 
90
93
  const result = computeZoom(
91
94
  delta,
@@ -95,9 +98,9 @@ export class ChartZoomController {
95
98
  this.currentKWidth,
96
99
  this.currentKGap,
97
100
  {
98
- minKWidth: this.deps.getMinKWidth(),
99
- maxKWidth: this.deps.getMaxKWidth(),
100
- zoomLevelCount: this.deps.zoomLevelCount,
101
+ minKWidth: opt.minKWidth,
102
+ maxKWidth: opt.maxKWidth,
103
+ zoomLevelCount: opt.zoomLevelCount,
101
104
  dpr,
102
105
  dataLength: this.deps.getDataLength(),
103
106
  plotWidth: this.deps.getPlotWidth(),
@@ -108,7 +111,7 @@ export class ChartZoomController {
108
111
  if (!result) return
109
112
 
110
113
  this.zoomState.actions.setZoomLevel(result.targetLevel)
111
- this.deps.setScrollLeft(result.newDomScrollLeft)
114
+ this.deps.viewport.actions.scrollTo(result.newDomScrollLeft)
112
115
  this.deps.onChange?.()
113
116
  }
114
117
  }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { createSignal } from '../../../foundation/reactivity/signal'
4
+ import { createViewportState } from '../../state/viewportState'
5
+ import { clampVisibleRange, getVisibleRange } from '../viewport'
6
+
7
+ describe('clampVisibleRange', () => {
8
+ it('clamps negative start to 0 and preserves end', () => {
9
+ expect(clampVisibleRange({ start: -1, end: 40 })).toEqual({ start: 0, end: 40 })
10
+ })
11
+
12
+ it('leaves non-negative start unchanged', () => {
13
+ expect(clampVisibleRange({ start: 12, end: 50 })).toEqual({ start: 12, end: 50 })
14
+ })
15
+ })
16
+
17
+ describe('getVisibleRange raw expansion', () => {
18
+ it('may return start < 0 when scroll is near the left edge (expansion pad)', () => {
19
+ // scrollLeft=0, first bars sit near plot origin → start-1 扩窗
20
+ const raw = getVisibleRange(0, 800, 8, 2, 100, 1)
21
+ expect(raw.start).toBeLessThan(0)
22
+ expect(raw.end).toBeGreaterThan(0)
23
+ })
24
+ })
25
+
26
+ describe('viewportState visibleRange SSOT', () => {
27
+ it('exposes clamped visibleRange/visibleFrom while keeping rawVisibleRange for load triggers', async () => {
28
+ const dataLength$ = createSignal(20)
29
+ const module = createViewportState({
30
+ options$: (() => ({ bottomAxisHeight: 30, kWidth: 8, kGap: 2 })) as any,
31
+ dataLength$,
32
+ period$: (() => 'daily') as any,
33
+ zoomLevel$: (() => 3) as any,
34
+ })
35
+
36
+ // resize 后 scrollLeftLogical≈0(右对齐短序列或左缘),raw start 常为 -1
37
+ module.actions.resize(800, 400, 1)
38
+ module.actions.scrollTo(module.readonly.leftLoadBufferWidth.peek())
39
+
40
+ const raw = module.readonly.rawVisibleRange()
41
+ const clamped = module.readonly.visibleRange()
42
+ const vs = module.readonly.viewportState()
43
+
44
+ expect(raw.start).toBeLessThan(0)
45
+ expect(clamped.start).toBe(0)
46
+ expect(clamped.end).toBe(raw.end)
47
+ expect(vs.visibleFrom).toBe(0)
48
+ expect(vs.visibleTo).toBe(clamped.end)
49
+ })
50
+ })
@@ -1,69 +1,40 @@
1
- import { type ReadonlySignal } from '../../foundation/reactivity/signal'
2
- import type { ChartDom, Viewport, ViewportState } from '../chartTypes'
1
+ import type { ChartDom } from '../chartTypes'
3
2
  import type { ChartStateKernel } from '../state/chartStateKernel'
4
3
 
5
- /** Minimal manager-level deps (DOM + lifecycle callbacks). */
4
+ /** DOM 生命周期依赖:ResizeObserver / scroll 监听 */
6
5
  export interface ViewportDependencies {
7
6
  getDom: () => ChartDom
8
7
  onResizeCompleted: () => void
9
8
  }
10
9
 
10
+ /**
11
+ * Viewport DOM 适配层:只挂 ResizeObserver 与 scroll,读写一律走 kernel。
12
+ */
11
13
  export class ChartViewportManager {
12
14
  private deps: ViewportDependencies
13
15
  private kernel: ChartStateKernel
14
16
  private resizeObserver?: ResizeObserver
15
17
  private onScroll?: (e: Event) => void
16
18
 
17
- get viewportSignal(): ReadonlySignal<ViewportState> {
18
- return this.kernel.viewport.readonly.viewportState
19
- }
20
-
21
19
  constructor(deps: ViewportDependencies, kernel: ChartStateKernel) {
22
20
  this.deps = deps
23
21
  this.kernel = kernel
24
22
  }
25
23
 
26
- getCachedScrollLeft(): number {
27
- return this.kernel.viewport.readonly.scrollLeft.peek()
28
- }
29
-
30
- getLogicalScrollLeft(): number {
31
- return this.kernel.viewport.readonly.scrollLeftLogical.peek()
32
- }
33
-
34
- getViewport(): Viewport | null {
35
- if (this.kernel.viewport.readonly.viewWidth.peek() === 0) return null
36
- return this.kernel.viewport.readonly.viewport.peek()
37
- }
38
-
39
- getEffectiveDpr(): number {
40
- return this.kernel.viewport.readonly.dpr.peek()
41
- }
42
-
43
- getObservedSize(): { width: number; height: number } {
44
- return {
45
- width: this.kernel.viewport.readonly.viewWidth.peek(),
46
- height: this.kernel.viewport.readonly.viewHeight.peek(),
47
- }
48
- }
49
-
50
- setScrollLeft(v: number): void {
51
- this.kernel.viewport.actions.scrollTo(v)
52
- }
53
-
54
24
  init(): void {
55
- if (typeof ResizeObserver === 'undefined') return
56
-
57
25
  const target = this.deps.getDom().container
58
26
  if (!target) return
59
27
 
28
+ // 首帧尺寸写入 kernel,不依赖 ResizeObserver 是否可用
60
29
  this.kernel.initViewport()
61
30
 
62
- this.onScroll = (e: Event) => {
31
+ this.onScroll = () => {
63
32
  this.kernel.viewport.actions.syncFromDomScroll()
64
33
  }
65
34
  target.addEventListener('scroll', this.onScroll, { passive: true })
66
35
 
36
+ if (typeof ResizeObserver === 'undefined') return
37
+
67
38
  this.resizeObserver = new ResizeObserver((entries) => {
68
39
  const entry = entries[0]
69
40
  if (!entry) return
@@ -119,26 +90,4 @@ export class ChartViewportManager {
119
90
  this.onScroll = undefined
120
91
  }
121
92
  }
122
-
123
- computeViewport(): Viewport | null {
124
- const container = this.deps.getDom().container
125
- if (!container) return null
126
-
127
- const viewWidth = this.kernel.viewport.readonly.viewWidth.peek()
128
- const viewHeight = this.kernel.viewport.readonly.viewHeight.peek()
129
- if (viewWidth === 0 || viewHeight === 0) {
130
- const fallbackW = Math.max(1, Math.round(container.clientWidth))
131
- const fallbackH = Math.max(1, Math.round(container.clientHeight))
132
- if (fallbackW > 0 && fallbackH > 0) {
133
- this.kernel.viewport.actions.resize(fallbackW, fallbackH, this.kernel.viewport.readonly.preciseDpr.peek())
134
- }
135
- }
136
-
137
- return this.kernel.viewport.readonly.viewport.peek()
138
- }
139
-
140
- /** @deprecated No-op — viewportState is now computed(). */
141
- updateViewportSignal(): void {
142
- // no-op
143
- }
144
93
  }
@@ -40,6 +40,20 @@ export function getVisibleRange(
40
40
  return { start, end }
41
41
  }
42
42
 
43
+ /**
44
+ * 将 raw visible range 钳制为可索引区间。
45
+ *
46
+ * @remarks getVisibleRange 左右各扩 1 根时 start 可能为 -1;
47
+ * 绘制 / hit-test / 数据下标必须用 clamp 后的 start>=0。
48
+ * 增量加载检测仍读 raw(start 小于 0 表示已滚到左缘扩窗)。
49
+ */
50
+ export function clampVisibleRange(range: {
51
+ start: number
52
+ end: number
53
+ }): { start: number; end: number } {
54
+ return { start: Math.max(0, range.start), end: range.end }
55
+ }
56
+
43
57
  /**
44
58
  * 计算指定索引区间内的价格范围(max/min)。
45
59
  *
@@ -2,6 +2,24 @@ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import { RendererPluginManager } from './rendererPluginManager'
4
4
 
5
+ describe('RendererPluginManager lifecycle', () => {
6
+ it('installs a renderer plugin exactly once', () => {
7
+ const manager = new RendererPluginManager()
8
+ const onInstall = vi.fn()
9
+ manager.setPluginHost({} as never)
10
+
11
+ manager.register({
12
+ name: 'test',
13
+ paneId: 'main',
14
+ priority: 0,
15
+ draw: vi.fn(),
16
+ onInstall,
17
+ })
18
+
19
+ expect(onInstall).toHaveBeenCalledTimes(1)
20
+ })
21
+ })
22
+
5
23
  describe('RendererPluginManager transaction', () => {
6
24
  it('coalesces renderer invalidations into one callback', () => {
7
25
  const manager = new RendererPluginManager()
@@ -304,7 +304,10 @@ export interface RenderContext {
304
304
  crosshairIndex?: number | null
305
305
  // 可选的其他 Canvas 上下文
306
306
  yAxisCtx?: CanvasRenderingContext2D
307
+ /** 轴区动态层(最新价标签、十字线价签) */
308
+ yAxisOverlayCtx?: CanvasRenderingContext2D
307
309
  leftAxisCtx?: CanvasRenderingContext2D
310
+ leftAxisOverlayCtx?: CanvasRenderingContext2D
308
311
  xAxisCtx?: CanvasRenderingContext2D
309
312
  borderCtx?: CanvasRenderingContext2D
310
313
  /** 覆盖层 Canvas 上下文(用于十字线、Tooltip 等动态内容) */
@@ -10,6 +10,17 @@
10
10
  * 一帧固定走 capture → derive → seal → render → publish → complete。
11
11
  * render 或 publish 期间的 writeInput 一律进入下一代 pending。
12
12
  * 非 idle 时调用 flush 不会嵌套发布,仅保留 dirty 供外层完成后调度。
13
+ *
14
+ * @remarks 适用范围
15
+ * 这是一个半通用的 rAF 合帧器(semi-general frame coalescer),
16
+ * 专为 ChartRenderer 的绘制管线设计,并非全场景通用的高频处理方案。
17
+ *
18
+ * @Todo 如需在其他场景(WebSocket 消息合并、键盘事件批处理)复用,需补充:
19
+ * - 深层合并 / 自定义 merge 策略(目前 mergeInput 仅浅合并)
20
+ * - 快照深层不可变保证(目前 sealSnapshotRoot 只冻根对象)
21
+ * - 超时/失帧策略(目前纯乐观逐帧)
22
+ * - 背压与优先级集成(FrameBudget 在 scheduler 层但未在此集成)
23
+ * - 非 Record 输入类型支持(目前 TInput 约束为 Record)
13
24
  */
14
25
 
15
26
  import { createSignal, type ReadonlySignal } from './signal'
@@ -88,10 +99,9 @@ export interface FrameTransaction<TInput extends Record<string, unknown>, TSnaps
88
99
  * @typeParam TInput - 可合并的输入形状(浅层 Partial 合并)
89
100
  * @typeParam TSnapshot - derive 产出的不可变快照类型
90
101
  */
91
- export function createFrameTransaction<
92
- TInput extends Record<string, unknown>,
93
- TSnapshot,
94
- >(options: FrameTransactionOptions<TInput, TSnapshot>): FrameTransaction<TInput, TSnapshot> {
102
+ export function createFrameTransaction<TInput extends Record<string, unknown>, TSnapshot>(
103
+ options: FrameTransactionOptions<TInput, TSnapshot>,
104
+ ): FrameTransaction<TInput, TSnapshot> {
95
105
  const { derive, render } = options
96
106
  const schedule =
97
107
  options.schedule ??
@@ -145,24 +155,27 @@ export function createFrameTransaction<
145
155
  }
146
156
 
147
157
  let sealedInput: TInput | undefined
158
+ // 封存输入,后续写入只能进下一代
148
159
  phase = 'capturing'
149
160
  try {
150
161
  sealedInput = pending
151
162
  dirty = false
152
- // 本帧 capture 之后的新写入进入 nextPending,不得污染 sealedInput
153
163
  pending = { ...sealedInput }
154
164
 
165
+ // 从封存输入纯推导不可变快照
155
166
  phase = 'deriving'
156
167
  const nextGeneration = generation + 1
157
168
  const snapshot = derive(sealedInput, nextGeneration)
158
169
 
170
+ // 冻结快照根对象
159
171
  phase = 'sealing'
160
172
  sealSnapshotRoot(snapshot)
161
173
 
174
+ // 用本帧快照绘制
162
175
  phase = 'rendering'
163
176
  render?.(snapshot)
164
177
 
165
- // 先推进代际再发布,保证订阅者读 generation 与 snapshot.generation 一致
178
+ // 先推进代际再发布,保证订阅者读到一致代际
166
179
  phase = 'publishing'
167
180
  generation = nextGeneration
168
181
  published.set(snapshot)
@@ -191,6 +204,7 @@ export function createFrameTransaction<
191
204
  }
192
205
 
193
206
  function scheduleFlush(): void {
207
+ // 已有 raf 帧,
194
208
  if (scheduleQueued) return
195
209
  if (!dirty && nextPending === null) return
196
210
  scheduleQueued = true