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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +9 -8
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/index.d.ts +1 -0
  5. package/dist/controllers/index.d.ts.map +1 -1
  6. package/dist/controllers/index.js.map +1 -1
  7. package/dist/controllers/types.d.ts +5 -0
  8. package/dist/controllers/types.d.ts.map +1 -1
  9. package/dist/data/binance.d.ts +1 -1
  10. package/dist/data/binance.d.ts.map +1 -1
  11. package/dist/data/binance.js +1 -1
  12. package/dist/data/binance.js.map +1 -1
  13. package/dist/data/timeShareBuffer.d.ts +3 -0
  14. package/dist/data/timeShareBuffer.d.ts.map +1 -1
  15. package/dist/data/timeShareBuffer.js +13 -0
  16. package/dist/data/timeShareBuffer.js.map +1 -1
  17. package/dist/engine/chart.d.ts +9 -9
  18. package/dist/engine/chart.d.ts.map +1 -1
  19. package/dist/engine/chart.js +87 -101
  20. package/dist/engine/chart.js.map +1 -1
  21. package/dist/engine/chartTypes.d.ts +5 -0
  22. package/dist/engine/chartTypes.d.ts.map +1 -1
  23. package/dist/engine/controller/interaction.d.ts +4 -2
  24. package/dist/engine/controller/interaction.d.ts.map +1 -1
  25. package/dist/engine/controller/interaction.js +18 -10
  26. package/dist/engine/controller/interaction.js.map +1 -1
  27. package/dist/engine/data/chartDataManager.d.ts +14 -18
  28. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  29. package/dist/engine/data/chartDataManager.js +55 -37
  30. package/dist/engine/data/chartDataManager.js.map +1 -1
  31. package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
  32. package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
  33. package/dist/engine/data/incrementalLoadHint.js +5 -2
  34. package/dist/engine/data/incrementalLoadHint.js.map +1 -1
  35. package/dist/engine/data/scrollCompensator.d.ts +3 -13
  36. package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
  37. package/dist/engine/data/scrollCompensator.js +13 -14
  38. package/dist/engine/data/scrollCompensator.js.map +1 -1
  39. package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
  40. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
  41. package/dist/engine/indicators/chartIndicatorManager.js +37 -37
  42. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
  43. package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
  44. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
  45. package/dist/engine/layout/chartPaneLayout.js +58 -16
  46. package/dist/engine/layout/chartPaneLayout.js.map +1 -1
  47. package/dist/engine/modes/timeShareMath.d.ts +56 -0
  48. package/dist/engine/modes/timeShareMath.d.ts.map +1 -0
  49. package/dist/engine/modes/timeShareMath.js +90 -0
  50. package/dist/engine/modes/timeShareMath.js.map +1 -0
  51. package/dist/engine/modes/timeShareMode.d.ts +5 -0
  52. package/dist/engine/modes/timeShareMode.d.ts.map +1 -1
  53. package/dist/engine/modes/timeShareMode.js +27 -27
  54. package/dist/engine/modes/timeShareMode.js.map +1 -1
  55. package/dist/engine/paneRenderer.d.ts +2 -0
  56. package/dist/engine/paneRenderer.d.ts.map +1 -1
  57. package/dist/engine/paneRenderer.js +15 -5
  58. package/dist/engine/paneRenderer.js.map +1 -1
  59. package/dist/engine/render/chartRenderer.d.ts +28 -22
  60. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  61. package/dist/engine/render/chartRenderer.js +143 -85
  62. package/dist/engine/render/chartRenderer.js.map +1 -1
  63. package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
  64. package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
  65. package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
  66. package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
  67. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
  68. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
  69. package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
  70. package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
  71. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
  72. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
  73. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
  74. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
  75. package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
  76. package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
  77. package/dist/engine/render/layers/yAxisLayer.js +11 -2
  78. package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
  79. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
  80. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  81. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
  82. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  83. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +73 -0
  84. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
  85. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +182 -0
  86. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
  87. package/dist/engine/renderers/leftYAxis.d.ts +15 -2
  88. package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
  89. package/dist/engine/renderers/leftYAxis.js +56 -16
  90. package/dist/engine/renderers/leftYAxis.js.map +1 -1
  91. package/dist/engine/renderers/timeShare.d.ts.map +1 -1
  92. package/dist/engine/renderers/timeShare.js +20 -9
  93. package/dist/engine/renderers/timeShare.js.map +1 -1
  94. package/dist/engine/renderers/yAxis.d.ts +16 -6
  95. package/dist/engine/renderers/yAxis.d.ts.map +1 -1
  96. package/dist/engine/renderers/yAxis.js +49 -16
  97. package/dist/engine/renderers/yAxis.js.map +1 -1
  98. package/dist/engine/state/viewportState.d.ts +4 -1
  99. package/dist/engine/state/viewportState.d.ts.map +1 -1
  100. package/dist/engine/state/viewportState.js +40 -8
  101. package/dist/engine/state/viewportState.js.map +1 -1
  102. package/dist/engine/subPaneManager.d.ts +5 -3
  103. package/dist/engine/subPaneManager.d.ts.map +1 -1
  104. package/dist/engine/subPaneManager.js +3 -2
  105. package/dist/engine/subPaneManager.js.map +1 -1
  106. package/dist/engine/utils/chartZoomController.d.ts +9 -7
  107. package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
  108. package/dist/engine/utils/chartZoomController.js +13 -12
  109. package/dist/engine/utils/chartZoomController.js.map +1 -1
  110. package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
  111. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
  112. package/dist/engine/viewport/chartViewportManager.js +7 -48
  113. package/dist/engine/viewport/chartViewportManager.js.map +1 -1
  114. package/dist/engine/viewport/viewport.d.ts +14 -0
  115. package/dist/engine/viewport/viewport.d.ts.map +1 -1
  116. package/dist/engine/viewport/viewport.js +10 -0
  117. package/dist/engine/viewport/viewport.js.map +1 -1
  118. package/dist/foundation/config/chartSettings.d.ts +0 -5
  119. package/dist/foundation/config/chartSettings.d.ts.map +1 -1
  120. package/dist/foundation/config/chartSettings.js +14 -0
  121. package/dist/foundation/config/chartSettings.js.map +1 -1
  122. package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
  123. package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
  124. package/dist/foundation/plugin/types.d.ts +3 -0
  125. package/dist/foundation/plugin/types.d.ts.map +1 -1
  126. package/dist/foundation/plugin/types.js.map +1 -1
  127. package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
  128. package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
  129. package/dist/foundation/reactivity/frameTransaction.js +17 -2
  130. package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
  131. package/dist/foundation/utils/kLineDraw/axis.d.ts +3 -0
  132. package/dist/foundation/utils/kLineDraw/axis.d.ts.map +1 -1
  133. package/dist/foundation/utils/kLineDraw/axis.js +40 -9
  134. package/dist/foundation/utils/kLineDraw/axis.js.map +1 -1
  135. package/dist/foundation/utils/sessionTimeLabels.d.ts +59 -0
  136. package/dist/foundation/utils/sessionTimeLabels.d.ts.map +1 -0
  137. package/dist/foundation/utils/sessionTimeLabels.js +170 -0
  138. package/dist/foundation/utils/sessionTimeLabels.js.map +1 -0
  139. package/dist/foundation/utils/timeShareAxisLabels.d.ts +27 -0
  140. package/dist/foundation/utils/timeShareAxisLabels.d.ts.map +1 -0
  141. package/dist/foundation/utils/timeShareAxisLabels.js +45 -0
  142. package/dist/foundation/utils/timeShareAxisLabels.js.map +1 -0
  143. package/dist/rendering/render/Renderer.d.ts +58 -74
  144. package/dist/rendering/render/Renderer.d.ts.map +1 -1
  145. package/dist/rendering/render/Renderer.js +8 -18
  146. package/dist/rendering/render/Renderer.js.map +1 -1
  147. package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
  148. package/dist/rendering/render/createWebGPURenderer.js +30 -17
  149. package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
  150. package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
  151. package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
  152. package/dist/rendering/render/physicalLine.d.ts +4 -0
  153. package/dist/rendering/render/physicalLine.d.ts.map +1 -0
  154. package/dist/rendering/render/physicalLine.js +37 -0
  155. package/dist/rendering/render/physicalLine.js.map +1 -0
  156. package/dist/rendering/render/physicalRegion.d.ts +14 -0
  157. package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
  158. package/dist/rendering/render/physicalRegion.js +15 -0
  159. package/dist/rendering/render/physicalRegion.js.map +1 -0
  160. package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
  161. package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
  162. package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
  163. package/dist/rendering/scene/createScene.d.ts.map +1 -1
  164. package/dist/rendering/scene/createScene.js +4 -3
  165. package/dist/rendering/scene/createScene.js.map +1 -1
  166. package/dist/version.d.ts +1 -1
  167. package/dist/version.js +1 -1
  168. package/package.json +1 -1
  169. package/src/__tests__/stateKernel.test.ts +1 -0
  170. package/src/controllers/createChartController.ts +9 -8
  171. package/src/controllers/index.ts +11 -0
  172. package/src/controllers/types.ts +7 -0
  173. package/src/data/__tests__/timeShareBuffer.test.ts +45 -0
  174. package/src/data/binance.ts +1 -1
  175. package/src/data/timeShareBuffer.ts +15 -0
  176. package/src/engine/__tests__/chart.dpr.test.ts +31 -2
  177. package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
  178. package/src/engine/__tests__/subPaneManager.test.ts +5 -3
  179. package/src/engine/chart.ts +105 -115
  180. package/src/engine/chartTypes.ts +5 -0
  181. package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
  182. package/src/engine/controller/interaction.ts +18 -10
  183. package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
  184. package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
  185. package/src/engine/data/chartDataManager.ts +64 -59
  186. package/src/engine/data/incrementalLoadHint.ts +8 -7
  187. package/src/engine/data/scrollCompensator.ts +16 -24
  188. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
  189. package/src/engine/indicators/chartIndicatorManager.ts +62 -62
  190. package/src/engine/layout/chartPaneLayout.ts +73 -21
  191. package/src/engine/modes/__tests__/timeShareMath.test.ts +185 -0
  192. package/src/engine/modes/__tests__/timeShareMode.test.ts +81 -0
  193. package/src/engine/modes/timeShareMath.ts +150 -0
  194. package/src/engine/modes/timeShareMode.ts +35 -28
  195. package/src/engine/paneRenderer.ts +17 -10
  196. package/src/engine/render/chartRenderer.ts +214 -145
  197. package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
  198. package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
  199. package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
  200. package/src/engine/render/layers/yAxisLayer.ts +29 -7
  201. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
  202. package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +290 -0
  203. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
  204. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
  205. package/src/engine/renderers/leftYAxis.ts +60 -18
  206. package/src/engine/renderers/timeShare.ts +25 -9
  207. package/src/engine/renderers/yAxis.ts +57 -22
  208. package/src/engine/state/__tests__/settingsState.test.ts +9 -0
  209. package/src/engine/state/viewportState.ts +44 -8
  210. package/src/engine/subPaneManager.ts +8 -5
  211. package/src/engine/utils/chartZoomController.ts +22 -19
  212. package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
  213. package/src/engine/viewport/chartViewportManager.ts +9 -60
  214. package/src/engine/viewport/viewport.ts +14 -0
  215. package/src/foundation/config/chartSettings.ts +13 -0
  216. package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
  217. package/src/foundation/plugin/types.ts +3 -0
  218. package/src/foundation/reactivity/frameTransaction.ts +20 -6
  219. package/src/foundation/utils/__tests__/sessionTimeLabels.test.ts +91 -0
  220. package/src/foundation/utils/kLineDraw/axis.ts +50 -9
  221. package/src/foundation/utils/sessionTimeLabels.ts +237 -0
  222. package/src/foundation/utils/timeShareAxisLabels.ts +94 -0
  223. package/src/rendering/render/Renderer.ts +69 -85
  224. package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
  225. package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
  226. package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
  227. package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
  228. package/src/rendering/render/createWebGPURenderer.ts +30 -17
  229. package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
  230. package/src/rendering/render/physicalLine.ts +45 -0
  231. package/src/rendering/render/physicalRegion.ts +32 -0
  232. package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
  233. package/src/rendering/scene/createScene.ts +4 -3
  234. package/src/version.ts +1 -1
