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

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 (110) 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 +82 -48
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  13. package/dist/data-fetchers/tradingview.js +4 -5
  14. package/dist/data-fetchers/tradingview.js.map +1 -1
  15. package/dist/engine/chart.d.ts +29 -367
  16. package/dist/engine/chart.d.ts.map +1 -1
  17. package/dist/engine/chart.js +239 -1842
  18. package/dist/engine/chart.js.map +1 -1
  19. package/dist/engine/chartContext.d.ts +24 -0
  20. package/dist/engine/chartContext.d.ts.map +1 -0
  21. package/dist/engine/chartContext.js +19 -0
  22. package/dist/engine/chartContext.js.map +1 -0
  23. package/dist/engine/chartTypes.d.ts +77 -0
  24. package/dist/engine/chartTypes.d.ts.map +1 -0
  25. package/dist/engine/chartTypes.js +2 -0
  26. package/dist/engine/chartTypes.js.map +1 -0
  27. package/dist/engine/data/chartDataManager.d.ts +102 -0
  28. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  29. package/dist/engine/data/chartDataManager.js +590 -0
  30. package/dist/engine/data/chartDataManager.js.map +1 -0
  31. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  32. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  33. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  34. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  35. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  36. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  37. package/dist/engine/layout/chartPaneLayout.js +388 -0
  38. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  39. package/dist/engine/render/chartRenderer.d.ts +86 -0
  40. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  41. package/dist/engine/render/chartRenderer.js +438 -0
  42. package/dist/engine/render/chartRenderer.js.map +1 -0
  43. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  44. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  45. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  46. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  47. package/dist/engine/renderers/comparisonLine.js +25 -11
  48. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  49. package/dist/engine/subPaneManager.d.ts +27 -6
  50. package/dist/engine/subPaneManager.d.ts.map +1 -1
  51. package/dist/engine/subPaneManager.js +54 -56
  52. package/dist/engine/subPaneManager.js.map +1 -1
  53. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  54. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  55. package/dist/engine/utils/chartZoomController.js +66 -0
  56. package/dist/engine/utils/chartZoomController.js.map +1 -0
  57. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  58. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  59. package/dist/engine/viewport/chartViewportManager.js +249 -0
  60. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  61. package/dist/plugin/types.d.ts +1 -0
  62. package/dist/plugin/types.d.ts.map +1 -1
  63. package/dist/plugin/types.js.map +1 -1
  64. package/dist/tokens/theme-china.d.ts.map +1 -1
  65. package/dist/tokens/theme-china.js +0 -4
  66. package/dist/tokens/theme-china.js.map +1 -1
  67. package/dist/tokens/theme-dark.d.ts.map +1 -1
  68. package/dist/tokens/theme-dark.js +0 -4
  69. package/dist/tokens/theme-dark.js.map +1 -1
  70. package/dist/tokens/theme-light.d.ts.map +1 -1
  71. package/dist/tokens/theme-light.js +1 -5
  72. package/dist/tokens/theme-light.js.map +1 -1
  73. package/dist/tokens/types.d.ts +0 -4
  74. package/dist/tokens/types.d.ts.map +1 -1
  75. package/dist/types/price.d.ts +2 -0
  76. package/dist/types/price.d.ts.map +1 -1
  77. package/dist/types/price.js.map +1 -1
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.d.ts.map +1 -1
  80. package/dist/version.js +1 -1
  81. package/dist/version.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/controllers/createChartController.ts +39 -10
  84. package/src/controllers/types.ts +6 -1
  85. package/src/data-fetchers/baostock.ts +3 -3
  86. package/src/data-fetchers/dataBuffer.ts +64 -23
  87. package/src/data-fetchers/tradingview.ts +4 -5
  88. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  89. package/src/engine/chart.ts +252 -2250
  90. package/src/engine/chartContext.ts +34 -0
  91. package/src/engine/chartTypes.ts +88 -0
  92. package/src/engine/data/chartDataManager.ts +691 -0
  93. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  94. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  95. package/src/engine/layout/chartPaneLayout.ts +474 -0
  96. package/src/engine/render/chartRenderer.ts +579 -0
  97. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  98. package/src/engine/renderers/comparisonLine.ts +25 -11
  99. package/src/engine/subPaneManager.ts +75 -59
  100. package/src/engine/utils/chartZoomController.ts +104 -0
  101. package/src/engine/viewport/chartViewportManager.ts +310 -0
  102. package/src/plugin/types.ts +1 -0
  103. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  104. package/src/tokens/theme-china.ts +0 -4
  105. package/src/tokens/theme-dark.ts +0 -4
  106. package/src/tokens/theme-light.ts +2 -6
  107. package/src/tokens/types.ts +0 -4
  108. package/src/types/price.ts +2 -0
  109. package/src/version.ts +1 -1
  110. package/src/engine/chart.d.ts +0 -626
