@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
@@ -4,56 +4,58 @@ import { describe, expect, it } from 'vitest'
4
4
  import { Pane } from '@/core/layout/pane'
5
5
  import { PaneRenderer } from '@/core/paneRenderer'
6
6
 
7
+ function createDom() {
8
+ return {
9
+ mainCanvas: document.createElement('canvas'),
10
+ overlayCanvas: document.createElement('canvas'),
11
+ yAxisCanvas: document.createElement('canvas'),
12
+ yAxisOverlayCanvas: document.createElement('canvas'),
13
+ }
14
+ }
15
+
7
16
  describe('PaneRenderer resize DPR mapping', () => {
8
17
  it('maps logical plot size to physical canvas size and keeps CSS size logical', () => {
9
- const mainCanvas = document.createElement('canvas')
10
- const overlayCanvas = document.createElement('canvas')
11
- const yAxisCanvas = document.createElement('canvas')
18
+ const dom = createDom()
12
19
  const pane = new Pane('main')
13
- const renderer = new PaneRenderer(
14
- { mainCanvas, overlayCanvas, yAxisCanvas },
15
- pane,
16
- {
17
- rightAxisWidth: 80,
18
- leftAxisWidth: 60,
19
- yPaddingPx: 0,
20
- priceLabelWidth: 60,
21
- },
22
- )
20
+ const renderer = new PaneRenderer(dom, pane, {
21
+ rightAxisWidth: 80,
22
+ leftAxisWidth: 60,
23
+ yPaddingPx: 0,
24
+ priceLabelWidth: 60,
25
+ })
23
26
 
24
27
  renderer.resize(500, 240, 2)
25
28
 
26
- expect(mainCanvas.width).toBe(Math.round(500 * 2))
27
- expect(mainCanvas.height).toBe(Math.round(240 * 2))
28
- expect(mainCanvas.style.width).toBe('500px')
29
- expect(mainCanvas.style.height).toBe('240px')
29
+ expect(dom.mainCanvas.width).toBe(Math.round(500 * 2))
30
+ expect(dom.mainCanvas.height).toBe(Math.round(240 * 2))
31
+ expect(dom.mainCanvas.style.width).toBe('500px')
32
+ expect(dom.mainCanvas.style.height).toBe('240px')
30
33
  // overlayCanvas should match mainCanvas
31
- expect(overlayCanvas.width).toBe(mainCanvas.width)
32
- expect(overlayCanvas.height).toBe(mainCanvas.height)
34
+ expect(dom.overlayCanvas.width).toBe(dom.mainCanvas.width)
35
+ expect(dom.overlayCanvas.height).toBe(dom.mainCanvas.height)
36
+ // yAxis overlay matches yAxis base
37
+ expect(dom.yAxisOverlayCanvas.width).toBe(dom.yAxisCanvas.width)
38
+ expect(dom.yAxisOverlayCanvas.height).toBe(dom.yAxisCanvas.height)
33
39
  })
34
40
 
35
41
  it('uses (rightAxisWidth + priceLabelWidth) for yAxis physical width when no parent width is available', () => {
36
- const mainCanvas = document.createElement('canvas')
37
- const overlayCanvas = document.createElement('canvas')
38
- const yAxisCanvas = document.createElement('canvas')
42
+ const dom = createDom()
39
43
  const pane = new Pane('main')
40
- const renderer = new PaneRenderer(
41
- { mainCanvas, overlayCanvas, yAxisCanvas },
42
- pane,
43
- {
44
- rightAxisWidth: 100,
45
- leftAxisWidth: 60,
46
- yPaddingPx: 0,
47
- priceLabelWidth: 70,
48
- },
49
- )
44
+ const renderer = new PaneRenderer(dom, pane, {
45
+ rightAxisWidth: 100,
46
+ leftAxisWidth: 60,
47
+ yPaddingPx: 0,
48
+ priceLabelWidth: 70,
49
+ })
50
50
 
51
51
  renderer.resize(500, 200, 1.5)
52
52
 
53
- expect(yAxisCanvas.width).toBe(Math.round((100 + 70) * 1.5))
54
- expect(yAxisCanvas.height).toBe(Math.round(200 * 1.5))
55
- expect(yAxisCanvas.style.width).toBe('170px')
56
- expect(yAxisCanvas.style.height).toBe('200px')
53
+ expect(dom.yAxisCanvas.width).toBe(Math.round((100 + 70) * 1.5))
54
+ expect(dom.yAxisCanvas.height).toBe(Math.round(200 * 1.5))
55
+ expect(dom.yAxisCanvas.style.width).toBe('170px')
56
+ expect(dom.yAxisCanvas.style.height).toBe('200px')
57
+ expect(dom.yAxisOverlayCanvas.width).toBe(dom.yAxisCanvas.width)
58
+ expect(dom.yAxisOverlayCanvas.height).toBe(dom.yAxisCanvas.height)
57
59
  })
58
60
 
59
61
  it('uses parent clientWidth for yAxis canvas width when available', () => {
@@ -63,28 +65,24 @@ describe('PaneRenderer resize DPR mapping', () => {
63
65
  configurable: true,
64
66
  })
65
67
 
66
- const mainCanvas = document.createElement('canvas')
67
- const overlayCanvas = document.createElement('canvas')
68
- const yAxisCanvas = document.createElement('canvas')
69
- host.appendChild(yAxisCanvas)
68
+ const dom = createDom()
69
+ host.appendChild(dom.yAxisCanvas)
70
+ host.appendChild(dom.yAxisOverlayCanvas)
70
71
 
71
72
  const pane = new Pane('main')
72
- const renderer = new PaneRenderer(
73
- { mainCanvas, overlayCanvas, yAxisCanvas },
74
- pane,
75
- {
76
- rightAxisWidth: 100,
77
- leftAxisWidth: 60,
78
- yPaddingPx: 0,
79
- priceLabelWidth: 70,
80
- },
81
- )
73
+ const renderer = new PaneRenderer(dom, pane, {
74
+ rightAxisWidth: 100,
75
+ leftAxisWidth: 60,
76
+ yPaddingPx: 0,
77
+ priceLabelWidth: 70,
78
+ })
82
79
 
83
80
  renderer.resize(500, 200, 1.5)
84
81
 
85
- expect(yAxisCanvas.width).toBe(Math.round(168 * 1.5))
86
- expect(yAxisCanvas.height).toBe(Math.round(200 * 1.5))
87
- expect(yAxisCanvas.style.width).toBe('168px')
88
- expect(yAxisCanvas.style.height).toBe('200px')
82
+ expect(dom.yAxisCanvas.width).toBe(Math.round(168 * 1.5))
83
+ expect(dom.yAxisCanvas.height).toBe(Math.round(200 * 1.5))
84
+ expect(dom.yAxisCanvas.style.width).toBe('168px')
85
+ expect(dom.yAxisCanvas.style.height).toBe('200px')
86
+ expect(dom.yAxisOverlayCanvas.width).toBe(dom.yAxisCanvas.width)
89
87
  })
90
88
  })