@@ -1,37 +1,37 @@
1
1
  import type { RendererPlugin, RenderContext } from '../../foundation/plugin/index'
2
2
  import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../foundation/plugin/index'
3
3
  import { resolveThemeColors } from '../../foundation/tokens/index'
4
- import type { KLineData } from '../../foundation/types/price'
5
4
  import { drawCrosshairPriceLabel, drawAxisPriceLabel } from '../../foundation/utils/kLineDraw/axis'
6
5
  import { roundToPhysicalPixel } from '../../foundation/utils/pixelAlign'
7
6
  import { getFont, setCanvasFont } from '../../foundation/tokens/fonts'
8
7
 
9
- /**
10
- * 创建 Y 轴渲染器插件
11
- * 按 pane capability 决定是否绘制刻度与价格标签
12
- */
13
- export function createYAxisRendererPlugin(options: {
8
+ type YAxisOptions = {
14
9
  axisWidth: number
15
10
  yPaddingPx: number
16
11
  getCrosshair?: () => { y: number; price: number; activePaneId: string | null } | null
17
- }): RendererPlugin {
12
+ }
13
+
14
+ function shouldShowRightAxis(period: string, settings: RenderContext['settings']): boolean {
15
+ if (period === 'timeshare') return true
16
+ const rightType = settings?.rightAxisType as string | undefined
17
+ return rightType !== 'none'
18
+ }
19
+
20
+ /**
21
+ * Y 轴静态层:刻度 + 价格范围带,画到 yAxisCtx(main 级刷新)
22
+ */
23
+ export function createYAxisStaticRendererPlugin(options: YAxisOptions): RendererPlugin {
18
24
  return {
19
25
  name: 'yAxis',
20
- version: '1.0.0',
21
- description: 'Y轴价格刻度渲染器',
22
- debugName: 'Y',
26
+ version: '2.0.0',
27
+ description: 'Y轴价格刻度渲染器(静态)',
28
+ debugName: 'Y轴刻度',
23
29
  paneId: GLOBAL_PANE_ID,
24
30
  priority: RENDERER_PRIORITY.SYSTEM_YAXIS,
25
- layer: 'overlay',
26
31
 
27
32
  draw(context: RenderContext) {
28
- const { ctx, pane, dpr, yAxisCtx, data, period } = context
29
-
30
- // 分时模式始终显示右轴,不受设置约束
31
- if (period !== 'timeshare') {
32
- const rightType = context.settings?.rightAxisType as string | undefined
33
- if (rightType === 'none') return
34
- }
33
+ const { ctx, pane, dpr, yAxisCtx, period } = context
34
+ if (!shouldShowRightAxis(period, context.settings)) return
35
35
 
36
36
  const tokenColors = resolveThemeColors(
37
37
  context.theme,
@@ -39,11 +39,8 @@ export function createYAxisRendererPlugin(options: {
39
39
  context.colorPresetSettings,
40
40
  )
41
41
  const scaleType = pane.yAxis.getScaleType()
42
-
43
42
  const targetCtx = yAxisCtx || ctx
44
43
  const axisWidth = yAxisCtx?.canvas ? yAxisCtx.canvas.width / dpr : options.axisWidth
45
- const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
46
-
47
44
  const isPercent = scaleType === 'percent' && pane.role === 'price'
48
45
 
49
46
  if (pane.capabilities.showPriceAxisTicks && context.yAxisTicks) {
@@ -70,7 +67,7 @@ export function createYAxisRendererPlugin(options: {
70
67
  }
71
68
  }
72
69
 
73
- // 绘制价格范围带(先于标签,使标签覆盖在范围带之上)
70
+ // 价格范围带(先于标签,使标签覆盖在范围带之上)
74
71
  if (context.yAxisRanges && pane.role === 'price') {
75
72
  for (const range of context.yAxisRanges) {
76
73
  const topY = range.topY + pane.top
@@ -83,6 +80,36 @@ export function createYAxisRendererPlugin(options: {
83
80
  targetCtx.restore()
84
81
  }
85
82
  }
83
+ },
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Y 轴动态层:yAxisLabels + 十字线价签,画到 yAxisOverlayCtx(overlay 级刷新)
89
+ */
90
+ export function createYAxisOverlayRendererPlugin(options: YAxisOptions): RendererPlugin {
91
+ return {
92
+ name: 'yAxisOverlay',
93
+ version: '2.0.0',
94
+ description: 'Y轴动态标签渲染器',
95
+ debugName: 'Y轴标签',
96
+ paneId: GLOBAL_PANE_ID,
97
+ priority: RENDERER_PRIORITY.SYSTEM_YAXIS + 1,
98
+ layer: 'overlay',
99
+
100
+ draw(context: RenderContext) {
101
+ const { pane, dpr, yAxisOverlayCtx, yAxisCtx, period } = context
102
+ if (!shouldShowRightAxis(period, context.settings)) return
103
+
104
+ const targetCtx = yAxisOverlayCtx ?? yAxisCtx
105
+ if (!targetCtx) return
106
+
107
+ const axisWidth = targetCtx.canvas ? targetCtx.canvas.width / dpr : options.axisWidth
108
+ targetCtx.clearRect(0, 0, axisWidth, pane.height)
109
+
110
+ const scaleType = pane.yAxis.getScaleType()
111
+ const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
112
+ const isPercent = scaleType === 'percent' && pane.role === 'price'
86
113
 
87
114
  // 绘制来自 yAxisLabels 的标签(最新价格、极值点、绘图锚点等)
88
115
  if (context.yAxisLabels && pane.role === 'price') {
@@ -151,3 +178,11 @@ export function createYAxisRendererPlugin(options: {
151
178
  },
152
179
  }
153
180
  }
181
+
182
+ /**
183
+ * @deprecated 使用 createYAxisStaticRendererPlugin + createYAxisOverlayRendererPlugin
184
+ * 保留兼容:静态+动态合画到 yAxisCtx
185
+ */
186
+ export function createYAxisRendererPlugin(options: YAxisOptions): RendererPlugin {
187
+ return createYAxisStaticRendererPlugin(options)
188
+ }
@@ -43,4 +43,13 @@ describe('settingsState', () => {
43
43
  ;(snap as { showGridLines?: boolean }).showGridLines = false
44
44
  }).toThrow()
45
45
  })
46
+
47
+ it('preserves extension keys like preClose through resolve/patch', () => {
48
+ expect(resolveSettings({ preClose: 12.34 }).preClose).toBe(12.34)
49
+ const s = createSettingsState({ preClose: 12.34 })
50
+ expect(s.readonly.settings.peek().preClose).toBe(12.34)
51
+ s.actions.patch({ showGridLines: false })
52
+ expect(s.readonly.settings.peek().preClose).toBe(12.34)
53
+ expect(s.readonly.settings.peek().showGridLines).toBe(false)
54
+ })
46
55
  })
@@ -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
  *
@@ -158,6 +158,11 @@ export type ChartSettings = {
158
158
  * @param partial - 偏好的部分设置(通常是组件 prop 传入)
159
159
  * @returns 合并后的 ChartSettings 对象
160
160
  */
161
+ const KNOWN_SETTING_KEYS = new Set<string>([
162
+ ...DEFAULT_SETTINGS.map((item) => item.key),
163
+ 'colorPresetSettings',
164
+ ])
165
+
161
166
  export function resolveSettings(partial?: Partial<ChartSettings>): ChartSettings {
162
167
  // 用 Partial<_SettingByKey> 而非 ChartSettings 避免交叉类型索引赋值报错
163
168
  const result: Partial<_SettingByKey> = {}
@@ -170,6 +175,14 @@ export function resolveSettings(partial?: Partial<ChartSettings>): ChartSettings
170
175
  ;(result as ChartSettings).colorPresetSettings = normalizeColorPresetSettings(
171
176
  partial?.colorPresetSettings,
172
177
  )
178
+ // 保留扩展字段(如 preClose),避免业务元数据被 resolve 清掉
179
+ if (partial) {
180
+ for (const [key, value] of Object.entries(partial)) {
181
+ if (KNOWN_SETTING_KEYS.has(key)) continue
182
+ if (value === undefined) continue
183
+ ;(result as Record<string, unknown>)[key] = value
184
+ }
185
+ }
173
186
  return result as ChartSettings
174
187
  }
175
188
 
@@ -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 等动态内容) */