@@ -31,16 +31,15 @@ import type {
31
31
  DataFetcher,
32
32
  } from './types'
33
33
  import type { CustomMarkerEntity } from '../engine/marker/registry'
34
- import {
35
- Chart,
36
- type ChartOptions,
37
- type ViewportState as LegacyViewportState,
38
- type IndicatorInstance as LegacyIndicatorInstance,
39
- type SubPaneInfo as LegacySubPaneInfo,
40
- type DrawingObject as LegacyDrawingObject,
41
- type DrawingToolType as LegacyDrawingToolType,
42
- type InteractionSnapshot as LegacyInteractionSnapshot,
43
- } from '../engine/chart'
34
+ import { Chart, type InteractionSnapshot as LegacyInteractionSnapshot } from '../engine/chart'
35
+ import type {
36
+ ChartOptions,
37
+ ViewportState as LegacyViewportState,
38
+ IndicatorInstance as LegacyIndicatorInstance,
39
+ SubPaneInfo as LegacySubPaneInfo,
40
+ DrawingObject as LegacyDrawingObject,
41
+ DrawingToolType as LegacyDrawingToolType,
42
+ } from '../engine/chartTypes'
44
43
  import { zoomLevelToKWidth, kGapFromKWidth } from '../engine/utils/zoom'
45
44
 
46
45
  // Plugin-backed drawings expose `kind` instead of legacy `type`.
@@ -371,6 +370,8 @@ export function createChartController(opts: ChartMountOptions): ChartController
371
370
  Readonly<Record<string, number>>
372
371
  >({})
373
372
  const interactionState: Signal<InteractionSnapshot> = createSignal(INITIAL_INTERACTION)
373
+ const comparisonColors: Signal<ReadonlyMap<string, string>> = createSignal<ReadonlyMap<string, string>>(new Map())
374
+ const comparisonLoading: Signal<boolean> = createSignal(false)
374
375
 
375
376
  // -------------------------------------------------------------------
376
377
  // Apply initial render state + seed data
@@ -477,6 +478,20 @@ export function createChartController(opts: ChartMountOptions): ChartController
477
478
  ),
478
479
  )
479
480
 
481
+ // comparisonColors
482
+ unsubs.push(
483
+ chart.comparisonColors.subscribe(() =>
484
+ comparisonColors.set(new Map(chart.comparisonColors.peek())),
485
+ ),
486
+ )
487
+
488
+ // comparisonLoading
489
+ unsubs.push(
490
+ chart.comparisonLoading.subscribe(() =>
491
+ comparisonLoading.set(chart.comparisonLoading.peek()),
492
+ ),
493
+ )
494
+
480
495
  // -------------------------------------------------------------------
481
496
  // Lifecycle guard
482
497
  // -------------------------------------------------------------------
@@ -501,6 +516,16 @@ export function createChartController(opts: ChartMountOptions): ChartController
501
516
  chart.setSymbols(next)
502
517
  }
503
518
 