@@ -41,9 +41,11 @@ function createMockContext(): SubPaneContext & {
41
41
  removeRenderer: vi.fn((name) => renderers.delete(name)),
42
42
  setRendererEnabled: vi.fn(),
43
43
  updateRendererConfig: vi.fn(),
44
- getRightAxisWidth: () => 60,
45
- getPriceLabelWidth: () => 60,
46
- getYPaddingPx: () => 4,
44
+ getOption: () => ({
45
+ rightAxisWidth: 60,
46
+ priceLabelWidth: 60,
47
+ yPaddingPx: 4,
48
+ }),
47
49
  getCrosshairPos: () => null,
48
50
  getCrosshairPrice: () => null,
49
51
  getActivePaneId: () => null,
@@ -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
@@ -275,17 +293,13 @@ export class Chart {
275
293
  defaultPaneMinHeightPx: o.defaultPaneMinHeightPx,
276
294
  }
277
295
  },
278
- getViewport: () => this.viewportManager.getViewport(),
296
+ viewport: this.kernel.viewport,
279
297
  setKnownPaneIds: (ids) => this.rendererPluginManager.setKnownPaneIds(ids),
280
298
  notifyPaneResize: (paneId, pane) =>
281
299
  this.rendererPluginManager.notifyResize(paneId, wrapPaneInfo(pane)),
