@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
@@ -6,12 +6,13 @@ import { TimeShareBuffer } from '../../data/timeShareBuffer'
6
6
  import type { TimeShareFetcherFn } from '../../data/types'
7
7
  import { createSignal, type ReadonlySignal, type Signal } from '../../foundation/reactivity/signal'
8
8
  import type { KLineData, TimeShareData } from '../../foundation/types/price'
9
- import type { ChartDom, Viewport } from '../chartTypes'
9
+ import type { ChartDom } from '../chartTypes'
10
10
  import type { VisibleRange, UpdateLevel } from '../layout/pane'
11
11
  import { getPhysicalKLineConfig } from '../utils/klineConfig'
12
- import { getVisibleRange } from '../viewport/viewport'
13
12
  import type { DataStateModule } from '../state/dataState'
14
13
  import type { DataManagerStateModule } from '../state/dataManagerState'
14
+ import type { ViewportStateModule } from '../state/viewportState'
15
+ import type { ComparisonStateModule } from '../state/comparisonState'
15
16
 
16
17
  import { ComparisonManager } from './comparisonManager'
17
18
  import { FetchBatchScheduler } from './fetchBatchScheduler'
@@ -20,17 +21,11 @@ import { ScrollCompensator } from './scrollCompensator'
20
21
 