519
+ function addComparisonSymbol(spec: SymbolSpec): void {
520
+ if (disposed) return
521
+ chart.addComparisonSymbol(spec)
522
+ }
523
+
524
+ function removeComparisonSymbol(symbol: string): void {
525
+ if (disposed) return
526
+ chart.removeComparisonSymbol(symbol)
527
+ }
528
+
504
529
  function setDataFetcher(fetcher: DataFetcher | null): void {
505
530
  if (disposed) return
506
531
  chart.setDataFetcher(fetcher)
@@ -783,8 +808,12 @@ export function createChartController(opts: ChartMountOptions): ChartController
783
808
  paneRatios,
784
809
  paneLayout,
785
810
  interactionState,
811
+ comparisonColors,
812
+ comparisonLoading,
786
813
  catalog: DEFAULT_INDICATOR_CATALOG,
787
814
  setSymbols,
815
+ addComparisonSymbol,
816
+ removeComparisonSymbol,
788
817
  setDataFetcher,
789
818
  setData,
790
819
  appendData,
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { Signal } from '../reactivity'
12
12
  import type { CustomMarkerEntity, MarkerEntity } from '../engine/marker/registry'
13
- import type { PaneSpec } from '../engine/chart'
13
+ import type { PaneSpec } from '../engine/chartTypes'
14
14
 
15
15
  // Controller-owned public surface. Legacy engine types may mirror these
16
16
  // shapes internally, but adapters depend only on core-defined contracts.
@@ -71,6 +71,7 @@ export interface KLineData {
71
71
  changePercent?: number
72
72
  changeAmount?: number
73
73
  turnoverRate?: number
74
+ date?: string
74
75
  }
75
76
 
76
77
  export type { PaneSpec }
@@ -242,12 +243,16 @@ export interface ChartController extends DrawingChartAdapter {
242
243
  readonly paneRatios: Signal<Readonly<Record<string, number>>>
243
244
  readonly paneLayout: Signal<ReadonlyArray<PaneSpec>>
244
245
  readonly interactionState: Signal<InteractionSnapshot>
246
+ readonly comparisonColors: Signal<ReadonlyMap<string, string>>
247
+ readonly comparisonLoading: Signal<boolean>
245
248
 
246
249
  // indicator catalog (static — adapters use for picker UI)
247
250
  readonly catalog: ReadonlyArray<IndicatorDefinition>
248
251
 
249
252
  // ---- Data ----
250
253
  setSymbols(next: ReadonlyArray<SymbolSpec>): void
254
+ addComparisonSymbol(spec: SymbolSpec): void
255
+ removeComparisonSymbol(symbol: string): void
251
256
  setDataFetcher(fetcher: DataFetcher | null): void
252
257
  setData(next: ReadonlyArray<KLineData>): void
253
258
  appendData(next: ReadonlyArray<KLineData>): void
@@ -11,13 +11,13 @@ const periodMap: Record<string, string> = { daily: 'd', weekly: 'w', monthly: 'm
11
11
  const res = await fetch(url)
12
12
  console.log(res)
13
13
  if (!res.ok) {
14
- console.warn(`[baostock] fetch failed: ${res.status} ${res.statusText}`)
15
- return []
14
+ throw new Error(`[baostock] fetch failed: ${res.status} ${res.statusText}`)
16
15
  }
17
16
  const json = await res.json()
18
17
  console.log(json)
19
18
  return (json.data ?? json).map((item: Record<string, unknown>) => ({
20
19
  timestamp: new Date(item.date as string).getTime(),
20
+ date: item.date as string,
21
21
  open: Number(item.open),
22
22
  high: Number(item.high),
23
23
  low: Number(item.low),
@@ -29,6 +29,6 @@ const periodMap: Record<string, string> = { daily: 'd', weekly: 'w', monthly: 'm
29
29
  })) as KLineData[]
30
30
  } catch (err) {
31
31
  console.warn('[baostock] network error:', err)
32
- return []
32
+ throw err
33
33
  }
34
34
  }
@@ -9,6 +9,7 @@ export interface DataWindow {
9
9
  const MS_PER_DAY = 86_400_000
10
10
  const INITIAL_LOAD_DAYS = 365
11
11
  const INCREMENTAL_LOAD_DAYS = 90
12
+ const FETCH_MAX_RETRIES = 2
12
13
 
13
14
  function formatDate(ts: number): string {
14
15
  const d = new Date(ts)
@@ -39,6 +40,7 @@ export class DataBuffer {
39
40
  private _dataSignal: Signal<ReadonlyArray<KLineData>>
40
41
  private _loadingSignal: Signal<boolean>
41
42
  private _fetcher: DataFetcher | null = null
43
+ private _requestFetch: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null = null
42
44
  private _currentSpec: SymbolSpec | null = null
43
45
  private _loadedWindow: DataWindow | null = null
44
46
  private _pendingFetch: Promise<void> | null = null
@@ -60,6 +62,10 @@ export class DataBuffer {
60
62
  return this._loadingSignal
61
63
  }
62
64
 
65
+ get currentSpec(): SymbolSpec | null {
66
+ return this._currentSpec
67
+ }
68
+
63
69
  get loadedWindow(): DataWindow | null {
64
70
  return this._loadedWindow
65
71
  }
@@ -68,17 +74,25 @@ export class DataBuffer {
68
74
  this._fetcher = fetcher
69
75
  }
70
76
 
71
- setSymbol(spec: SymbolSpec): void {
77
+ setRequestFetch(fn: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null): void {
78
+ this._requestFetch = fn
79
+ }
80
+
81
+ setSymbol(spec: SymbolSpec, initialStartTs?: number): void {
72
82
  this._currentSpec = spec
73
83
  this._data = []
74
84
  this._loadedWindow = null
75
85
  this._attemptedBoundaries.clear()
76
86
  this._dataSignal.set([])
77
- this.loadInitial()
87
+ if (initialStartTs !== undefined) {
88
+ this.loadInitialRange(initialStartTs, Date.now())
89
+ } else {
90
+ this.loadInitial()
91
+ }
78
92
  }
79
93
 
80
94
  ensureRange(requestStartTs: number, _requestEndTs: number): void {
81
- if (this._disposed || !this._fetcher || !this._currentSpec) return
95
+ if (this._disposed || (!this._requestFetch && !this._fetcher) || !this._currentSpec) return
82
96
  if (!this._loadedWindow) return
83
97
 
84
98
  if (requestStartTs >= this._loadedWindow.earliestTs) return
@@ -95,7 +109,7 @@ export class DataBuffer {
95
109
  }
96
110
 
97
111
  private loadInitial(): void {
98
- if (!this._fetcher || !this._currentSpec || this._disposed) return
112
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
99
113
 
100
114
  const now = Date.now()
101
115
  const startDate = now - INITIAL_LOAD_DAYS * MS_PER_DAY
@@ -104,13 +118,18 @@ export class DataBuffer {
104
118
  this.fetchRange(startDate, endDate)
105
119
  }
106
120
 
107
- private fetchRange(startTs: number, endTs: number): void {
108
- if (!this._fetcher || !this._currentSpec || this._disposed) return
121
+ private loadInitialRange(startTs: number, endTs: number): void {
122
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
123
+ this.fetchRange(startTs, endTs)
124
+ }
125
+
126
+ private fetchRange(startTs: number, endTs: number, retryCount = 0): void {
127
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
109
128
 
110
129
  if (this._pendingFetch) {
111
130
  this._pendingFetch = this._pendingFetch.then(() => {
112
131
  if (this._disposed) return
113
- this.fetchRange(startTs, endTs)
132
+ return this.fetchRange(startTs, endTs, retryCount)
114
133
  })
115
134
  return
116
135
  }
@@ -120,15 +139,18 @@ export class DataBuffer {
120
139
 
121
140
  this._loadingSignal.set(true)
122
141
 
123
- this._pendingFetch = fetcher(spec.source ?? 'baostock', {
124
- symbol: spec.symbol,
125
- startDate: formatDate(startTs),
126
- endDate: formatDate(endTs),
127
- period: spec.period ?? 'daily',
128
- adjust: spec.adjust ?? 'none',
129
- exchange: spec.exchange,
130
- })
131
- .then((incoming) => {
142
+ const doFetch = (): Promise<void> => {
143
+ const fetchPromise = this._requestFetch
144
+ ? this._requestFetch(spec, startTs, endTs)
145
+ : (fetcher as NonNullable<DataFetcher>)(spec.source ?? 'baostock', {
146
+ symbol: spec.symbol,
147
+ startDate: formatDate(startTs),
148
+ endDate: formatDate(endTs),
149
+ period: spec.period ?? 'daily',
150
+ adjust: spec.adjust ?? 'none',
151
+ exchange: spec.exchange,
152
+ })
153
+ return fetchPromise.then((incoming) => {
132
154
  if (this._disposed) return
133
155
 
134
156
  const oldLength = this._data.length
@@ -161,16 +183,35 @@ export class DataBuffer {
161
183
  }
162
184
  }
163
185
  })
164
- .catch((err) => {
186
+ }
187
+
188
+ const attempt = (count: number): Promise<void> => {
189
+ return doFetch().catch((err) => {
165
190
  if (this._disposed) return
166
- console.error('[DataBuffer] fetch failed:', err)
167
- })
168
- .finally(() => {
169
- this._pendingFetch = null
170
- if (!this._disposed) {
171
- this._loadingSignal.set(false)
191
+
192
+ if (count < FETCH_MAX_RETRIES) {
193
+ const delay = Math.pow(2, count) * 1000
194
+ console.warn(
195
+ `[DataBuffer] fetch failed, retry ${count + 1}/${FETCH_MAX_RETRIES} in ${delay}ms:`,
196
+ err,
197
+ )
198
+ return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => {
199
+ if (this._disposed) return
200
+ return attempt(count + 1)
201
+ })
172
202
  }
203
+
204
+ console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
205
+ this._attemptedBoundaries.delete(endTs)
173
206
  })
207
+ }
208
+
209
+ this._pendingFetch = attempt(retryCount).finally(() => {
210
+ this._pendingFetch = null
211
+ if (!this._disposed) {
212
+ this._loadingSignal.set(false)
213
+ }
214
+ })
174
215
  }
175
216
 
176
217
  dispose(): void {
@@ -21,13 +21,11 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
21
21
  try {
22
22
  const res = await fetch(url)
23
23
  if (!res.ok) {
24
- console.warn(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
25
- return []
24
+ throw new Error(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
26
25
  }
27
26
  const json = await res.json()
28
27
  if (!json.success) {
29
- console.warn(`[tradingview] API error: ${json.error_msg}`)
30
- return []
28
+ throw new Error(`[tradingview] API error: ${json.error_msg}`)
31
29
  }
32
30
  if (json.warning) {
33
31
  console.warn(`[tradingview] ${json.warning}`)
@@ -35,6 +33,7 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
35
33
 
36
34
  return (json.data ?? []).map((item: Record<string, unknown>) => ({
37
35
  timestamp: item.ts_open as number,
36
+ date: item.date as string,
38
37
  open: item.open as number,
39
38
  high: item.high as number,
40
39
  low: item.low as number,
@@ -44,6 +43,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
44
43
  })) as KLineData[]
45
44
  } catch (err) {
46
45
  console.warn('[tradingview] network error:', err)
47
- return []
46
+ throw err
48
47
  }
49
48
  }
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { SubPaneManager, type SubPaneContext } from '../subPaneManager'
3
+ import type { SubIndicatorType } from '../renderers/Indicator'
4
+ import type { IndicatorScheduler } from '../indicators/scheduler'
5
+
6
+ function createMockScheduler(): Partial<IndicatorScheduler> {
7
+ return {
8
+ getIndicatorMetadata: vi.fn((_id: string) => ({
9
+ rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
10
+ updateConfig: vi.fn(),
11
+ scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
12
+ })),
13
+ onSubPaneChanged: vi.fn(),
14
+ }
15
+ }
16
+
17
+ function createMockContext(): SubPaneContext {
18
+ const scheduler = createMockScheduler()
19
+ return {
20
+ getIndicatorScheduler: vi.fn(() => scheduler as unknown as IndicatorScheduler),
21
+ hasPane: vi.fn(() => false),
22
+ upsertPane: vi.fn(),
23
+ getRenderer: vi.fn(),
24
+ useRenderer: vi.fn(),
25
+ removeRenderer: vi.fn(),
26
+ removePaneDefinition: vi.fn(),
27
+ updateRendererConfig: vi.fn(),
28
+ getRightAxisWidth: vi.fn(() => 60),
29
+ getPriceLabelWidth: vi.fn(() => 60),
30
+ getYPaddingPx: vi.fn(() => 4),
31
+ getCrosshairPos: vi.fn(() => null),
32
+ getCrosshairPrice: vi.fn(() => null),
33
+ getActivePaneId: vi.fn(() => null),
34
+ }
35
+ }
36
+
37
+ describe('SubPaneManager', () => {
38
+ let manager: SubPaneManager
39
+ let ctx: SubPaneContext
40
+
41
+ beforeEach(() => {
42
+ manager = new SubPaneManager()
43
+ ctx = createMockContext()
44
+ vi.clearAllMocks()
45
+ })
46
+
47
+ describe('updateParams', () => {
48
+ it('should update paneTitle renderer config with new params and indicatorId', () => {
49
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
50
+ period1: 6,
51
+ period2: 12,
52
+ period3: 24,
53
+ })
54
+
55
+ const entry = manager.getByPaneId('RSI_0')
56
+ expect(entry).toBeDefined()
57
+ vi.clearAllMocks()
58
+
59
+ const newParams = { period1: 10, period2: 20, period3: 30 }
60
+ manager.updateParams(ctx, 'RSI_0', newParams)
61
+
62
+ expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
63
+ entry!.paneTitleRendererName,
64
+ { params: newParams, indicatorId: 'RSI' },
65
+ )
66
+ })
67
+
68
+ it('should update main indicator renderer config with new params', () => {
69
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
70
+ period1: 6,
71
+ period2: 12,
72
+ period3: 24,
73
+ })
74
+
75
+ const entry = manager.getByPaneId('RSI_0')
76
+ expect(entry).toBeDefined()
77
+ vi.clearAllMocks()
78
+
79
+ const newParams = { period1: 10, period2: 20, period3: 30 }
80
+ manager.updateParams(ctx, 'RSI_0', newParams)
81
+
82
+ expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
83
+ entry!.rendererName,
84
+ newParams,
85
+ )
86
+ })
87
+
88
+ it('should update scheduler config via definition.updateConfig', () => {
89
+ const updateConfigSpy = vi.fn()
90
+ const customScheduler: Partial<IndicatorScheduler> = {
91
+ getIndicatorMetadata: vi.fn(() => ({
92
+ rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
93
+ updateConfig: updateConfigSpy,
94
+ scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
95
+ })),
96
+ onSubPaneChanged: vi.fn(),
97
+ }
98
+ const customCtx: SubPaneContext = {
99
+ ...ctx,
100
+ getIndicatorScheduler: vi.fn(() => customScheduler as unknown as IndicatorScheduler),
101
+ }
102
+
103
+ manager.create(customCtx, 'RSI_0', 'RSI' as SubIndicatorType, {
104
+ period1: 6,
105
+ period2: 12,
106
+ period3: 24,
107
+ })
108
+
109
+ const newParams = { period1: 10, period2: 20, period3: 30 }
110
+ manager.updateParams(customCtx, 'RSI_0', newParams)
111
+
112
+ expect(updateConfigSpy).toHaveBeenCalled()
113
+ })
114
+
115
+ it('should update entry params in the manager', () => {
116
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
117
+ period1: 6,
118
+ period2: 12,
119
+ period3: 24,
120
+ })
121
+
122
+ const newParams = { period1: 10, period2: 20, period3: 30 }
123
+ manager.updateParams(ctx, 'RSI_0', newParams)
124
+
125
+ const entry = manager.getByPaneId('RSI_0')
126
+ expect(entry?.params).toEqual(newParams)
127
+ })
128
+
129
+ it('should fire entries signal on update', () => {
130
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
131
+ period1: 6,
132
+ period2: 12,
133
+ period3: 24,
134
+ })
135
+
136
+ const listener = vi.fn()
137
+ manager.entriesSignal.subscribe(listener)
138
+ vi.clearAllMocks()
139
+
140
+ const newParams = { period1: 10, period2: 20, period3: 30 }
141
+ manager.updateParams(ctx, 'RSI_0', newParams)
142
+
143
+ expect(listener).toHaveBeenCalledTimes(1)
144
+ })
145
+
146
+ it('should silently skip when paneId does not exist', () => {
147
+ const newParams = { period1: 10, period2: 20, period3: 30 }
148
+ manager.updateParams(ctx, 'NONEXISTENT', newParams)
149
+
150
+ expect(ctx.updateRendererConfig).not.toHaveBeenCalled()
151
+ expect(ctx.getIndicatorScheduler().onSubPaneChanged).not.toHaveBeenCalled()
152
+ })
153
+ })
154
+ })