282
300
  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
- },
301
+ pane: this.kernel.pane,
302
+ afterCommitLayout: () => this.ensurePaneScaleTypesFromSettings(),
289
303
  })
290
304
 
291
305
  this.alertController = createAlertController()
@@ -300,51 +314,25 @@ export class Chart {
300
314
  kGap: this.kernel.viewport.readonly.kGap(),
301
315
  }
302
316
  },
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
317
  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(),
318
+ viewport: this.kernel.viewport,
319
+ comparison: this.kernel.comparison,
328
320
  scheduleDraw: (level) => this.scheduleDraw(level),
329
321
  resetInteraction: () => this.interaction.reset(),
330
322
  getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
331
323
  isPointerDown: () => this.interaction.isPointerDown(),
332
324
  onTimeShareDataReady: (dataLength) => {
333
- const vp = this.viewportManager.computeViewport()
325
+ const vp = this.getViewport()
334
326
  if (!vp || vp.plotWidth <= 0) return
335
327
  const result = this._activeMode.computeKWidth(dataLength, vp.plotWidth, vp.dpr)
336
328
  if (result) {
337
329
  this.applyRenderState(result.kWidth, result.kGap)
338
330
  const leftBuffer = this.getLeftLoadBufferWidth()
339
- this.viewportManager.setScrollLeft(leftBuffer)
331
+ this.kernel.viewport.actions.scrollTo(leftBuffer)
340
332
  }
341
333
  },
342
334
  onDataProcessed: (data, range) => this.evaluateAlerts(data, range),
343
335
  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
336
  },
349
337
  this.kernel.data,
350
338
  this.kernel.dataManager,
