@363045841yyt/klinechart-core 0.8.1-alpha.4 → 0.8.2

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 (167) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +21 -1
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +6 -1
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.js +3 -3
  7. package/dist/data-fetchers/baostock.js.map +1 -1
  8. package/dist/data-fetchers/dataBuffer.d.ts +5 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +85 -48
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/gotdx.d.ts +3 -0
  13. package/dist/data-fetchers/gotdx.d.ts.map +1 -0
  14. package/dist/data-fetchers/gotdx.js +101 -0
  15. package/dist/data-fetchers/gotdx.js.map +1 -0
  16. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  17. package/dist/data-fetchers/hundred-mock.js +28 -5
  18. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  19. package/dist/data-fetchers/index.d.ts +1 -0
  20. package/dist/data-fetchers/index.d.ts.map +1 -1
  21. package/dist/data-fetchers/index.js +1 -0
  22. package/dist/data-fetchers/index.js.map +1 -1
  23. package/dist/data-fetchers/router.d.ts.map +1 -1
  24. package/dist/data-fetchers/router.js +3 -0
  25. package/dist/data-fetchers/router.js.map +1 -1
  26. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  27. package/dist/data-fetchers/thousand-mock.js +24 -14
  28. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  29. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  30. package/dist/data-fetchers/tradingview.js +12 -6
  31. package/dist/data-fetchers/tradingview.js.map +1 -1
  32. package/dist/engine/chart.d.ts +29 -367
  33. package/dist/engine/chart.d.ts.map +1 -1
  34. package/dist/engine/chart.js +239 -1842
  35. package/dist/engine/chart.js.map +1 -1
  36. package/dist/engine/chartContext.d.ts +24 -0
  37. package/dist/engine/chartContext.d.ts.map +1 -0
  38. package/dist/engine/chartContext.js +19 -0
  39. package/dist/engine/chartContext.js.map +1 -0
  40. package/dist/engine/chartTypes.d.ts +77 -0
  41. package/dist/engine/chartTypes.d.ts.map +1 -0
  42. package/dist/engine/chartTypes.js +2 -0
  43. package/dist/engine/chartTypes.js.map +1 -0
  44. package/dist/engine/data/chartDataManager.d.ts +103 -0
  45. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  46. package/dist/engine/data/chartDataManager.js +593 -0
  47. package/dist/engine/data/chartDataManager.js.map +1 -0
  48. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  49. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  50. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  51. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  52. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  53. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  54. package/dist/engine/layout/chartPaneLayout.js +388 -0
  55. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  56. package/dist/engine/render/chartRenderer.d.ts +86 -0
  57. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  58. package/dist/engine/render/chartRenderer.js +440 -0
  59. package/dist/engine/render/chartRenderer.js.map +1 -0
  60. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  61. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  62. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  63. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  64. package/dist/engine/renderers/comparisonLine.js +25 -11
  65. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  66. package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
  67. package/dist/engine/renderers/timeAxis.js +1 -0
  68. package/dist/engine/renderers/timeAxis.js.map +1 -1
  69. package/dist/engine/subPaneManager.d.ts +27 -6
  70. package/dist/engine/subPaneManager.d.ts.map +1 -1
  71. package/dist/engine/subPaneManager.js +54 -56
  72. package/dist/engine/subPaneManager.js.map +1 -1
  73. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  74. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  75. package/dist/engine/utils/chartZoomController.js +66 -0
  76. package/dist/engine/utils/chartZoomController.js.map +1 -0
  77. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  78. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  79. package/dist/engine/viewport/chartViewportManager.js +249 -0
  80. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/plugin/types.d.ts +3 -0
  86. package/dist/plugin/types.d.ts.map +1 -1
  87. package/dist/plugin/types.js.map +1 -1
  88. package/dist/semantic/index.d.ts +1 -1
  89. package/dist/semantic/index.d.ts.map +1 -1
  90. package/dist/semantic/index.js.map +1 -1
  91. package/dist/semantic/schema.json +1 -1
  92. package/dist/semantic/types.d.ts +2 -1
  93. package/dist/semantic/types.d.ts.map +1 -1
  94. package/dist/tokens/theme-china.d.ts.map +1 -1
  95. package/dist/tokens/theme-china.js +0 -4
  96. package/dist/tokens/theme-china.js.map +1 -1
  97. package/dist/tokens/theme-dark.d.ts.map +1 -1
  98. package/dist/tokens/theme-dark.js +0 -4
  99. package/dist/tokens/theme-dark.js.map +1 -1
  100. package/dist/tokens/theme-light.d.ts.map +1 -1
  101. package/dist/tokens/theme-light.js +1 -5
  102. package/dist/tokens/theme-light.js.map +1 -1
  103. package/dist/tokens/types.d.ts +0 -4
  104. package/dist/tokens/types.d.ts.map +1 -1
  105. package/dist/types/price.d.ts +2 -0
  106. package/dist/types/price.d.ts.map +1 -1
  107. package/dist/types/price.js.map +1 -1
  108. package/dist/utils/dateFormat.d.ts +25 -0
  109. package/dist/utils/dateFormat.d.ts.map +1 -1
  110. package/dist/utils/dateFormat.js +78 -0
  111. package/dist/utils/dateFormat.js.map +1 -1
  112. package/dist/utils/kLineDraw/axis.d.ts +2 -0
  113. package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
  114. package/dist/utils/kLineDraw/axis.js +11 -6
  115. package/dist/utils/kLineDraw/axis.js.map +1 -1
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.d.ts.map +1 -1
  118. package/dist/version.js +1 -1
  119. package/dist/version.js.map +1 -1
  120. package/package.json +1 -1
  121. package/src/controllers/createChartController.ts +39 -10
  122. package/src/controllers/types.ts +6 -1
  123. package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
  124. package/src/data-fetchers/baostock.ts +3 -3
  125. package/src/data-fetchers/dataBuffer.ts +70 -23
  126. package/src/data-fetchers/gotdx.ts +138 -0
  127. package/src/data-fetchers/hundred-mock.ts +35 -5
  128. package/src/data-fetchers/index.ts +1 -0
  129. package/src/data-fetchers/router.ts +3 -0
  130. package/src/data-fetchers/thousand-mock.ts +30 -14
  131. package/src/data-fetchers/tradingview.ts +12 -6
  132. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  133. package/src/engine/chart.ts +252 -2250
  134. package/src/engine/chartContext.ts +34 -0
  135. package/src/engine/chartTypes.ts +88 -0
  136. package/src/engine/data/chartDataManager.ts +695 -0
  137. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  138. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  139. package/src/engine/layout/chartPaneLayout.ts +474 -0
  140. package/src/engine/render/chartRenderer.ts +581 -0
  141. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  142. package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
  143. package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
  144. package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
  145. package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
  146. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
  147. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
  148. package/src/engine/renderers/comparisonLine.ts +25 -11
  149. package/src/engine/renderers/timeAxis.ts +1 -0
  150. package/src/engine/subPaneManager.ts +75 -59
  151. package/src/engine/utils/chartZoomController.ts +104 -0
  152. package/src/engine/viewport/chartViewportManager.ts +310 -0
  153. package/src/index.ts +1 -0
  154. package/src/plugin/types.ts +3 -0
  155. package/src/semantic/index.ts +1 -0
  156. package/src/semantic/schema.json +1 -1
  157. package/src/semantic/types.ts +3 -1
  158. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  159. package/src/tokens/theme-china.ts +0 -4
  160. package/src/tokens/theme-dark.ts +0 -4
  161. package/src/tokens/theme-light.ts +2 -6
  162. package/src/tokens/types.ts +0 -4
  163. package/src/types/price.ts +2 -0
  164. package/src/utils/dateFormat.ts +85 -0
  165. package/src/utils/kLineDraw/axis.ts +13 -6
  166. package/src/version.ts +1 -1
  167. package/src/engine/chart.d.ts +0 -626
