@363045841yyt/klinechart-core 0.7.5-alpha.2 → 0.7.6

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 (66) hide show
  1. package/README.md +8 -8
  2. package/README.zh-CN.md +8 -8
  3. package/dist/controllers/createChartController.d.ts.map +1 -1
  4. package/dist/controllers/createChartController.js +145 -21
  5. package/dist/controllers/createChartController.js.map +1 -1
  6. package/dist/controllers/index.d.ts +10 -1
  7. package/dist/controllers/index.d.ts.map +1 -1
  8. package/dist/controllers/index.js +10 -0
  9. package/dist/controllers/index.js.map +1 -1
  10. package/dist/controllers/types.d.ts +65 -8
  11. package/dist/controllers/types.d.ts.map +1 -1
  12. package/dist/engine/chart.d.ts +13 -31
  13. package/dist/engine/chart.d.ts.map +1 -1
  14. package/dist/engine/chart.js +120 -140
  15. package/dist/engine/chart.js.map +1 -1
  16. package/dist/engine/controller/interaction.d.ts +1 -1
  17. package/dist/engine/controller/interaction.d.ts.map +1 -1
  18. package/dist/engine/controller/interaction.js +10 -2
  19. package/dist/engine/controller/interaction.js.map +1 -1
  20. package/dist/engine/drawing/interaction.d.ts +3 -3
  21. package/dist/engine/drawing/interaction.d.ts.map +1 -1
  22. package/dist/engine/drawing/interaction.js +38 -46
  23. package/dist/engine/drawing/interaction.js.map +1 -1
  24. package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
  25. package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
  26. package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
  27. package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
  28. package/dist/engine/renderers/paneTitle.d.ts +5 -24
  29. package/dist/engine/renderers/paneTitle.d.ts.map +1 -1
  30. package/dist/engine/renderers/paneTitle.js +10 -5
  31. package/dist/engine/renderers/paneTitle.js.map +1 -1
  32. package/dist/engine/renderers/webgl/candleSurface.d.ts +4 -4
  33. package/dist/engine/renderers/webgl/candleSurface.d.ts.map +1 -1
  34. package/dist/engine/renderers/webgl/candleSurface.js +36 -56
  35. package/dist/engine/renderers/webgl/candleSurface.js.map +1 -1
  36. package/dist/engine/subPaneManager.d.ts +6 -0
  37. package/dist/engine/subPaneManager.d.ts.map +1 -1
  38. package/dist/engine/subPaneManager.js +38 -1
  39. package/dist/engine/subPaneManager.js.map +1 -1
  40. package/dist/semantic/controller.d.ts +1 -2
  41. package/dist/semantic/controller.d.ts.map +1 -1
  42. package/dist/semantic/index.d.ts +1 -1
  43. package/dist/semantic/index.d.ts.map +1 -1
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.d.ts.map +1 -1
  46. package/dist/version.js +1 -1
  47. package/dist/version.js.map +1 -1
  48. package/package.json +6 -6
  49. package/src/controllers/createChartController.ts +158 -29
  50. package/src/controllers/index.ts +34 -0
  51. package/src/controllers/types.ts +79 -8
  52. package/src/engine/chart.ts +133 -154
  53. package/src/engine/controller/interaction.ts +9 -2
  54. package/src/engine/drawing/interaction.ts +38 -47
  55. package/src/engine/renderers/Indicator/indicatorData.ts +1 -1
  56. package/src/engine/renderers/paneTitle.ts +16 -25
  57. package/src/engine/renderers/webgl/candleSurface.ts +40 -56
  58. package/src/engine/subPaneManager.ts +43 -1
  59. package/src/semantic/controller.ts +1 -1
  60. package/src/semantic/index.ts +1 -1
  61. package/src/version.ts +1 -1
  62. package/dist/engine/chart-store.d.ts +0 -75
  63. package/dist/engine/chart-store.d.ts.map +0 -1
  64. package/dist/engine/chart-store.js +0 -88
  65. package/dist/engine/chart-store.js.map +0 -1
  66. package/src/engine/chart-store.ts +0 -121
@@ -1,6 +1,6 @@
1
1
  import type { KLineData } from '../types/price'
2
2
  import type { ChartSettings } from '../config/chartSettings'