@@ -352,22 +340,16 @@ export class Chart {
352
340
 
353
341
  this.zoomController = new ChartZoomController(
354
342
  {
355
- getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
356
- getCurrentDpr: () => this.viewportManager.getEffectiveDpr(),
343
+ viewport: this.kernel.viewport,
344
+ options: this.kernel.options,
345
+ period$: this.kernel.dataManager.readonly.currentPeriod,
357
346
  getClientWidth: () =>
358
- this.viewportManager.getViewport()?.viewWidth ?? this.dom.container?.clientWidth ?? 0,
347
+ this.getViewport()?.viewWidth ?? this.dom.container?.clientWidth ?? 0,
359
348
  getDataLength: () => this.dataManager.getData().length,
360
349
  getPlotWidth: () => this.getLeftLoadBufferWidth(),
361
- setScrollLeft: (v) => {
362
- this.viewportManager.setScrollLeft(v)
363
- },
364
350
  onChange: () => {
365
351
  this.scheduleDraw()
366
352
  },
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
353
  },
372
354
  this.kernel.zoom,
373
355
  )
@@ -387,8 +369,9 @@ export class Chart {
387
369
  removeRenderer: (name) => this.removeRenderer(name),
388
370
  updateRendererConfig: (name, config) => this.updateRendererConfig(name, config),
389
371
  setRendererEnabled: (name, enabled) => this.setRendererEnabled(name, enabled),
390
- getPaneRatiosSignal: () =>
391
- this.kernel.pane.readonly.paneRatios as ReadonlySignal<Readonly<Record<string, number>>>,
372
+ paneRatios$: this.kernel.pane.readonly.paneRatios as ReadonlySignal<
373
+ Readonly<Record<string, number>>
374
+ >,
392
375
  paneSpecs$: this.kernel.pane.readonly.paneSpecs,
393
376
  projectPaneLayout: (specs, ratios) => {
394
377
  this.layoutManager.projectState(specs, ratios)
@@ -405,24 +388,17 @@ export class Chart {
405
388
  getLayer: (id) => this.renderer?.getScene()?.getLayer(id) ?? null,
406
389
  setLayerVisibility: (id, visible) =>
407
390
  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(),
391
+ indicator: this.kernel.indicator,
392
+ subPaneOps: {
393
+ entries: this.kernel.subPane.readonly.entries,
394
+ create: (paneId, indicatorId, params) =>
395
+ this.kernel.actions.createSubPane(paneId, indicatorId, params),
396
+ remove: (paneId) => this.kernel.actions.removeSubPane(paneId),
397
+ replace: (paneId, indicatorId, params) =>
398
+ this.kernel.actions.replaceSubPane(paneId, indicatorId, params),
399
+ setParams: (paneId, params) => this.kernel.actions.updateSubPaneParams(paneId, params),
400
+ clear: () => this.kernel.actions.clearSubPanes(),
401
+ },
426
402
  runRendererTransaction: (run) => this.runRuntimeProjection(run),
427
403
  })
428
404
 
@@ -456,10 +432,10 @@ export class Chart {
456
432
  getSceneRenderer: () => this.rendererHost.renderer,
457
433
  getPluginHost: () => this.pluginHost,
458
434
  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,
435
+ theme$: this.kernel.effectiveTheme$,
436
+ zoom: this.kernel.zoom,
437
+ options: this.kernel.options,
438
+ viewport: this.kernel.viewport,
463
439
  getDataManager: () => this.dataManager,
464
440
  getIndicatorManager: () => this.indicatorManager,
465
441
  getActiveMode: () => this._activeMode,
@@ -468,6 +444,9 @@ export class Chart {
468
444
  drawings$: this.kernel.drawing.readonly.drawings,
469
445
  selectedDrawingId$: this.kernel.drawing.readonly.selectedDrawingId,
470
446
  getOverlay: () => this.drawingSession?.getPaintOverlay() ?? [],
447
+ onLegendContext: (ctx) => {
448
+ this._legendTemplateContext.set(ctx)
449
+ },
471
450
  })
472
451
  this.renderer.registerDrawingPlugins()
473
452
  this.renderer.initCoreRenderers()
@@ -476,7 +455,8 @@ export class Chart {
476
455
  }
477
456
 
478
457
  getViewport(): Viewport | null {
479
- return this.viewportManager.getViewport()
458
+ if (this.kernel.viewport.readonly.viewWidth.peek() === 0) return null
459
+ return this.kernel.viewport.readonly.viewport.peek()
480
460
  }
481
461
 
482
462
  /** 获取当前活跃的模式处理器 */
@@ -572,7 +552,7 @@ export class Chart {
572
552
  }
573
553
 
574
554
  getCurrentDpr(): number {
575
- return this.viewportManager.getEffectiveDpr()
555
+ return this.kernel.viewport.readonly.dpr.peek()
576
556
  }
577
557
 
578
558
  /** 获取当前周期 */
@@ -580,25 +560,6 @@ export class Chart {
580
560
  return this.dataManager.currentPeriod
581
561
  }
582
562
 
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
563
  /** 获取插件宿主 */
603
564
  get plugin(): PluginHostImpl {
604
565
  return this.pluginHost
@@ -1211,14 +1172,14 @@ export class Chart {
1211
1172
  resize() {
1212
1173
  if (this._activeMode === this._timeShareMode) {
1213
1174
  const tsData = this.dataManager.getTimeShareData()
1214
- const vp = this.viewportManager.computeViewport()
1175
+ const vp = this.getViewport()
1215
1176
  if (!vp || vp.plotWidth <= 0) return
1216
1177
  if (tsData.length > 0) {
1217
1178
  const result = this._activeMode.computeKWidth(tsData.length, vp.plotWidth, vp.dpr)
1218
1179
  if (result) {
1219
1180
  this.applyRenderState(result.kWidth, result.kGap)
1220
1181
  const leftBuffer = this.getLeftLoadBufferWidth()
1221
- this.viewportManager.setScrollLeft(leftBuffer)
1182
+ this.kernel.viewport.actions.scrollTo(leftBuffer)
1222
1183
  }
1223
1184
  }
1224
1185
  this.renderer.clearCachedFrame()
@@ -1226,7 +1187,7 @@ export class Chart {
1226
1187
  this.scheduleDraw()
1227
1188
  return
1228
1189
  }
1229
- const vp = this.viewportManager.computeViewport()
1190
+ const vp = this.getViewport()
1230
1191
  // 防御性检查:容器尺寸无效时跳过布局
1231
1192
  if (!vp || vp.viewWidth < 10 || vp.viewHeight < 10) {
1232
1193
  return
@@ -1241,22 +1202,27 @@ export class Chart {
1241
1202
  * @param level 更新级别,默认为 All
1242
1203
  */
1243
1204
  scheduleDraw(level: UpdateLevel = UpdateLevel.All): void {
1205
+ // 正在批量安装/卸载 renderer(pane 重排、指标切换、子图增删),等全部完成再 flush
1244
1206
  if (this.runtimeProjectionDepth > 0) {
1245
- this.runtimeProjectionDrawPending = true
1207
+ this.pendingProjectionLevel = this.pendingProjectionLevel
1208
+ ? mergeUpdateLevel(this.pendingProjectionLevel, level)
1209
+ : level
1246
1210
  return
1247
1211
  }
1248
1212
  this.renderer.scheduleDraw(level)
1249
1213
  }
1250
1214
 
1215
+ /** 在 RendererPluginManager 事务内执行 run,推迟期间的所有 scheduleDraw 到最外层统一 flush */
1251
1216
  private runRuntimeProjection(run: () => void): void {
1252
1217
  this.runtimeProjectionDepth++
1253
1218
  try {
1254
1219
  this.rendererPluginManager.transaction(run)
1255
1220
  } finally {
1256
1221
  this.runtimeProjectionDepth--
1257
- if (this.runtimeProjectionDepth === 0 && this.runtimeProjectionDrawPending) {
1258
- this.runtimeProjectionDrawPending = false
1259
- this.renderer?.scheduleDraw(UpdateLevel.All)
1222
+ if (this.runtimeProjectionDepth === 0 && this.pendingProjectionLevel !== null) {
1223
+ const level = this.pendingProjectionLevel
1224
+ this.pendingProjectionLevel = null
1225
+ this.renderer?.scheduleDraw(level)
1260
1226
  }
1261
1227
  }
1262
1228
  }
@@ -1310,10 +1276,6 @@ export class Chart {
1310
1276
  await this.pluginHost.destroy()
1311
1277
  }
1312
1278
 
1313
- private computeViewport(): Viewport | null {
1314
- return this.viewportManager.computeViewport()
1315
- }
1316
-
1317
1279
  // ==================== Facade API (High-level interface for adapters) ====================
1318
1280
 
1319
1281
  /** interactionSnapshot lazy computed for the createChartController facade */
@@ -1329,7 +1291,7 @@ export class Chart {
1329
1291
 
1330
1292
  /** 视口状态信号 */
1331
1293
  get viewport(): ReadonlySignal<ViewportState> {
1332
- return this.viewportManager.viewportSignal
1294
+ return this.kernel.viewport.readonly.viewportState
1333
1295
  }
1334
1296
 
1335
1297
  /** 数据信号 */
@@ -1359,12 +1321,12 @@ export class Chart {
1359
1321
 
1360
1322
  /** 比较商品颜色信号 */
1361
1323
  get comparisonColors(): ReadonlySignal<ReadonlyMap<string, string>> {
1362
- return this.dataManager.comparisonColors
1324
+ return this.kernel.comparison.readonly.colors
1363
1325
  }
1364
1326
 
1365
1327
  /** 比较商品加载信号 */
1366
1328
  get comparisonLoading(): ReadonlySignal<boolean> {
1367
- return this.dataManager.comparisonLoading
1329
+ return this.kernel.comparison.readonly.loading
1368
1330
  }
1369
1331
 
1370
1332
  /** 生效主题信号(settings.theme + systemTheme 推导) */
@@ -1414,6 +1376,11 @@ export class Chart {
1414
1376
  return this._interactionSnapshot
1415
1377
  }
1416
1378
 
1379
+ /** 主图左上角图例模板上下文(null 表示无数据) */
1380
+ get legendTemplateContext(): ReadonlySignal<LegendTemplateContext | null> {
1381
+ return this._legendTemplateContext
1382
+ }
1383
+
1417
1384
  // ---------- Data ----------
1418
1385
 
1419
1386
  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', () => {