@@ -0,0 +1,474 @@
1
+ import { Pane, UpdateLevel } from './pane'
2
+ import type { PaneRole } from '../../plugin'
3
+ import { PaneRenderer } from '../paneRenderer'
4
+ import type { SharedWebGLSurface } from '../renderers/webgl/sharedWebGLSurface'
5
+ import type { ChartDom, PaneSpec, Viewport } from '../chartTypes'
6
+
7
+ export interface PaneLayoutDependencies {
8
+ getDom: () => ChartDom
9
+ getOption: () => {
10
+ rightAxisWidth: number
11
+ yPaddingPx: number
12
+ priceLabelWidth?: number
13
+ paneGap?: number
14
+ defaultPaneMinHeightPx?: number
15
+ }
16
+ getViewport: () => Viewport | null
17
+ getSharedWebGLSurface: () => SharedWebGLSurface
18
+ setKnownPaneIds: (ids: string[]) => void
19
+ notifyPaneResize: (paneId: string, pane: Pane) => void
20
+ scheduleDraw: (level?: UpdateLevel) => void
21
+ onLayoutChange: (ratios: Record<string, number>, specs: PaneSpec[]) => void
22
+ }
23
+
24
+ export class ChartPaneLayout {
25
+ private deps: PaneLayoutDependencies
26
+ private paneRenderers: PaneRenderer[] = []
27
+ private _internalPaneRatios: Map<string, number> = new Map()
28
+ private _paneSpecs: PaneSpec[]
29
+
30
+ constructor(initialPaneSpecs: PaneSpec[], deps: PaneLayoutDependencies) {
31
+ this.deps = deps
32
+ this._paneSpecs = initialPaneSpecs.map((s) => ({ ...s }))
33
+ this.syncPaneRatiosFromSpecs(this._paneSpecs)
34
+ this.initPanes()
35
+ }
36
+
37
+ getPaneRenderers(): PaneRenderer[] {
38
+ return this.paneRenderers
39
+ }
40
+
41
+ getPaneSpecs(): PaneSpec[] {
42
+ return this._paneSpecs
43
+ }
44
+
45
+ getInternalPaneRatios(): Map<string, number> {
46
+ return this._internalPaneRatios
47
+ }
48
+
49
+ setInternalPaneRatio(paneId: string, ratio: number): void {
50
+ this._internalPaneRatios.set(paneId, ratio)
51
+ }
52
+
53
+ deleteInternalPaneRatio(paneId: string): void {
54
+ this._internalPaneRatios.delete(paneId)
55
+ }
56
+
57
+ private resolvePaneRole(spec: PaneSpec, index: number): PaneRole {
58
+ if (spec.role) return spec.role
59
+ return index === 0 ? 'price' : 'indicator'
60
+ }
61
+
62
+ private initPanes() {
63
+ const prevScaleTypes = new Map<string, 'linear' | 'log' | 'percent'>()
64
+ for (const r of this.paneRenderers) {
65
+ prevScaleTypes.set(r.getPane().id, r.getPane().yAxis.getScaleType())
66
+ }
67
+
68
+ this.paneRenderers = this._paneSpecs.map((spec, index) => {
69
+ const pane = new Pane(spec.id, {
70
+ role: this.resolvePaneRole(spec, index),
71
+ capabilities: spec.capabilities,
72
+ })
73
+
74
+ const prev = prevScaleTypes.get(spec.id)
75
+ if (prev) pane.yAxis.setScaleType(prev)
76
+
77
+ const mainCanvas = document.createElement('canvas')
78
+ const overlayCanvas = document.createElement('canvas')
79
+ const yAxisCanvas = document.createElement('canvas')
80
+
81
+ const isMain = pane.role === 'price'
82
+
83
+ mainCanvas.id = `${spec.id}-main`
84
+ mainCanvas.className = isMain ? 'main-canvas main' : 'main-canvas sub'
85
+ mainCanvas.style.position = 'absolute'
86
+ mainCanvas.style.left = '0'
87
+ mainCanvas.style.top = '0'
88
+
89
+ overlayCanvas.id = `${spec.id}-overlay`
90
+ overlayCanvas.className = 'overlay-canvas'
91
+ overlayCanvas.style.position = 'absolute'
92
+ overlayCanvas.style.left = '0'
93
+ overlayCanvas.style.top = '0'
94
+ overlayCanvas.style.pointerEvents = 'none'
95
+ overlayCanvas.style.backgroundColor = 'transparent'
96
+
97
+ yAxisCanvas.id = `${spec.id}-yAxis`
98
+ yAxisCanvas.className = 'right-axis'
99
+ yAxisCanvas.style.position = 'absolute'
100
+ yAxisCanvas.style.left = '0'
101
+
102
+ const renderer = new PaneRenderer(
103
+ { mainCanvas, overlayCanvas, yAxisCanvas },
104
+ pane,
105
+ {
106
+ rightAxisWidth: this.deps.getOption().rightAxisWidth,
107
+ yPaddingPx: this.deps.getOption().yPaddingPx,
108
+ priceLabelWidth: this.deps.getOption().priceLabelWidth,
109
+ },
110
+ this.deps.getSharedWebGLSurface(),
111
+ )
112
+
113
+ return renderer
114
+ })
115
+
116
+ const dom = this.deps.getDom()
117
+ const canvasLayer = dom.canvasLayer
118
+ const rightAxisLayer = dom.rightAxisLayer
119
+ if (canvasLayer) {
120
+ const existingCanvases = canvasLayer.querySelectorAll('canvas:not(.x-axis-canvas)')
121
+ existingCanvases.forEach((canvas) => canvas.remove())
122
+ }
123
+ if (rightAxisLayer) {
124
+ const existingAxisCanvases = rightAxisLayer.querySelectorAll('canvas.right-axis')
125
+ existingAxisCanvases.forEach((canvas) => canvas.remove())
126
+ }
127
+
128
+ this.paneRenderers.forEach((renderer) => {
129
+ const domEls = renderer.getDom()
130
+ canvasLayer.appendChild(domEls.mainCanvas)
131
+ canvasLayer.appendChild(domEls.overlayCanvas)
132
+ rightAxisLayer.appendChild(domEls.yAxisCanvas)
133
+ })
134
+
135
+ this.deps.setKnownPaneIds(
136
+ this.paneRenderers.map((renderer) => renderer.getPane().id),
137
+ )
138
+
139
+ this._paneSpecs = this._paneSpecs.map((spec, index) => ({
140
+ ...spec,
141
+ role: this.paneRenderers[index]?.getPane().role ?? spec.role,
142
+ }))
143
+ }
144
+
145
+ private syncPaneRatiosFromSpecs(specs: PaneSpec[]): void {
146
+ const next = new Map<string, number>()
147
+ for (const spec of specs) {
148
+ const prev = this._internalPaneRatios.get(spec.id)
149
+ const incoming = Number.isFinite(spec.ratio) ? spec.ratio : 0
150
+ const ratio = prev !== undefined ? prev : (incoming > 0 ? incoming : 1)
151
+ next.set(spec.id, ratio)
152
+ }
153
+ this._internalPaneRatios = next
154
+ this.normalizeVisiblePaneRatios(specs)
155
+ this.syncPaneRatiosToSpecs()
156
+ }
157
+
158
+ private syncPaneRatiosToSpecs(): void {
159
+ const visible = this._paneSpecs.filter((p) => p.visible !== false)
160
+ const visibleSum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0)
161
+ const safeVisibleSum = visibleSum > 0 ? visibleSum : 1
162
+
163
+ this._paneSpecs = this._paneSpecs.map((spec) => {
164
+ const ratio = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
165
+ if (spec.visible === false) {
166
+ return { ...spec, ratio }
167
+ }
168
+ return { ...spec, ratio: ratio / safeVisibleSum }
169
+ })
170
+ }
171
+
172
+ private normalizeVisiblePaneRatios(specs: PaneSpec[]): void {
173
+ const visible = specs.filter((p) => p.visible !== false)
174
+ if (visible.length === 0) return
175
+
176
+ let sum = 0
177
+ for (const spec of visible) {
178
+ const raw = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
179
+ const safe = Number.isFinite(raw) && raw > 0 ? raw : 0
180
+ this._internalPaneRatios.set(spec.id, safe)
181
+ sum += safe
182
+ }
183
+
184
+ if (sum <= 0) {
185
+ const equal = 1 / visible.length
186
+ for (const spec of visible) {
187
+ this._internalPaneRatios.set(spec.id, equal)
188
+ }
189
+ return
190
+ }
191
+
192
+ for (const spec of visible) {
193
+ const v = this._internalPaneRatios.get(spec.id) ?? 0
194
+ this._internalPaneRatios.set(spec.id, v / sum)
195
+ }
196
+ }
197
+
198
+ private getPaneMinHeight(spec: PaneSpec, plotHeight: number): number {
199
+ const fallback = this.deps.getOption().defaultPaneMinHeightPx ?? 120
200
+ const raw = spec.minHeightPx ?? fallback
201
+ return Math.max(1, Math.min(Math.round(raw), Math.max(1, plotHeight)))
202
+ }
203
+
204
+ private computePaneHeightsByRatio(visibleSpecs: PaneSpec[], availableH: number): number[] {
205
+ if (visibleSpecs.length === 0) return []
206
+
207
+ const ratios = visibleSpecs.map((spec) => this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0)
208
+ const ratioSum = ratios.reduce((s, r) => s + (r > 0 ? r : 0), 0)
209
+ const safeRatios = ratioSum > 0
210
+ ? ratios.map((r) => (r > 0 ? r : 0) / ratioSum)
211
+ : visibleSpecs.map(() => 1 / visibleSpecs.length)
212
+
213
+ const heights = safeRatios.map((r) => Math.max(1, Math.round(availableH * r)))
214
+ const mins = visibleSpecs.map((spec) => this.getPaneMinHeight(spec, availableH))
215
+
216
+ for (let i = 0; i < heights.length; i++) {
217
+ heights[i] = Math.max(heights[i]!, Math.min(mins[i]!, availableH))
218
+ }
219
+
220
+ let total = heights.reduce((s, h) => s + h, 0)
221
+
222
+ if (total > availableH) {
223
+ let overflow = total - availableH
224
+ while (overflow > 0) {
225
+ let shrunk = false
226
+ for (let i = heights.length - 1; i >= 0 && overflow > 0; i--) {
227
+ const minH = Math.max(1, Math.min(mins[i]!, availableH))
228
+ const h = heights[i]!
229
+ if (h > minH) {
230
+ heights[i] = h - 1
231
+ overflow--
232
+ shrunk = true
233
+ }
234
+ }
235
+ if (!shrunk) break
236
+ }
237
+ } else if (total < availableH) {
238
+ heights[heights.length - 1] = (heights[heights.length - 1] ?? 1) + (availableH - total)
239
+ }
240
+
241
+ total = heights.reduce((s, h) => s + h, 0)
242
+ if (total !== availableH && heights.length > 0) {
243
+ heights[heights.length - 1] = Math.max(1, (heights[heights.length - 1] ?? 1) + (availableH - total))
244
+ }
245
+
246
+ return heights
247
+ }
248
+
249
+ layoutPanes() {
250
+ const vp = this.deps.getViewport()
251
+ if (!vp) return
252
+
253
+ const visibleSpecs = this._paneSpecs.filter((p) => p.visible !== false)
254
+ if (visibleSpecs.length === 0) return
255
+
256
+ const opt = this.deps.getOption()
257
+ const gap = Math.max(0, opt.paneGap ?? 0)
258
+ let y = 0
259
+
260
+ const totalGaps = gap * Math.max(0, visibleSpecs.length - 1)
261
+ const availableH = Math.max(1, vp.plotHeight - totalGaps)
262
+
263
+ this.normalizeVisiblePaneRatios(visibleSpecs)
264
+ const paneHeights = this.computePaneHeightsByRatio(visibleSpecs, availableH)
265
+
266
+ for (let i = 0; i < visibleSpecs.length; i++) {
267
+ const spec = visibleSpecs[i]
268
+ if (!spec) continue
269
+
270
+ const renderer = this.paneRenderers.find((r) => r.getPane().id === spec.id)
271
+ if (!renderer) continue
272
+
273
+ const pane = renderer.getPane()
274
+ const h = paneHeights[i] ?? 1
275
+
276
+ pane.setLayout(y, h)
277
+ pane.setPadding(opt.yPaddingPx, opt.yPaddingPx)
278
+
279
+ renderer.resize(vp.plotWidth, h, vp.dpr)
280
+ renderer.setWebGLRegion({
281
+ x: 0,
282
+ y,
283
+ width: vp.plotWidth,
284
+ height: h,
285
+ dpr: vp.dpr,
286
+ })
287
+ this.deps.notifyPaneResize(pane.id, pane)
288
+ const domEls = renderer.getDom()
289
+ domEls.mainCanvas.style.top = `${y}px`
290
+ domEls.overlayCanvas.style.top = `${y}px`
291
+ domEls.yAxisCanvas.style.top = `${y}px`
292
+ domEls.yAxisCanvas.style.left = '0px'
293
+
294
+ y += h + gap
295
+ }
296
+
297
+ const finalAvailable = Math.max(1, availableH)
298
+ for (const spec of visibleSpecs) {
299
+ const renderer = this.paneRenderers.find((r) => r.getPane().id === spec.id)
300
+ if (!renderer) continue
301
+ const h = renderer.getPane().height
302
+ this._internalPaneRatios.set(spec.id, h / finalAvailable)
303
+ }
304
+ this.normalizeVisiblePaneRatios(visibleSpecs)
305
+ this.syncPaneRatiosToSpecs()
306
+ this.emitLayoutChange()
307
+ }
308
+
309
+ getPaneLayoutSpecs(): PaneSpec[] {
310
+ const visible = this._paneSpecs.filter((p) => p.visible !== false)
311
+ const sum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0)
312
+ const safeSum = sum > 0 ? sum : 1
313
+ return this._paneSpecs.map((spec) => {
314
+ const base = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
315
+ const ratio = spec.visible === false ? base : base / safeSum
316
+ const pane = this.paneRenderers.find((r) => r.getPane().id === spec.id)?.getPane()
317
+ return {
318
+ ...spec,
319
+ ratio,
320
+ role: pane?.role ?? spec.role,
321
+ capabilities: pane ? { ...pane.capabilities } : spec.capabilities,
322
+ }
323
+ })
324
+ }
325
+
326
+ private emitLayoutChange(): void {
327
+ const ratios: Record<string, number> = {}
328
+ this._internalPaneRatios.forEach((ratio, id) => {
329
+ ratios[id] = ratio
330
+ })
331
+ this.deps.onLayoutChange(ratios, this.getPaneLayoutSpecs())
332
+ }
333
+
334
+ applyPaneLayoutSpecs(panes: PaneSpec[]): void {
335
+ this._paneSpecs = panes.map((spec) => ({ ...spec }))
336
+ this.syncPaneRatiosFromSpecs(this._paneSpecs)
337
+ this.initPanes()
338
+ this.layoutPanes()
339
+ this.deps.scheduleDraw()
340
+ }
341
+
342
+ updatePaneLayout(panes: PaneSpec[]): void {
343
+ this._internalPaneRatios.clear()
344
+ this.applyPaneLayoutSpecs(panes)
345
+ }
346
+
347
+ setPaneDefinitions(defs: PaneSpec[]): void {
348
+ this.applyPaneLayoutSpecs(defs)
349
+ }
350
+
351
+ upsertPane(def: PaneSpec): void {
352
+ const idx = this._paneSpecs.findIndex((pane) => pane.id === def.id)
353
+ if (idx === -1) {
354
+ this.applyPaneLayoutSpecs([...this._paneSpecs, { ...def }])
355
+ return
356
+ }
357
+
358
+ const next = [...this._paneSpecs]
359
+ next[idx] = { ...next[idx], ...def }
360
+ this.applyPaneLayoutSpecs(next)
361
+ }
362
+
363
+ removePaneDefinition(paneId: string): void {
364
+ if (!this._paneSpecs.some((pane) => pane.id === paneId)) return
365
+ this._internalPaneRatios.delete(paneId)
366
+ this.applyPaneLayoutSpecs(this._paneSpecs.filter((pane) => pane.id !== paneId))
367
+ }
368
+
369
+ addPane(paneId: string): void {
370
+ if (this._paneSpecs.some((spec) => spec.id === paneId)) {
371
+ console.warn(`Pane "${paneId}" already exists`)
372
+ return
373
+ }
374
+
375
+ const hasPricePane = this._paneSpecs.some((spec, index) => this.resolvePaneRole(spec, index) === 'price')
376
+ const role: PaneRole = hasPricePane ? 'indicator' : 'price'
377
+ this.applyPaneLayoutSpecs([
378
+ ...this._paneSpecs,
379
+ { id: paneId, ratio: 1, visible: true, role },
380
+ ])
381
+ }
382
+
383
+ removePane(paneId: string): void {
384
+ if (!this._paneSpecs.some((spec) => spec.id === paneId)) return
385
+
386
+ const next = this._paneSpecs.filter((spec) => spec.id !== paneId)
387
+ this._internalPaneRatios.delete(paneId)
388
+ this.applyPaneLayoutSpecs(next)
389
+ }
390
+
391
+ hasPane(paneId: string): boolean {
392
+ return this._paneSpecs.some((spec) => spec.id === paneId)
393
+ }
394
+
395
+ resizePaneBoundary(upperPaneId: string, deltaY: number): boolean {
396
+ if (!Number.isFinite(deltaY) || deltaY === 0) return false
397
+ const vp = this.deps.getViewport()
398
+ if (!vp) return false
399
+
400
+ const visibleSpecs = this._paneSpecs.filter((p) => p.visible !== false)
401
+ const boundaryIndex = visibleSpecs.findIndex((p) => p.id === upperPaneId)
402
+ if (boundaryIndex < 0 || boundaryIndex >= visibleSpecs.length - 1) return false
403
+
404
+ const upperSpec = visibleSpecs[boundaryIndex]
405
+ const lowerSpec = visibleSpecs[boundaryIndex + 1]
406
+ if (!upperSpec || !lowerSpec) return false
407
+
408
+ const heights = new Map<string, number>()
409
+ for (const spec of visibleSpecs) {
410
+ const renderer = this.paneRenderers.find((r) => r.getPane().id === spec.id)
411
+ if (renderer) {
412
+ heights.set(spec.id, renderer.getPane().height)
413
+ }
414
+ }
415
+
416
+ const expandIdx = deltaY > 0 ? boundaryIndex : boundaryIndex + 1
417
+ const shrinkIdx = deltaY > 0 ? boundaryIndex + 1 : boundaryIndex
418
+ const expandDir = deltaY > 0 ? -1 : 1
419
+ const shrinkDir = deltaY > 0 ? 1 : -1
420
+
421
+ let remaining = Math.abs(deltaY)
422
+
423
+ let shrinkCursor = shrinkIdx
424
+ while (remaining > 0 && shrinkCursor >= 0 && shrinkCursor < visibleSpecs.length) {
425
+ const spec = visibleSpecs[shrinkCursor]
426
+ if (!spec) break
427
+
428
+ const currentH = heights.get(spec.id) ?? 0
429
+ const minH = this.getPaneMinHeight(spec, vp.plotHeight)
430
+ const canShrink = Math.max(0, currentH - minH)
431
+
432
+ if (canShrink > 0) {
433
+ const shrink = Math.min(canShrink, remaining)
434
+ heights.set(spec.id, currentH - shrink)
435
+ remaining -= shrink
436
+ }
437
+
438
+ if (remaining > 0) {
439
+ shrinkCursor += shrinkDir
440
+ }
441
+ }
442
+
443
+ if (remaining > 0) return false
444
+
445
+ const expandSpec = visibleSpecs[expandIdx]
446
+ if (!expandSpec) return false
447
+ const expandCurrentH = heights.get(expandSpec.id) ?? 0
448
+ heights.set(expandSpec.id, expandCurrentH + Math.abs(deltaY))
449
+
450
+ const opt = this.deps.getOption()
451
+ const gap = Math.max(0, opt.paneGap ?? 0)
452
+ const totalGaps = gap * Math.max(0, visibleSpecs.length - 1)
453
+ const availableH = Math.max(1, vp.plotHeight - totalGaps)
454
+
455
+ for (const spec of visibleSpecs) {
456
+ const h = heights.get(spec.id) ?? 0
457
+ this._internalPaneRatios.set(spec.id, h / availableH)
458
+ }
459
+
460
+ this.normalizeVisiblePaneRatios(visibleSpecs)
461
+ this.syncPaneRatiosToSpecs()
462
+
463
+ this.layoutPanes()
464
+ this.deps.scheduleDraw()
465
+ return true
466
+ }
467
+
468
+ destroy(): void {
469
+ this.paneRenderers.forEach((r) => r.destroy())
470
+ this.paneRenderers = []
471
+ this._internalPaneRatios.clear()
472
+ this._paneSpecs = []
473
+ }
474
+ }