3
- import { createSignal, type Signal } from '../reactivity/signal'
3
+ import { createSignal, computed, type Signal, type Computed } from '../reactivity/signal'
4
4
  import { getVisibleRange } from './viewport/viewport'
5
5
  import { Pane, type VisibleRange, UpdateLevel } from './layout/pane'
6
6
  import { InteractionController, type InteractionSnapshot } from './controller/interaction'
@@ -9,7 +9,6 @@ import { PaneRenderer } from './paneRenderer'
9
9
  import { SharedWebGLSurface } from './renderers/webgl/sharedWebGLSurface'
10
10
  import { MarkerManager, type CustomMarkerEntity } from './marker/registry'
11
11
  import { getPhysicalKLineConfig, calcKWidthPx } from './utils/klineConfig'
12
- import { computeContentWidth } from './chart-store'
13
12
  import { computeZoom, computeZoomToLevel, type ZoomConfig } from './utils/zoom'
14
13
  import { IndicatorScheduler } from './indicators/scheduler'
15
14
  import { getRegisteredIndicatorDefinitions } from './indicators/indicatorDefinitionRegistry'
@@ -165,6 +164,11 @@ type FrameData = {
165
164
  useCachedFrame: boolean
166
165
  }
167
166
 
167
+ /** 主图指标条目,存在 = 激活 */
168
+ interface MainIndicatorEntry {
169
+ params: Record<string, number | boolean | string>
170
+ }
171
+
168
172
  export class Chart {
169
173
  private dom: ChartDom
170
174
  private opt: ResolvedChartOptions
@@ -209,21 +213,12 @@ export class Chart {
209
213
  /** pane ratio 状态(按 paneId 维护,sum=1 仅对可见 pane) */
210
214
  private _internalPaneRatios: Map<string, number> = new Map()
211
215
 
212
- /** 视口变化回调(供外部同步 DPR/尺寸) */
213
- private onViewportChange?: (viewport: Viewport) => void
214
-
215
216
  /** 共享 X 轴上下文缓存 */
216
217
  private xAxisCtx: CanvasRenderingContext2D | null = null
217
218
 
218
219
  /** Chart 级共享 WebGL canvas/context */
219
220
  private sharedWebGLSurface: SharedWebGLSurface
220
221
 
221
- /** pane 布局回流回调(Chart -> UI 单向) */
222
- private onPaneLayoutChange?: (panes: PaneSpec[]) => void
223
-
224
- /** 数据变化回调(供外部同步 dataLength) */
225
- private onDataChange?: (data: KLineData[]) => void
226
-
227
222
  /** 当前缩放级别(1 ~ zoomLevelCount) */
228
223
  private currentZoomLevel: number = 1
229
224
 
@@ -249,13 +244,13 @@ export class Chart {
249
244
  } | null = null
250
245
 
251
246
  /** 副图管理器 */
252
- private subPaneManager: SubPaneManager
247
+ private subPaneManager = new SubPaneManager()
253
248
 
254
- /** 当前激活的主图指标列表(如 ['boll', 'ma']) */
255
- private activeMainIndicators: Set<string> = new Set()
249
+ /** 主图指标激活状态与参数(存在即激活,默认参数在 enable 时初始化) */
250
+ private _mainIndicatorsSignal: Signal<Map<string, MainIndicatorEntry>> = createSignal<Map<string, MainIndicatorEntry>>(new Map())
256
251
 
257
- /** 主图指标参数配置 */
258
- private mainIndicatorParams: Record<string, Record<string, number | boolean | string>> = {
252
+ /** 主图指标默认参数 */
253
+ private static DEFAULT_MAIN_PARAMS: Record<string, Record<string, number | boolean | string>> = {
259
254
  MA: { ma5: true, ma10: true, ma20: true, ma30: true, ma60: true },
260
255
  BOLL: { period: 20, multiplier: 2, showUpper: true, showMiddle: true, showLower: true, showBand: true },
261
256
  EXPMA: { fastPeriod: 12, slowPeriod: 50 },
@@ -289,22 +284,26 @@ export class Chart {
289
284
  return false
290
285
  }
291
286
 
292
- if (this.activeMainIndicators.has(id)) {
287
+ const map = this._mainIndicatorsSignal.peek()
288
+ const existing = map.get(id)
289
+
290
+ if (existing) {
293
291
  // 已启用,更新参数
294
292
  if (params) {
295
- this.mainIndicatorParams[id] = { ...this.mainIndicatorParams[id], ...params }
293
+ const next = new Map(map)
294
+ next.set(id, { params: { ...existing.params, ...params } })
295
+ this._mainIndicatorsSignal.set(next)
296
296
  this.updateIndicatorSchedulerConfig(id)
297
- this.syncIndicatorsSignal()
298
297
  }
299
298
  return true
300
299
  }
301
300
 
302
- this.activeMainIndicators.add(id)
303
-
304
301
  // 合并默认参数和传入参数
305
- if (params) {
306
- this.mainIndicatorParams[id] = { ...this.mainIndicatorParams[id], ...params }
307
- }
302
+ const defaults = Chart.DEFAULT_MAIN_PARAMS[id] ?? {}
303
+ const merged = params ? { ...defaults, ...params } : defaults
304
+ const next = new Map(map)
305
+ next.set(id, { params: merged })
306
+ this._mainIndicatorsSignal.set(next)
308
307
 
309
308
  // 启用对应的渲染器
310
309
  this.enableMainIndicatorRenderer(id)
@@ -313,7 +312,6 @@ export class Chart {
313
312
  this.updateIndicatorSchedulerConfig(id)
314
313
 
315
314
  this.scheduleDraw()
316
- this.syncIndicatorsSignal()
317
315
  return true
318
316
  }
319
317
 
@@ -324,9 +322,12 @@ export class Chart {
324
322
  */
325
323
  disableMainIndicator(indicatorId: string): boolean {
326
324
  const id = indicatorId.toUpperCase()
327
- if (!this.activeMainIndicators.has(id)) return false
325
+ const map = this._mainIndicatorsSignal.peek()
326
+ if (!map.has(id)) return false
328
327
 
329
- this.activeMainIndicators.delete(id)
328
+ const next = new Map(map)
329
+ next.delete(id)
330
+ this._mainIndicatorsSignal.set(next)
330
331
 
331
332
  // 禁用对应的渲染器
332
333
  this.disableMainIndicatorRenderer(id)
@@ -335,7 +336,6 @@ export class Chart {
335
336
  this.updateIndicatorSchedulerConfig(id)
336
337
 
337
338
  this.scheduleDraw()
338
- this.syncIndicatorsSignal()
339
339
  return true
340
340
  }
341
341
 
@@ -357,7 +357,7 @@ export class Chart {
357
357
  * @returns 激活的指标ID数组
358
358
  */
359
359
  getActiveMainIndicators(): string[] {
360
- return Array.from(this.activeMainIndicators)
360
+ return [...this._mainIndicatorsSignal.peek().keys()]
361
361
  }
362
362
 
363
363
  /**
@@ -365,7 +365,7 @@ export class Chart {
365
365
  * @param indicatorId 指标ID
366
366
  */
367
367
  isMainIndicatorActive(indicatorId: string): boolean {
368
- return this.activeMainIndicators.has(indicatorId.toUpperCase())
368
+ return this._mainIndicatorsSignal.peek().has(indicatorId.toUpperCase())
369
369
  }
370
370
 
371
371
  /**
@@ -375,22 +375,25 @@ export class Chart {
375
375
  */
376
376
  updateMainIndicatorParams(indicatorId: string, params: Record<string, number | boolean | string>): void {
377
377
  const id = indicatorId.toUpperCase()
378
- if (!this.mainIndicatorParams[id]) {
379
- this.mainIndicatorParams[id] = {}
380
- }
381
- this.mainIndicatorParams[id] = { ...this.mainIndicatorParams[id], ...params }
378
+ const map = this._mainIndicatorsSignal.peek()
379
+ const entry = map.get(id)
380
+ if (!entry) return
381
+
382
+ const merged = { ...entry.params, ...params }
383
+ const next = new Map(map)
384
+ next.set(id, { params: merged })
385
+ this._mainIndicatorsSignal.set(next)
382
386
 
383
387
  // 同步更新渲染器配置
384
388
  const rendererName = id.toLowerCase()
385
389
  const renderer = this.getRenderer(rendererName)
386
390
  if (renderer && renderer.setConfig) {
387
- renderer.setConfig(this.mainIndicatorParams[id])
391
+ renderer.setConfig(merged)
388
392
  }
389
393
 
390
394
  // 更新调度器
391
395
  this.updateIndicatorSchedulerConfig(id)
392
396
  this.scheduleDraw()
393
- this.syncIndicatorsSignal()
394
397
  }
395
398
 
396
399
  /**
@@ -398,19 +401,19 @@ export class Chart {
398
401
  * @param indicatorId 指标ID
399
402
  */
400
403
  getMainIndicatorParams(indicatorId: string): Record<string, number | boolean | string> | null {
401
- return this.mainIndicatorParams[indicatorId.toUpperCase()] ?? null
404
+ return this._mainIndicatorsSignal.peek().get(indicatorId.toUpperCase())?.params ?? null
402
405
  }
403
406
 
404
407
  /**
405
408
  * 清除所有主图指标
406
409
  */
407
410
  clearMainIndicators(): void {
408
- for (const id of this.activeMainIndicators) {
411
+ const map = this._mainIndicatorsSignal.peek()
412
+ for (const id of map.keys()) {
409
413
  this.disableMainIndicatorRenderer(id)
410
414
  }
411
- this.activeMainIndicators.clear()
415
+ this._mainIndicatorsSignal.set(new Map())
412
416
  this.scheduleDraw()
413
- this.syncIndicatorsSignal()
414
417
  }
415
418
 
416
419
  /**
@@ -572,8 +575,9 @@ export class Chart {
572
575
  * 更新调度器配置(内部方法)
573
576
  */
574
577
  private updateIndicatorSchedulerConfig(indicatorId: string): void {
575
- const isActive = this.activeMainIndicators.has(indicatorId)
576
- const params = this.mainIndicatorParams[indicatorId] || {}
578
+ const entry = this._mainIndicatorsSignal.peek().get(indicatorId)
579
+ const isActive = entry !== undefined
580
+ const params = entry?.params ?? {}
577
581
 
578
582
  switch (indicatorId) {
579
583
  case 'MA':
@@ -653,7 +657,7 @@ export class Chart {
653
657
  setActiveMainIndicators(indicators: string[]): void {
654
658
  // 计算需要启用和禁用的指标
655
659
  const newSet = new Set(indicators.map(i => i.toUpperCase()))
656
- const currentSet = new Set(this.activeMainIndicators)
660
+ const currentSet = new Set(this._mainIndicatorsSignal.peek().keys())
657
661
 
658
662
  // 禁用不再激活的
659
663
  for (const id of currentSet) {
@@ -709,14 +713,25 @@ export class Chart {
709
713
  }
710
714
  this.indicatorScheduler.setInvalidateCallback(() => this.scheduleDraw())
711
715
 
712
- // 初始化副图管理器
713
- this.subPaneManager = new SubPaneManager()
714
716
  // 注册副图活跃列表提供者,调度器据此只计算启用的副图
715
717
  this.indicatorScheduler.setActiveSubPaneProvider(
716
718
  () => this.subPaneManager.getPaneIds(),
717
719
  )
718
720
 
719
721
  this.initPanes()
722
+
723
+ // dev: 主副图状态变更日志
724
+ if ((import.meta as any).env?.MODE !== 'production') {
725
+ this._indicatorsComputed.subscribe(() => {
726
+ const instances = this._indicatorsComputed.peek()
727
+ console.log('[Chart] indicators signal changed:', instances)
728
+ })
729
+ this._subPanesComputed.subscribe(() => {
730
+ const subPanes = this._subPanesComputed.peek()
731
+ console.log('[Chart] subPanes signal changed:', subPanes)
732
+ })
733
+ }
734
+
720
735
  // 注册绘图主插件(负责绘制 shape,layer: 'main')
721
736
  this.useRenderer(createDrawingRendererPlugin({ store: this.drawingStore }))
722
737
  // 注册绘图标签插件(负责推送选中绘图的轴标签,layer: 'overlay')
@@ -929,7 +944,7 @@ export class Chart {
929
944
  this.interaction.setKLinePositions(kLinePositions, range, kWidthPx)
930
945
 
931
946
  // 4. 通知调度器当前活跃主图指标 + 获取价格范围
932
- this.indicatorScheduler.setActiveMainIndicators(Array.from(this.activeMainIndicators))
947
+ this.indicatorScheduler.setActiveMainIndicators([...this._mainIndicatorsSignal.peek().keys()])
933
948
  const mainIndicatorRange = useCachedFrame ? null : this.indicatorScheduler.getMainIndicatorPriceRange()
934
949
  const hasCrosshair = this.interaction.getCrosshairIndex() !== null
935
950
 
@@ -1218,21 +1233,6 @@ export class Chart {
1218
1233
  return this.zoomLevelCount
1219
1234
  }
1220
1235
 
1221
- /** 注册视口变化回调 */
1222
- setOnViewportChange(cb: (viewport: Viewport) => void) {
1223
- this.onViewportChange = cb
1224
- }
1225
-
1226
- /** 注册 pane 布局回流回调 */
1227
- setOnPaneLayoutChange(cb: (panes: PaneSpec[]) => void) {
1228
- this.onPaneLayoutChange = cb
1229
- }
1230
-
1231
- /** 注册数据变化回调 */
1232
- setOnDataChange(cb: (data: KLineData[]) => void) {
1233
- this.onDataChange = cb
1234
- }
1235
-
1236
1236
  /** 获取所有 PaneRenderer */
1237
1237
  getPaneRenderers(): PaneRenderer[] {
1238
1238
  return this.paneRenderers
@@ -1410,9 +1410,8 @@ export class Chart {
1410
1410
  ratios[id] = ratio
1411
1411
  })
1412
1412
  this._paneRatiosSignal.set(ratios)
1413
- this.syncSubPanesSignal()
1414
1413
 
1415
- this.onPaneLayoutChange?.(this.getPaneLayoutSpecs())
1414
+ this._paneLayoutSignal.set(this.getPaneLayoutSpecs())
1416
1415
  }
1417
1416
 
1418
1417
  private applyPaneLayoutSpecs(panes: PaneSpec[]): void {
@@ -1586,8 +1585,6 @@ export class Chart {
1586
1585
  this.upsertPane({ id: paneId, ratio: this._internalPaneRatios.get(paneId) ?? 1, visible: true, role: 'indicator' })
1587
1586
 
1588
1587
  const success = this.subPaneManager.create(this, paneId, indicatorId, params ?? this.getDefaultSubPaneParams(indicatorId))
1589
- this.syncIndicatorsSignal()
1590
- this.syncSubPanesSignal()
1591
1588
  return success
1592
1589
  }
1593
1590
 
@@ -1597,9 +1594,6 @@ export class Chart {
1597
1594
  */
1598
1595
  removeSubPane(paneId: string): void {
1599
1596
  this.subPaneManager.remove(this, paneId)
1600
- this._internalPaneRatios.delete(paneId)
1601
- this.syncIndicatorsSignal()
1602
- this.syncSubPanesSignal()
1603
1597
  }
1604
1598
 
1605
1599
  /**
@@ -1610,8 +1604,6 @@ export class Chart {
1610
1604
  */
1611
1605
  replaceSubPaneIndicator(paneId: string, newIndicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
1612
1606
  this.subPaneManager.replaceIndicator(this, paneId, newIndicatorId, params ?? this.getDefaultSubPaneParams(newIndicatorId))
1613
- this.syncIndicatorsSignal()
1614
- this.syncSubPanesSignal()
1615
1607
  }
1616
1608
 
1617
1609
  /**
@@ -1621,7 +1613,6 @@ export class Chart {
1621
1613
  */
1622
1614
  updateSubPaneParams(paneId: string, params: Record<string, unknown>): void {
1623
1615
  this.subPaneManager.updateParams(this, paneId, params)
1624
- this.syncIndicatorsSignal()
1625
1616
  }
1626
1617
 
1627
1618
  /**
@@ -1643,8 +1634,6 @@ export class Chart {
1643
1634
 
1644
1635
  // 更新布局,移除所有副图 pane
1645
1636
  this.applyPaneLayoutSpecs(this.opt.panes.filter((spec) => !subPaneIds.includes(spec.id)))
1646
- this.syncIndicatorsSignal()
1647
- this.syncSubPanesSignal()
1648
1637
  }
1649
1638
 
1650
1639
  /**
@@ -1767,7 +1756,6 @@ export class Chart {
1767
1756
  updateData(data: KLineData[]) {
1768
1757
  this._internalData = data ?? []
1769
1758
  this._dataSignal.set([...this._internalData])
1770
- this.onDataChange?.(this._internalData)
1771
1759
 
1772
1760
  // 重算 DOM scrollLeft 状态, 防止左右滚动超出数据长度范围
1773
1761
  const container = this.dom.container
@@ -1834,13 +1822,16 @@ export class Chart {
1834
1822
 
1835
1823
  /** 获取内容总宽度(用于外部 scroll-content 撑开 scrollWidth) */
1836
1824
  getContentWidth(): number {
1837
- return computeContentWidth({
1838
- dataLength: this._internalData.length,
1839
- kWidth: this.opt.kWidth,
1840
- kGap: this.opt.kGap,
1841
- viewWidth: this._internalViewport?.plotWidth ?? 0,
1842
- viewportDpr: this.getEffectiveDpr(),
1843
- })
1825
+ const dataLength = this._internalData.length
1826
+ if (dataLength === 0) return 0
1827
+ const kWidth = this.opt.kWidth
1828
+ const kGap = this.opt.kGap
1829
+ const viewWidth = this._internalViewport?.plotWidth ?? 0
1830
+ const dpr = this.getEffectiveDpr()
1831
+ const TRAILING_DRAWING_SLOTS = 24
1832
+ const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
1833
+ const dataPlotWidth = (startXPx + (dataLength + TRAILING_DRAWING_SLOTS) * unitPx) / dpr
1834
+ return Math.max(dataPlotWidth, viewWidth)
1844
1835
  }
1845
1836
 
1846
1837
 
@@ -1922,8 +1913,6 @@ export class Chart {
1922
1913
  // 清理渲染器插件管理器(会调用所有 onUninstall)
1923
1914
  this.rendererPluginManager.clear()
1924
1915
 
1925
- this.onViewportChange = undefined
1926
- this.onPaneLayoutChange = undefined
1927
1916
  this.indicatorScheduler.destroy()
1928
1917
  await this.pluginHost.destroy()
1929
1918
  }
@@ -2241,7 +2230,18 @@ export class Chart {
2241
2230
 
2242
2231
  this._internalViewport = vp
2243
2232
  if (viewportChanged) {
2244
- this.onViewportChange?.(vp)
2233
+ const current = this._viewportSignal.peek()
2234
+ this._viewportSignal.set({
2235
+ zoomLevel: current.zoomLevel,
2236
+ plotWidth: vp.plotWidth,
2237
+ plotHeight: vp.plotHeight,
2238
+ dpr: vp.dpr > 0 ? vp.dpr : current.dpr,
2239
+ visibleFrom: current.visibleFrom,
2240
+ visibleTo: current.visibleTo,
2241
+ desiredScrollLeft: current.desiredScrollLeft,
2242
+ kWidth: current.kWidth,
2243
+ kGap: current.kGap,
2244
+ })
2245
2245
  }
2246
2246
  return vp
2247
2247
  }
@@ -2263,11 +2263,10 @@ export class Chart {
2263
2263
 
2264
2264
  private _dataSignal = createSignal<ReadonlyArray<KLineData>>([])
2265
2265
  private _themeSignal = createSignal<'light' | 'dark'>('light')
2266
- private _indicatorsSignal = createSignal<ReadonlyArray<IndicatorInstance>>([])
2267
- private _subPanesSignal = createSignal<ReadonlyArray<SubPaneInfo>>([])
2268
2266
  private _drawingToolSignal = createSignal<DrawingToolType | null>(null)
2269
2267
  private _drawingsSignal = createSignal<ReadonlyArray<import('../plugin').DrawingObject>>([])
2270
2268
  private _paneRatiosSignal = createSignal<Readonly<Record<string, number>>>({})
2269
+ private _paneLayoutSignal = createSignal<PaneSpec[]>([])
2271
2270
  private _interactionSignal = createSignal<InteractionSnapshot>({
2272
2271
  crosshairPos: null,
2273
2272
  crosshairIndex: null,
@@ -2285,6 +2284,38 @@ export class Chart {
2285
2284
  isHoveringRightAxis: false,
2286
2285
  })
2287
2286
 
2287
+ private _indicatorsComputed = computed<ReadonlyArray<IndicatorInstance>>(() => {
2288
+ const mainIndicators: IndicatorInstance[] = [...this._mainIndicatorsSignal().entries()].map(([id, entry]) => ({
2289
+ id,
2290
+ definitionId: id,
2291
+ label: id,
2292
+ name: id,
2293
+ role: 'main' as const,
2294
+ params: { ...entry.params },
2295
+ }))
2296
+
2297
+ const subIndicators: IndicatorInstance[] = this.subPaneManager.entriesSignal().map(entry => ({
2298
+ id: entry.paneId,
2299
+ definitionId: entry.indicatorId,
2300
+ label: entry.indicatorId,
2301
+ name: entry.indicatorId,
2302
+ role: 'sub' as const,
2303
+ paneId: entry.paneId,
2304
+ params: { ...entry.params },
2305
+ }))
2306
+
2307
+ return [...mainIndicators, ...subIndicators]
2308
+ })
2309
+ private _subPanesComputed = computed<ReadonlyArray<SubPaneInfo>>(() => {
2310
+ const ratios = this._paneRatiosSignal()
2311
+ return this.subPaneManager.entriesSignal().map(entry => ({
2312
+ paneId: entry.paneId,
2313
+ indicatorId: entry.indicatorId,
2314
+ params: { ...entry.params },
2315
+ ratio: ratios[entry.paneId] ?? 1,
2316
+ }))
2317
+ })
2318
+
2288
2319
  /** 视口状态信号 */
2289
2320
  get viewport(): Signal<ViewportState> {
2290
2321
  return this._viewportSignal
@@ -2300,14 +2331,14 @@ export class Chart {
2300
2331
  return this._themeSignal
2301
2332
  }
2302
2333
 
2303
- /** 指标实例列表信号 */
2304
- get indicators(): Signal<ReadonlyArray<IndicatorInstance>> {
2305
- return this._indicatorsSignal
2334
+ /** 指标实例列表信号(派生信号,自动随主/副图状态更新) */
2335
+ get indicators(): Computed<ReadonlyArray<IndicatorInstance>> {
2336
+ return this._indicatorsComputed
2306
2337
  }
2307
2338
 
2308
- /** 子图信息信号 */
2309
- get subPanes(): Signal<ReadonlyArray<SubPaneInfo>> {
2310
- return this._subPanesSignal
2339
+ /** 子图信息信号(派生信号,自动随副图条目/比例更新) */
2340
+ get subPanes(): Computed<ReadonlyArray<SubPaneInfo>> {
2341
+ return this._subPanesComputed
2311
2342
  }
2312
2343
 
2313
2344
  /** 当前绘图工具信号 */
@@ -2325,6 +2356,10 @@ export class Chart {
2325
2356
  return this._paneRatiosSignal
2326
2357
  }
2327
2358
 
2359
+ get paneLayout(): Signal<PaneSpec[]> {
2360
+ return this._paneLayoutSignal
2361
+ }
2362
+
2328
2363
  /** 交互状态信号 */
2329
2364
  get interactionState(): Signal<InteractionSnapshot> {
2330
2365
  return this._interactionSignal
@@ -2576,9 +2611,6 @@ export class Chart {
2576
2611
  if (role === 'main') {
2577
2612
  const success = this.enableMainIndicator(definitionId, params as Record<string, number | boolean | string>)
2578
2613
  if (!success) return null
2579
-
2580
- // 更新 indicators signal
2581
- this.syncIndicatorsSignal()
2582
2614
  return definitionId.toUpperCase()
2583
2615
  } else {
2584
2616
  // 副图指标
@@ -2589,10 +2621,6 @@ export class Chart {
2589
2621
  params as Record<string, number | boolean | string>,
2590
2622
  )
2591
2623
  if (!success) return null
2592
-
2593
- // 更新 signals
2594
- this.syncIndicatorsSignal()
2595
- this.syncSubPanesSignal()
2596
2624
  return paneId
2597
2625
  }
2598
2626
  }
@@ -2605,25 +2633,18 @@ export class Chart {
2605
2633
  removeIndicator(instanceId: string): boolean {
2606
2634
  const id = instanceId.toUpperCase()
2607
2635
 
2608
- // 先尝试作为主图指标移除(直接检查内部状态,不依赖 signal)
2609
- if (this.activeMainIndicators.has(id)) {
2610
- const success = this.disableMainIndicator(instanceId)
2611
- if (success) {
2612
- this.syncIndicatorsSignal()
2613
- }
2614
- return success
2636
+ // 先尝试作为主图指标移除
2637
+ if (this._mainIndicatorsSignal.peek().has(id)) {
2638
+ return this.disableMainIndicator(instanceId)
2615
2639
  }
2616
2640
 
2617
- // 再尝试作为副图指标移除(检查 sub pane 是否存在)
2641
+ // 再尝试作为副图指标移除
2618
2642
  const subPaneEntry = this.getSubPaneEntry(instanceId)
2619
2643
  if (subPaneEntry) {
2620
2644
  this.removeSubPane(instanceId)
2621
- this.syncIndicatorsSignal()
2622
- this.syncSubPanesSignal()
2623
2645
  return true
2624
2646
  }
2625
2647
 
2626
- // 都没找到,返回 false
2627
2648
  return false
2628
2649
  }
2629
2650
 
@@ -2636,10 +2657,9 @@ export class Chart {
2636
2657
  updateIndicatorParams(instanceId: string, params: Record<string, unknown>): boolean {
2637
2658
  const id = instanceId.toUpperCase()
2638
2659
 
2639
- // 先尝试作为主图指标更新(直接检查内部状态)
2640
- if (this.activeMainIndicators.has(id)) {
2660
+ // 先尝试作为主图指标更新
2661
+ if (this._mainIndicatorsSignal.peek().has(id)) {
2641
2662
  this.updateMainIndicatorParams(instanceId, params as Record<string, number | boolean | string>)
2642
- this.syncIndicatorsSignal()
2643
2663
  return true
2644
2664
  }
2645
2665
 
@@ -2647,11 +2667,9 @@ export class Chart {
2647
2667
  const subPaneEntry = this.getSubPaneEntry(instanceId)
2648
2668
  if (subPaneEntry) {
2649
2669
  this.updateSubPaneParams(instanceId, params)
2650
- this.syncIndicatorsSignal()
2651
2670
  return true
2652
2671
  }
2653
2672
 
2654
- // 都没找到
2655
2673
  return false
2656
2674
  }
2657
2675
 
@@ -2667,46 +2685,7 @@ export class Chart {
2667
2685
  return false
2668
2686
  }
2669
2687
 
2670
- /**
2671
- * 同步 indicators signal
2672
- */
2673
- private syncIndicatorsSignal(): void {
2674
- const mainIndicators: IndicatorInstance[] = this.getActiveMainIndicators().map(id => ({
2675
- id,
2676
- definitionId: id,
2677
- label: id,
2678
- name: id,
2679
- role: 'main',
2680
- params: this.getMainIndicatorParams(id) ?? {},
2681
- }))
2682
-
2683
- const subIndicators: IndicatorInstance[] = this.getSubPaneEntries().map(entry => ({
2684
- id: entry.paneId,
2685
- definitionId: entry.indicatorId,
2686
- label: entry.indicatorId,
2687
- name: entry.indicatorId,
2688
- role: 'sub',
2689
- paneId: entry.paneId,
2690
- params: entry.params,
2691
- }))
2692
2688
 
2693
- this._indicatorsSignal.set([...mainIndicators, ...subIndicators])
2694
- }
2695
-
2696
- /**
2697
- * 同步 sub panes signal
2698
- */
2699
- private syncSubPanesSignal(): void {
2700
- const entries = this.getSubPaneEntries()
2701
- const subPanes: SubPaneInfo[] = entries.map(entry => ({
2702
- paneId: entry.paneId,
2703
- indicatorId: entry.indicatorId,
2704
- params: entry.params,
2705
- ratio: this._internalPaneRatios.get(entry.paneId) ?? 1,
2706
- }))
2707
-
2708
- this._subPanesSignal.set(subPanes)
2709
- }
2710
2689
 
2711
2690
  // ---------- Sub Panes ----------
2712
2691
 
@@ -102,10 +102,17 @@ export class InteractionController {
102
102
 
103
103
  constructor(chart: Chart) {
104
104
  this.chart = chart
105
+ this.setupPinchZoom()
105
106
  }
106
107
 
107
- setOnPinchZoom(callback: (delta: number, centerX: number) => void) {
108
- this.pinchTracker.setOnPinchZoom(callback)
108
+ private setupPinchZoom(): void {
109
+ this.pinchTracker.setOnPinchZoom((delta, centerClientX) => {
110
+ const container = this.chart.getDom().container
111
+ if (!container) return
112
+ const rect = container.getBoundingClientRect()
113
+ const centerX = centerClientX - rect.left
114
+ this.chart.handlePinchZoom(delta, centerX)
115
+ })
109
116
  }
110
117
 
111
118
  /** 更新用户设置 */