21
22
  export interface DataDependencies {
22
23
  getOption: () => { kWidth: number; kGap: number }
23
- getEffectiveDpr: () => number
24
- getLogicalScrollLeft: () => number
25
- getCachedScrollLeft: () => number
26
- setScrollLeft: (v: number) => void
27
24
  getDom: () => ChartDom
28
- getObservedSize: () => { width: number; height: number }
29
- getViewport: () => Viewport | null
30
- getVisibleRange: () => VisibleRange | null
31
- /** 几何 SSOT:由 kernel viewport.readonly 注入 */
32
- getLeftLoadBufferWidth: () => number
33
- getContentWidth: () => number
25
+ /** scroll / dpr / 可见区间 / 几何 SSOT */
26
+ viewport: ViewportStateModule
27
+ /** 对比叠加状态 SSOT */
28
+ comparison: ComparisonStateModule
34
29
  scheduleDraw: (level?: UpdateLevel) => void
35
30
  resetInteraction: () => void
36
31
  getIndicatorScheduler: () => {
@@ -40,11 +35,8 @@ export interface DataDependencies {
40
35
  isPointerDown: () => boolean
41
36
  onTimeShareDataReady: (dataLength: number) => void
42
37
  onDataProcessed?: (data: KLineData[], range: VisibleRange) => void
38
+ /** 写 symbols 选择(含 primary + comparison) */
43
39
  setSymbols: (symbols: ReadonlyArray<SymbolSpec>) => void
44
- setComparisonLoading: (loading: boolean) => void
45
- comparisonSpecs$: ReadonlySignal<ReadonlyArray<SymbolSpec>>
46
- comparisonColors$: ReadonlySignal<ReadonlyMap<string, string>>
47
- comparisonLoading$: ReadonlySignal<boolean>
48
40
  }
49
41
 
50
42
  const BUF_PRIMARY = 'main'
@@ -95,10 +87,10 @@ export class ChartDataManager {
95
87
  getKLineBuffer: (key) => this._klineBuffers.get(key),
96
88
  getKLineBufferKeys: () => [...this._klineBuffers.keys()],
97
89
  scheduleDraw: () => this.deps.scheduleDraw(),
98
- getSpecs: () => this.deps.comparisonSpecs$.peek(),
99
- setLoading: (loading) => this.deps.setComparisonLoading(loading),
90
+ getSpecs: () => this.deps.comparison.readonly.specs.peek(),
91
+ setLoading: (loading) => this.deps.comparison.actions.setLoading(loading),
100
92
  })
101
- this._comparisonSpecsUnsub = this.deps.comparisonSpecs$.subscribe(() => {
93
+ this._comparisonSpecsUnsub = this.deps.comparison.readonly.specs.subscribe(() => {
102
94
  this.reconcileComparisonBuffers()
103
95
  })
104
96
  this.reconcileComparisonBuffers()
@@ -305,7 +297,7 @@ export class ChartDataManager {
305
297
  this._dmState.actions.setRangeInitialized(true)
306
298
  }
307
299
 
308
- let currentRange = this.deps.getVisibleRange()
300
+ let currentRange = this.getVisibleRangeOrNull()
309
301
  if (!currentRange && this._dmState.readonly.rangeInitialized.peek() && bufferData.length > 0) {
310
302
  currentRange = { start: 0, end: bufferData.length }
311
303
  }
@@ -325,7 +317,7 @@ export class ChartDataManager {
325
317
  }
326
318
 
327
319
  private recordIncrementalLoad(prependedCount: number): void {
328
- this._dmState.actions.recordIncrementalLoad(prependedCount, this.deps.getLeftLoadBufferWidth())
320
+ this._dmState.actions.recordIncrementalLoad(prependedCount, this.deps.viewport.readonly.leftLoadBufferWidth.peek())
329
321
  }
330
322
 
331
323
  private scheduleIncrementalLoadHintFlush(key: string): void {
@@ -370,7 +362,7 @@ export class ChartDataManager {
370
362
  // ── Internal helpers ──
371
363
 
372
364
  getLeftLoadBufferWidth(): number {
373
- return this.deps.getLeftLoadBufferWidth()
365
+ return this.deps.viewport.readonly.leftLoadBufferWidth.peek()
374
366
  }
375
367
 
376
368
  private getActiveKLineLength(): number {
@@ -378,9 +370,21 @@ export class ChartDataManager {
378
370
  return buf ? buf.getRawData().length : 0
379
371
  }
380
372
 
373
+ /** 无 viewport / 无数据时返回 null;clamped 可索引区间(start>=0) */
374
+ private getVisibleRangeOrNull(): VisibleRange | null {
375
+ if (this.deps.viewport.readonly.viewWidth.peek() === 0) return null
376
+ return this.deps.viewport.readonly.visibleRange.peek()
377
+ }
378
+
379
+ /** raw 可见区间(含左右扩窗,start 可能为 -1);供增量加载左缘检测 */
380
+ private getRawVisibleRangeOrNull(): VisibleRange | null {
381
+ if (this.deps.viewport.readonly.viewWidth.peek() === 0) return null
382
+ return this.deps.viewport.readonly.rawVisibleRange.peek()
383
+ }
384
+
381
385
  /** 当前可见范围(on-demand 实时计算,消除 stale 缓存) */
382
386
  getCurrentVisibleRange(): VisibleRange | null {
383
- return this.deps.getVisibleRange()
387
+ return this.getVisibleRangeOrNull()
384
388
  }
385
389
 
386
390
  /** Unified data signal — always reflects the active buffer's data */
@@ -469,7 +473,7 @@ export class ChartDataManager {
469
473
  }
470
474
 
471
475
  getComparisonSpecs(): SymbolSpec[] {
472
- return this.deps.comparisonSpecs$.peek().map((spec) => ({ ...spec }))
476
+ return this.deps.comparison.readonly.specs.peek().map((spec) => ({ ...spec }))
473
477
  }
474
478
 
475
479
  get dataBuffer(): KLineBuffer {
@@ -485,15 +489,15 @@ export class ChartDataManager {
485
489
  }
486
490
 
487
491
  get comparisonColors(): ReadonlySignal<ReadonlyMap<string, string>> {
488
- return this.deps.comparisonColors$
492
+ return this.deps.comparison.readonly.colors
489
493
  }
490
494
 
491
495
  get comparisonLoading(): ReadonlySignal<boolean> {
492
- return this.deps.comparisonLoading$
496
+ return this.deps.comparison.readonly.loading
493
497
  }
494
498
 
495
499
  getComparisonColors(): Map<string, string> {
496
- return new Map(this.deps.comparisonColors$.peek())
500
+ return new Map(this.deps.comparison.readonly.colors.peek())
497
501
  }
498
502
 
499
503
  // ── Data updates (KLine) ──
@@ -554,20 +558,22 @@ export class ChartDataManager {
554
558
  if (data.length === 0) return
555
559
  const window = buf.loadedWindow
556
560
  if (!window) return
557
- const range = this.deps.getVisibleRange()
558
- if (!range) return
561
+ // 左缘扩窗检测必须用 raw(start 可为 -1);数据下标用 clamped
562
+ const rawRange = this.getRawVisibleRangeOrNull()
563
+ const range = this.getVisibleRangeOrNull()
564
+ if (!rawRange || !range) return
559
565
 
560
566
  const MS_PER_DAY = 86_400_000
561
567
  const spec = buf.currentSpec
562
568
  const gapDays = getPeriodDays(spec?.period)
563
569
  let firstVisibleTs: number | undefined
564
570
 
565
- if (range.start < 0 && this._dataFetcher) {
571
+ if (rawRange.start < 0 && this._dataFetcher) {
566
572
  const earlierThanEarliest = window.earliestTs - gapDays * MS_PER_DAY
567
573
  buf.ensureRange(earlierThanEarliest, window.earliestTs)
568
574
  firstVisibleTs = data[0]?.timestamp
569
575
  } else if (range.start < data.length) {
570
- firstVisibleTs = data[Math.max(0, range.start)]?.timestamp
576
+ firstVisibleTs = data[range.start]?.timestamp
571
577
  if (firstVisibleTs !== undefined && firstVisibleTs < window.earliestTs) {
572
578
  buf.ensureRange(firstVisibleTs, window.earliestTs)
573
579
  }
@@ -587,18 +593,18 @@ export class ChartDataManager {
587
593
 
588
594
  addComparisonSymbol(spec: SymbolSpec): void {
589
595
  const primary = this._dataState.readonly.symbols.peek()[0]
590
- if (!primary || this.deps.comparisonSpecs$.peek().some((item) => item.symbol === spec.symbol))
596
+ if (!primary || this.deps.comparison.readonly.specs.peek().some((item) => item.symbol === spec.symbol))
591
597
  return
592
- this.deps.setSymbols([primary, ...this.deps.comparisonSpecs$.peek(), spec])
598
+ this.deps.setSymbols([primary, ...this.deps.comparison.readonly.specs.peek(), spec])
593
599
  }
594
600
 
595
601
  setComparisonData(symbol: string, data: KLineData[]): void {
596
602
  const primary = this._dataState.readonly.symbols.peek()[0]
597
603
  if (!primary) return
598
- if (!this.deps.comparisonSpecs$.peek().some((spec) => spec.symbol === symbol)) {
604
+ if (!this.deps.comparison.readonly.specs.peek().some((spec) => spec.symbol === symbol)) {
599
605
  this.deps.setSymbols([
600
606
  primary,
601
- ...this.deps.comparisonSpecs$.peek(),
607
+ ...this.deps.comparison.readonly.specs.peek(),
602
608
  { symbol, period: 'daily' },
603
609
  ])
604
610
  }
@@ -607,11 +613,11 @@ export class ChartDataManager {
607
613
 
608
614
  removeComparisonSymbol(symbol: string): void {
609
615
  const primary = this._dataState.readonly.symbols.peek()[0]
610
- if (!primary || !this.deps.comparisonSpecs$.peek().some((spec) => spec.symbol === symbol))
616
+ if (!primary || !this.deps.comparison.readonly.specs.peek().some((spec) => spec.symbol === symbol))
611
617
  return
612
618
  this.deps.setSymbols([
613
619
  primary,
614
- ...this.deps.comparisonSpecs$.peek().filter((spec) => spec.symbol !== symbol),
620
+ ...this.deps.comparison.readonly.specs.peek().filter((spec) => spec.symbol !== symbol),
615
621
  ])
616
622
  this.deps.scheduleDraw()
617
623
  }
@@ -636,19 +642,8 @@ export class ChartDataManager {
636
642
  const dataLen = kRaw?.length ?? 0
637
643
  let visibleStart = 0
638
644
  if (dataLen > 0) {
639
- const vp = this.deps.getViewport()
640
- if (vp) {
641
- const opt = this.deps.getOption()
642
- const vRange = getVisibleRange(
643
- vp.scrollLeft,
644
- vp.plotWidth,
645
- opt.kWidth,
646
- opt.kGap,
647
- dataLen,
648
- vp.dpr,
649
- )
650
- visibleStart = vRange ? Math.max(0, vRange.start) : 0
651
- }
645
+ const vRange = this.getVisibleRangeOrNull()
646
+ visibleStart = vRange ? Math.max(0, vRange.start) : 0
652
647
  }
653
648
  // 双路径守卫:
654
649
  // - switchToTimeShareForDate 路径:setTimeShareQueryDate 已在
@@ -693,7 +688,7 @@ export class ChartDataManager {
693
688
  return
694
689
  }
695
690
  const next = { ...current, period }
696
- this.setSymbols([next, ...this.deps.comparisonSpecs$.peek()])
691
+ this.setSymbols([next, ...this.deps.comparison.readonly.specs.peek()])
697
692
  }
698
693
 
699
694
  /**
@@ -725,7 +720,7 @@ export class ChartDataManager {
725
720
  incremental: false,
726
721
  source: source.source ?? 'custom',
727
722
  }
728
- this.setSymbols([spec, ...this.deps.comparisonSpecs$.peek()])
723
+ this.setSymbols([spec, ...this.deps.comparison.readonly.specs.peek()])
729
724
 
730
725
  const symbolCode = spec.symbol
731
726
  if (symbolCode) {
@@ -760,7 +755,7 @@ export class ChartDataManager {
760
755
  this._dataState.actions.setData([])
761
756
  this._dmState.actions.setRangeInitialized(false)
762
757
  this._dmState.actions.setSavedScrollTimestamp(null)
763
- this.setSymbols([spec, ...this.deps.comparisonSpecs$.peek()])
758
+ this.setSymbols([spec, ...this.deps.comparison.readonly.specs.peek()])
764
759
  }
765
760
 
766
761
  getPreCustomSpec(): SymbolSpec | null {
@@ -872,12 +867,12 @@ export class ChartDataManager {
872
867
  )
873
868
  this._dmState.actions.setSavedScrollTimestamp(null)
874
869
  if (idx >= 0) {
875
- const dpr = this.deps.getEffectiveDpr()
870
+ const dpr = this.deps.viewport.readonly.dpr.peek()
876
871
  const opt = this.deps.getOption()
877
872
  const { unitPx, startXPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
878
873
  const leftBuffer = this.getLeftLoadBufferWidth()
879
874
  const scrollLeft = ((idx + 1) * unitPx + startXPx) / dpr + leftBuffer
880
- this.deps.setScrollLeft(scrollLeft)
875
+ this.deps.viewport.actions.scrollTo(scrollLeft)
881
876
  return true
882
877
  }
883
878
  return false
@@ -886,7 +881,7 @@ export class ChartDataManager {
886
881
  // ── Content width ──
887
882
 
888
883
  getContentWidth(): number {
889
- return this.deps.getContentWidth()
884
+ return this.deps.viewport.readonly.contentWidth.peek()
890
885
  }
891
886
 
892
887
  scrollToRight(): void {
@@ -962,12 +957,12 @@ export class ChartDataManager {
962
957
  }
963
958
 
964
959
  getLogicalIndexAtX(mouseX: number): number | null {
965
- const vp = this.deps.getViewport()
966
- if (!vp) return null
960
+ if (this.deps.viewport.readonly.viewWidth.peek() === 0) return null
961
+ const vp = this.deps.viewport.readonly.viewport.peek()
967
962
  const buf = this.getActiveDataBuffer()
968
963
  const data = buf ? buf.getRawData() : []
969
964
  if (data.length === 0) return null
970
- const dpr = this.deps.getEffectiveDpr()
965
+ const dpr = this.deps.viewport.readonly.dpr.peek()
971
966
  const opt = this.deps.getOption()
972
967
  const { startXPx, unitPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
973
968
  const worldX = Math.round((vp.scrollLeft + mouseX) * dpr)
@@ -1,14 +1,14 @@
1
1
  import type { ChartDom } from '../chartTypes'
2
2
  import { getPhysicalKLineConfig } from '../utils/klineConfig'
3
+ import type { ViewportStateModule } from '../state/viewportState'
3
4
 
4
5
  /** 与 theme selectionFill 对齐:light #2D7FF933 / dark #4A9EFF33;fallback 为 light 默认 */
5
6
  const INCREMENTAL_LOAD_HINT_BG = 'var(--klc-color-selection-fill, #2D7FF933)'
6
7
 
7
8
  export interface HintDeps {
8
9
  getOption: () => { kWidth: number; kGap: number }
9
- getEffectiveDpr: () => number
10
10
  getDom: () => ChartDom
11
- getViewport: () => { viewHeight: number } | null
11
+ viewport: ViewportStateModule
12
12
  }
13
13
 
14
14
  interface ActiveHint {
@@ -86,17 +86,18 @@ export class IncrementalLoadHint {
86
86
  }
87
87
 
88
88
  private _updateGeometry(hint: HTMLDivElement, count: number, leftBufferWidth: number): void {
89
- const dpr = this.deps.getEffectiveDpr()
89
+ const dpr = this.deps.viewport.readonly.dpr.peek()
90
90
  const opt = this.deps.getOption()
91
91
  const { unitPx, startXPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
92
92
 
93
93
  hint.style.left = `${leftBufferWidth}px`
94
94
  const width = (startXPx + count * unitPx) / dpr
95
95
  hint.style.width = `${Math.max(0, width)}px`
96
- hint.style.height = `${Math.max(
97
- 0,
98
- this.deps.getViewport()?.viewHeight ?? this.deps.getDom().container?.clientHeight ?? 0,
99
- )}px`
96
+ const viewHeight =
97
+ this.deps.viewport.readonly.viewHeight.peek() ||
98
+ this.deps.getDom().container?.clientHeight ||
99
+ 0
100
+ hint.style.height = `${Math.max(0, viewHeight)}px`
100
101
  }
101
102
 
102
103
  private _fadeOutAndRemove(entry: ActiveHint): void {
@@ -1,17 +1,10 @@
1
- import type { ChartDom, Viewport } from '../chartTypes'
2
1
  import { getPhysicalKLineConfig } from '../utils/klineConfig'
2
+ import type { ViewportStateModule } from '../state/viewportState'
3
3
 
4
4
  export interface ScrollDeps {
5
5
  getOption: () => { kWidth: number; kGap: number }
6
- getEffectiveDpr: () => number
7
- getCachedScrollLeft: () => number
8
- setScrollLeft: (v: number) => void
9
- getDom: () => ChartDom
10
- getObservedSize: () => { width: number; height: number }
11
- getViewport: () => Viewport | null
12
- /** 几何 SSOT:由 kernel viewport.readonly 注入 */
13
- getLeftLoadBufferWidth: () => number
14
- getContentWidth: () => number
6
+ /** scroll / dpr / 几何 SSOT */
7
+ viewport: ViewportStateModule
15
8
  }
16
9
 
17
10
  export const SCROLL_TRAILING_SLOTS = 30
@@ -20,53 +13,52 @@ export class ScrollCompensator {
20
13
  constructor(private deps: ScrollDeps) {}
21
14
 
22
15
  compensatePrepend(count: number): void {
23
- const dpr = this.deps.getEffectiveDpr()
16
+ const dpr = this.deps.viewport.readonly.dpr.peek()
24
17
  const opt = this.deps.getOption()
25
18
  const { unitPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
26
19
  const compensation = (count * unitPx) / dpr
27
- const nextScrollLeft = this.deps.getCachedScrollLeft() + compensation
28
- this.deps.setScrollLeft(nextScrollLeft)
20
+ const nextScrollLeft = this.deps.viewport.readonly.scrollLeft.peek() + compensation
21
+ this.deps.viewport.actions.scrollTo(nextScrollLeft)
29
22
  }
30
23
 
31
24
  adjustScrollAfterDataChange(dataLength: number): void {
32
- const scrollLeft = this.deps.getCachedScrollLeft()
25
+ const scrollLeft = this.deps.viewport.readonly.scrollLeft.peek()
33
26
  if (scrollLeft < 0) return
34
27
 
35
28
  if (scrollLeft <= 0) {
36
- this.deps.setScrollLeft(this.deps.getLeftLoadBufferWidth())
29
+ this.deps.viewport.actions.scrollTo(this.deps.viewport.readonly.leftLoadBufferWidth.peek())
37
30
  return
38
31
  }
39
32
 
40
- const dpr = this.deps.getEffectiveDpr()
33
+ const dpr = this.deps.viewport.readonly.dpr.peek()
41
34
  const opt = this.deps.getOption()
42
35
  const { unitPx, startXPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
43
36
  const totalDataWidth = (startXPx + dataLength * unitPx) / dpr
44
- const leftBuffer = this.deps.getLeftLoadBufferWidth()
37
+ const leftBuffer = this.deps.viewport.readonly.leftLoadBufferWidth.peek()
45
38
  if (scrollLeft >= leftBuffer + totalDataWidth) {
46
- this.deps.setScrollLeft(leftBuffer)
39
+ this.deps.viewport.actions.scrollTo(leftBuffer)
47
40
  }
48
41
  }
49
42
 
50
43
  scrollToRight(dataLength: number): void {
51
44
  if (dataLength === 0) return
52
- const dpr = this.deps.getEffectiveDpr()
45
+ const dpr = this.deps.viewport.readonly.dpr.peek()
53
46
  const opt = this.deps.getOption()
54
47
  const { unitPx, startXPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
55
48
  const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
56
- const viewport = this.deps.getViewport()
57
- const clientWidth = viewport?.viewWidth ?? 0
49
+ const clientWidth = this.deps.viewport.readonly.viewWidth.peek()
58
50
  if (clientWidth <= 0) return
59
51
 
60
- const leftBuffer = this.deps.getLeftLoadBufferWidth()
52
+ const leftBuffer = this.deps.viewport.readonly.leftLoadBufferWidth.peek()
61
53
  let target: number
62
54
  if (lastKLineEndPx <= clientWidth) {
63
55
  target = leftBuffer - (clientWidth - lastKLineEndPx)
64
56
  } else {
65
57
  target = leftBuffer + (lastKLineEndPx - clientWidth)
66
58
  }
67
- const contentWidth = this.deps.getContentWidth()
59
+ const contentWidth = this.deps.viewport.readonly.contentWidth.peek()
68
60
  const maxScroll = Math.max(0, contentWidth - clientWidth)
69
61
  const scrollLeft = Math.round(Math.max(0, Math.min(target, maxScroll)) * dpr) / dpr
70
- this.deps.setScrollLeft(scrollLeft)
62
+ this.deps.viewport.actions.scrollTo(scrollLeft)
71
63
  }
72
64
  }
@@ -52,7 +52,7 @@ function createMockDeps() {
52
52
  upsertPane: vi.fn(),
53
53
  removePaneDefinition: vi.fn(),
54
54
  getPaneSpecs: vi.fn(() => []),
55
- getPaneRatiosSignal: () => paneRatiosSignal,
55
+ paneRatios$: paneRatiosSignal,
56
56
  paneSpecs$: paneSpecsSignal,
57
57
  getInternalPaneRatios: vi.fn(() => new Map()),
58
58
  setInternalPaneRatio: vi.fn(),
@@ -68,30 +68,22 @@ function createMockDeps() {
68
68
  removeLayer: vi.fn(() => false),
69
69
  getLayer: vi.fn(() => null),
70
70
  setLayerVisibility: vi.fn(),
71
- mainIndicators$: indicatorState.readonly.mainIndicators,
72
- upsertMainIndicator: (id, p) => indicatorState.actions.upsert(id, p),
73
- removeMainIndicator: (id) => indicatorState.actions.remove(id),
74
- setMainIndicatorParams: (id, p) => indicatorState.actions.setParams(id, p),
75
- replaceMainIndicators: (entries) => indicatorState.actions.replaceAll(entries),
76
- clearMainIndicators: () => indicatorState.actions.clear(),
77
- subPanes$: subPaneState.readonly.entries,
78
- createSubPaneState: vi.fn((paneId, indicatorId, params) =>
79
- subPaneState.actions.upsert({ paneId, indicatorId, params }),
80
- ),
81
- removeSubPaneState: vi.fn((paneId) => subPaneState.actions.remove(paneId)),
82
- replaceSubPaneState: vi.fn((paneId, indicatorId, params) =>
83
- subPaneState.actions.replace({ paneId, indicatorId, params }),
84
- ),
85
- updateSubPaneStateParams: vi.fn((paneId, params) =>
86
- subPaneState.actions.setParams(paneId, params),
87
- ),
88
- clearSubPaneState: vi.fn(() => subPaneState.actions.clear()),
71
+ indicator: indicatorState,
72
+ subPaneOps: {
73
+ entries: subPaneState.readonly.entries,
74
+ create: vi.fn((paneId, indicatorId, params) =>
75
+ subPaneState.actions.upsert({ paneId, indicatorId, params }),
76
+ ),
77
+ remove: vi.fn((paneId) => subPaneState.actions.remove(paneId)),
78
+ replace: vi.fn((paneId, indicatorId, params) =>
79
+ subPaneState.actions.replace({ paneId, indicatorId, params }),
80
+ ),
81
+ setParams: vi.fn((paneId, params) => subPaneState.actions.setParams(paneId, params)),
82
+ clear: vi.fn(() => subPaneState.actions.clear()),
83
+ },
89
84
  projectPaneLayout: vi.fn(),
90
85
  runRendererTransaction: (run) => run(),
91
86
  getIndicatorScheduler: vi.fn(),
92
- getRightAxisWidth: () => 60,
93
- getPriceLabelWidth: () => 60,
94
- getYPaddingPx: () => 4,
95
87
  } as IndicatorDependencies & { rendererMap: Map<string, any> }
96
88
  }
97
89
 
@@ -169,9 +161,9 @@ describe('ChartIndicatorManager', () => {
169
161
  it('creates sub-pane business state before projecting runtime resources', () => {
170
162
  expect(manager.createSubPane('RSI_0', 'RSI', { period1: 6 })).toBe(true)
171
163
 
172
- expect(deps.createSubPaneState).toHaveBeenCalledTimes(1)
164
+ expect(deps.subPaneOps.create).toHaveBeenCalledTimes(1)
173
165
  expect(deps.useRenderer).toHaveBeenCalled()
174
- expect(deps.createSubPaneState.mock.invocationCallOrder[0]).toBeLessThan(
166
+ expect(deps.subPaneOps.create.mock.invocationCallOrder[0]).toBeLessThan(
175
167
  deps.useRenderer.mock.invocationCallOrder[0]!,
176
168
  )
177
169
  expect(manager.getSubPaneEntry('RSI_0')?.params).toEqual({ period1: 6 })
@@ -183,7 +175,7 @@ describe('ChartIndicatorManager', () => {
183
175
 
184
176
  expect(manager.createSubPane('RSI_0', 'MACD', { fast: 5 })).toBe(true)
185
177
 
186
- expect(deps.replaceSubPaneState).not.toHaveBeenCalled()
178
+ expect(deps.subPaneOps.replace).not.toHaveBeenCalled()
187
179
  expect(manager.getSubPaneEntry('RSI_0')?.indicatorId).toBe('RSI')
188
180
  expect(manager.getSubPaneEntry('RSI_0')?.params).toEqual({ period1: 6 })
189
181
  })
@@ -213,7 +205,7 @@ describe('ChartIndicatorManager', () => {
213
205
  manager.destroy()
214
206
  vi.clearAllMocks()
215
207
 
216
- deps.upsertMainIndicator('MA', { ma5: true })
208
+ deps.indicator.actions.upsert('MA', { ma5: true })
217
209
 
218
210
  expect(deps.useRenderer).not.toHaveBeenCalled()
219
211
  expect(deps.scheduleDraw).not.toHaveBeenCalled()