@363045841yyt/klinechart-core 0.8.14 → 0.8.15-alpha.0

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 (151) hide show
  1. package/README.md +5 -4
  2. package/README.zh-CN.md +44 -43
  3. package/dist/config/chartSettings.d.ts +13 -0
  4. package/dist/config/chartSettings.d.ts.map +1 -1
  5. package/dist/config/chartSettings.js +11 -0
  6. package/dist/config/chartSettings.js.map +1 -1
  7. package/dist/controllers/createChartController.d.ts.map +1 -1
  8. package/dist/controllers/createChartController.js +0 -1
  9. package/dist/controllers/createChartController.js.map +1 -1
  10. package/dist/controllers/types.d.ts +2 -2
  11. package/dist/controllers/types.d.ts.map +1 -1
  12. package/dist/data-fetchers/baostock.js +1 -1
  13. package/dist/data-fetchers/baostock.js.map +1 -1
  14. package/dist/data-fetchers/dataBuffer.d.ts +4 -1
  15. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  16. package/dist/data-fetchers/dataBuffer.js +24 -9
  17. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  18. package/dist/data-fetchers/gotdx.js +2 -2
  19. package/dist/data-fetchers/gotdx.js.map +1 -1
  20. package/dist/data-fetchers/tradingview.js +1 -1
  21. package/dist/data-fetchers/tradingview.js.map +1 -1
  22. package/dist/engine/chart.d.ts +9 -16
  23. package/dist/engine/chart.d.ts.map +1 -1
  24. package/dist/engine/chart.js +158 -124
  25. package/dist/engine/chart.js.map +1 -1
  26. package/dist/engine/controller/interaction.d.ts +11 -4
  27. package/dist/engine/controller/interaction.d.ts.map +1 -1
  28. package/dist/engine/controller/interaction.js +94 -17
  29. package/dist/engine/controller/interaction.js.map +1 -1
  30. package/dist/engine/controller/tooltipPosition.d.ts +3 -0
  31. package/dist/engine/controller/tooltipPosition.d.ts.map +1 -1
  32. package/dist/engine/controller/tooltipPosition.js +15 -1
  33. package/dist/engine/controller/tooltipPosition.js.map +1 -1
  34. package/dist/engine/data/chartDataManager.d.ts +18 -6
  35. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  36. package/dist/engine/data/chartDataManager.js +149 -51
  37. package/dist/engine/data/chartDataManager.js.map +1 -1
  38. package/dist/engine/data/comparisonManager.d.ts +0 -1
  39. package/dist/engine/data/comparisonManager.d.ts.map +1 -1
  40. package/dist/engine/data/comparisonManager.js +8 -8
  41. package/dist/engine/data/comparisonManager.js.map +1 -1
  42. package/dist/engine/data/incrementalLoadHint.d.ts +6 -5
  43. package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
  44. package/dist/engine/data/incrementalLoadHint.js +76 -58
  45. package/dist/engine/data/incrementalLoadHint.js.map +1 -1
  46. package/dist/engine/data/scrollCompensator.d.ts +1 -0
  47. package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
  48. package/dist/engine/data/scrollCompensator.js +2 -2
  49. package/dist/engine/data/scrollCompensator.js.map +1 -1
  50. package/dist/engine/indicators/chartIndicatorManager.d.ts +4 -2
  51. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
  52. package/dist/engine/indicators/chartIndicatorManager.js +1 -29
  53. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
  54. package/dist/engine/indicators/scheduler.d.ts +5 -0
  55. package/dist/engine/indicators/scheduler.d.ts.map +1 -1
  56. package/dist/engine/indicators/scheduler.js +14 -8
  57. package/dist/engine/indicators/scheduler.js.map +1 -1
  58. package/dist/engine/render/chartRenderer.d.ts +1 -0
  59. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  60. package/dist/engine/render/chartRenderer.js +13 -9
  61. package/dist/engine/render/chartRenderer.js.map +1 -1
  62. package/dist/engine/renderers/candle.js +5 -2
  63. package/dist/engine/renderers/candle.js.map +1 -1
  64. package/dist/engine/renderers/extremaMarkers.d.ts.map +1 -1
  65. package/dist/engine/renderers/extremaMarkers.js +2 -1
  66. package/dist/engine/renderers/extremaMarkers.js.map +1 -1
  67. package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
  68. package/dist/engine/renderers/leftYAxis.js +1 -0
  69. package/dist/engine/renderers/leftYAxis.js.map +1 -1
  70. package/dist/engine/renderers/subVolume.js +8 -9
  71. package/dist/engine/renderers/subVolume.js.map +1 -1
  72. package/dist/engine/renderers/yAxis.d.ts.map +1 -1
  73. package/dist/engine/renderers/yAxis.js +1 -0
  74. package/dist/engine/renderers/yAxis.js.map +1 -1
  75. package/dist/engine/utils/chartZoomController.d.ts +8 -10
  76. package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
  77. package/dist/engine/utils/chartZoomController.js +33 -13
  78. package/dist/engine/utils/chartZoomController.js.map +1 -1
  79. package/dist/engine/utils/zoom.d.ts +12 -2
  80. package/dist/engine/utils/zoom.d.ts.map +1 -1
  81. package/dist/engine/utils/zoom.js +9 -1
  82. package/dist/engine/utils/zoom.js.map +1 -1
  83. package/dist/engine/viewport/chartViewportManager.d.ts +13 -6
  84. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
  85. package/dist/engine/viewport/chartViewportManager.js +37 -17
  86. package/dist/engine/viewport/chartViewportManager.js.map +1 -1
  87. package/dist/reactivity/index.d.ts +1 -1
  88. package/dist/reactivity/index.d.ts.map +1 -1
  89. package/dist/reactivity/index.js +1 -1
  90. package/dist/reactivity/index.js.map +1 -1
  91. package/dist/reactivity/signal.d.ts +37 -7
  92. package/dist/reactivity/signal.d.ts.map +1 -1
  93. package/dist/reactivity/signal.js +75 -11
  94. package/dist/reactivity/signal.js.map +1 -1
  95. package/dist/semantic/types.d.ts +1 -1
  96. package/dist/semantic/types.d.ts.map +1 -1
  97. package/dist/semantic/validator.d.ts +1 -1
  98. package/dist/semantic/validator.d.ts.map +1 -1
  99. package/dist/semantic/validator.js +5 -5
  100. package/dist/semantic/validator.js.map +1 -1
  101. package/dist/types/kLine.d.ts +1 -1
  102. package/dist/types/kLine.d.ts.map +1 -1
  103. package/dist/types/kLine.js +11 -8
  104. package/dist/types/kLine.js.map +1 -1
  105. package/dist/types/price.d.ts +4 -3
  106. package/dist/types/price.d.ts.map +1 -1
  107. package/dist/types/price.js +1 -1
  108. package/dist/types/price.js.map +1 -1
  109. package/dist/utils/viewportSide.d.ts +2 -0
  110. package/dist/utils/viewportSide.d.ts.map +1 -0
  111. package/dist/utils/viewportSide.js +4 -0
  112. package/dist/utils/viewportSide.js.map +1 -0
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.d.ts.map +1 -1
  115. package/dist/version.js +1 -1
  116. package/dist/version.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/config/chartSettings.ts +18 -7
  119. package/src/controllers/createChartController.ts +0 -1
  120. package/src/controllers/types.ts +2 -2
  121. package/src/data-fetchers/baostock.ts +1 -1
  122. package/src/data-fetchers/dataBuffer.ts +25 -9
  123. package/src/data-fetchers/gotdx.ts +2 -2
  124. package/src/data-fetchers/tradingview.ts +1 -1
  125. package/src/engine/chart.ts +170 -130
  126. package/src/engine/controller/interaction.ts +98 -20
  127. package/src/engine/controller/tooltipPosition.ts +22 -1
  128. package/src/engine/data/chartDataManager.ts +158 -62
  129. package/src/engine/data/comparisonManager.ts +7 -8
  130. package/src/engine/data/incrementalLoadHint.ts +96 -63
  131. package/src/engine/data/scrollCompensator.ts +3 -2
  132. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +0 -1
  133. package/src/engine/indicators/chartIndicatorManager.ts +5 -33
  134. package/src/engine/indicators/scheduler.ts +19 -11
  135. package/src/engine/render/chartRenderer.ts +15 -10
  136. package/src/engine/renderers/candle.ts +5 -3
  137. package/src/engine/renderers/extremaMarkers.ts +2 -1
  138. package/src/engine/renderers/leftYAxis.ts +1 -0
  139. package/src/engine/renderers/subVolume.ts +11 -12
  140. package/src/engine/renderers/yAxis.ts +1 -0
  141. package/src/engine/utils/chartZoomController.ts +40 -26
  142. package/src/engine/utils/zoom.ts +26 -3
  143. package/src/engine/viewport/chartViewportManager.ts +37 -18
  144. package/src/reactivity/index.ts +1 -1
  145. package/src/reactivity/signal.ts +71 -10
  146. package/src/semantic/types.ts +1 -1
  147. package/src/semantic/validator.ts +7 -7
  148. package/src/types/kLine.ts +10 -7
  149. package/src/types/price.ts +5 -4
  150. package/src/utils/viewportSide.ts +3 -0
  151. package/src/version.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { createSignal } from '../reactivity/signal';
1
+ import { createStateStore, createSignal } from '../reactivity/signal';
2
2
  import { InteractionController } from './controller/interaction';
3
3
  import { ChartDataManager } from './data/chartDataManager';
4
4
  import { ChartIndicatorManager } from './indicators/chartIndicatorManager';
@@ -19,7 +19,7 @@ import { createVolumeLookbacks, pushToVolumeLookbacks, } from '../alerts/rolling
19
19
  export { getPhysicalKLineConfig };
20
20
  export class Chart {
21
21
  dom;
22
- opt;
22
+ _optionsSignal;
23
23
  dataManager;
24
24
  viewportManager;
25
25
  layoutManager;
@@ -43,12 +43,8 @@ export class Chart {
43
43
  _activeMode;
44
44
  _kLineMode = new KLineMode();
45
45
  _timeShareMode = new TimeShareMode();
46
- /** 模式切换时保存的渲染状态(退出分时时恢复) */
47
- _modeSavedKWidth = 0;
48
- _modeSavedKGap = 0;
49
- _modeSavedZoomLevel = 0;
50
- /** 分时模式激活前的 pane Y 轴刻度类型(退出分时时恢复) */
51
- _savedScaleTypes;
46
+ /** 进入分时模式时保存的快照,退出时恢复(包含 zoom/scale/indicators) */
47
+ _savedTimeShareState = null;
52
48
  /** 上次预警评估的最新 K 线时间戳(用于去重) */
53
49
  _lastAlertTimestamp = null;
54
50
  /** 预警控制器 */
@@ -100,11 +96,11 @@ export class Chart {
100
96
  this.dom = dom;
101
97
  const { kWidth: _kWidth, kGap: _kGap, ...restOpt } = opt;
102
98
  // Chart 不持有业务 SSOT,kWidth/kGap/zoomLevel 由外部通过 applyRenderState() 传入
103
- this.opt = { ...restOpt, kWidth: _kWidth ?? 0, kGap: _kGap ?? 0 };
99
+ this._optionsSignal = createSignal({ ...restOpt, kWidth: _kWidth ?? 0, kGap: _kGap ?? 0 });
104
100
  this._activeMode = this._kLineMode;
105
101
  this.interaction = new InteractionController(this);
106
102
  this.interaction.setOnInteractionChange((snapshot) => {
107
- this._interactionSignal.set(snapshot);
103
+ this.state.set.interaction(snapshot);
108
104
  });
109
105
  this.pluginHost = createPluginHost();
110
106
  this.rendererPluginManager = new RendererPluginManager();
@@ -112,44 +108,49 @@ export class Chart {
112
108
  // 注入依赖
113
109
  this.rendererPluginManager.setPluginHost(this.pluginHost);
114
110
  this.rendererPluginManager.setInvalidateCallback(() => this.scheduleDraw());
111
+ const initialOpt = this._optionsSignal.peek();
115
112
  this.viewportManager = new ChartViewportManager({
116
113
  getDom: () => this.dom,
117
- getBottomAxisHeight: () => this.opt.bottomAxisHeight,
114
+ getBottomAxisHeight: () => this._optionsSignal.peek().bottomAxisHeight,
118
115
  getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
119
116
  getZoomLevel: () => this.zoomController.currentZoomLevel,
120
- getLastVisibleRange: () => this.dataManager.lastVisibleRange,
121
- getKWidth: () => this.opt.kWidth,
122
- getKGap: () => this.opt.kGap,
117
+ getLastVisibleRange: () => this.dataManager.getCurrentVisibleRange() ?? { start: 0, end: 0 },
118
+ getKWidth: () => this._optionsSignal.peek().kWidth,
119
+ getKGap: () => this._optionsSignal.peek().kGap,
123
120
  scheduleDraw: (level) => this.scheduleDraw(level),
124
121
  onResizeCompleted: () => {
125
122
  this.resize();
126
123
  },
127
124
  resizeSharedWebGLSurface: (plotWidth, plotHeight, dpr) => this.sharedWebGLSurface.resize(plotWidth, plotHeight, dpr),
128
125
  });
129
- this.layoutManager = new ChartPaneLayout(this.opt.panes, {
126
+ this.viewportManager.setContentWidthProvider(() => Math.max(this.dataManager.getContentWidth(), this.dataManager.getLeftLoadBufferWidth()));
127
+ this.layoutManager = new ChartPaneLayout(initialOpt.panes, {
130
128
  getDom: () => this.dom,
131
- getOption: () => ({
132
- rightAxisWidth: this.opt.rightAxisWidth,
133
- leftAxisWidth: this.opt.leftAxisWidth,
134
- yPaddingPx: this.opt.yPaddingPx,
135
- priceLabelWidth: this.opt.priceLabelWidth,
136
- paneGap: this.opt.paneGap,
137
- defaultPaneMinHeightPx: this.opt.defaultPaneMinHeightPx,
138
- }),
129
+ getOption: () => {
130
+ const o = this._optionsSignal.peek();
131
+ return {
132
+ rightAxisWidth: o.rightAxisWidth,
133
+ leftAxisWidth: o.leftAxisWidth,
134
+ yPaddingPx: o.yPaddingPx,
135
+ priceLabelWidth: o.priceLabelWidth,
136
+ paneGap: o.paneGap,
137
+ defaultPaneMinHeightPx: o.defaultPaneMinHeightPx,
138
+ };
139
+ },
139
140
  getViewport: () => this.viewportManager.getViewport(),
140
141
  getSharedWebGLSurface: () => this.sharedWebGLSurface,
141
142
  setKnownPaneIds: (ids) => this.rendererPluginManager.setKnownPaneIds(ids),
142
143
  notifyPaneResize: (paneId, pane) => this.rendererPluginManager.notifyResize(paneId, wrapPaneInfo(pane)),
143
144
  scheduleDraw: (level) => this.scheduleDraw(level),
144
145
  onLayoutChange: (ratios, specs) => {
145
- this._paneRatiosSignal.set(ratios);
146
- this._paneLayoutSignal.set(specs);
147
- this.opt = { ...this.opt, panes: specs };
146
+ this.state.set.paneRatios(ratios);
147
+ this.state.set.paneLayout(specs);
148
+ this._optionsSignal.set({ ...this._optionsSignal.peek(), panes: specs });
148
149
  },
149
150
  });
150
151
  this.alertController = createAlertController();
151
152
  this.dataManager = new ChartDataManager({
152
- getOption: () => this.opt,
153
+ getOption: () => this._optionsSignal.peek(),
153
154
  getEffectiveDpr: () => this.viewportManager.getEffectiveDpr(),
154
155
  getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
155
156
  getCachedScrollLeft: () => this.viewportManager.getCachedScrollLeft(),
@@ -162,9 +163,6 @@ export class Chart {
162
163
  scheduleDraw: (level) => this.scheduleDraw(level),
163
164
  resetInteraction: () => this.interaction.reset(),
164
165
  getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
165
- setPendingIndicatorDataUpdate: (v) => {
166
- this.dataManager.pendingIndicatorDataUpdate = v;
167
- },
168
166
  isPointerDown: () => this.interaction.isPointerDown(),
169
167
  onTimeShareDataReady: (dataLength) => {
170
168
  const vp = this.viewportManager.computeViewport();
@@ -173,12 +171,8 @@ export class Chart {
173
171
  const result = this._activeMode.computeKWidth(dataLength, vp.plotWidth, vp.dpr);
174
172
  if (result) {
175
173
  this.applyRenderState(result.kWidth, result.kGap);
176
- const container = this.dom.container;
177
- if (container) {
178
- const leftBuffer = this.dataManager.getLeftLoadBufferWidth();
179
- this.viewportManager.setScrollLeft(leftBuffer);
180
- this.viewportManager.applyPendingScrollLeft(container);
181
- }
174
+ const leftBuffer = this.dataManager.getLeftLoadBufferWidth();
175
+ this.viewportManager.setScrollLeft(leftBuffer);
182
176
  }
183
177
  },
184
178
  onDataProcessed: (data, range) => this.evaluateAlerts(data, range), // Alert 管线绑定
@@ -186,28 +180,23 @@ export class Chart {
186
180
  this.zoomController = new ChartZoomController({
187
181
  getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
188
182
  getCurrentDpr: () => this.viewportManager.getEffectiveDpr(),
189
- getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
190
- getContentWidth: () => this.dataManager.getContentWidth(),
191
183
  getClientWidth: () => this.viewportManager.getViewport()?.viewWidth ?? this.dom.container?.clientWidth ?? 0,
184
+ getDataLength: () => this.dataManager.getData().length,
185
+ getPlotWidth: () => this.dataManager.getLeftLoadBufferWidth(),
192
186
  setScrollLeft: (v) => {
193
187
  this.viewportManager.setScrollLeft(v);
194
188
  },
195
- onZoomCommitted: (result) => {
196
- this.opt = { ...this.opt, kWidth: result.kWidth, kGap: result.kGap };
197
- this.updateViewportSignal();
198
- this.scheduleDraw();
189
+ onChange: () => {
190
+ this.syncKWidthKGap();
199
191
  },
200
- getKWidth: () => this.opt.kWidth,
201
- getKGap: () => this.opt.kGap,
202
- getMinKWidth: () => this.opt.minKWidth,
203
- getMaxKWidth: () => this.opt.maxKWidth,
204
- zoomLevelCount: Math.max(2, Math.round(this.opt.zoomLevels ?? 20)),
205
- initialZoomLevel: this.opt.initialZoomLevel ?? 1,
192
+ getMinKWidth: () => this._optionsSignal.peek().minKWidth,
193
+ getMaxKWidth: () => this._optionsSignal.peek().maxKWidth,
194
+ zoomLevelCount: Math.max(2, Math.round(this._optionsSignal.peek().zoomLevels ?? 20)),
195
+ initialZoomLevel: this._optionsSignal.peek().initialZoomLevel ?? 1,
206
196
  });
207
- // 注意:初始 kWidth/kGap 应由外部通过 applyRenderState() 传入
208
197
  // 初始化指标管理器
209
198
  this.indicatorManager = new ChartIndicatorManager({
210
- getOption: () => this.opt,
199
+ getOption: () => this._optionsSignal.peek(),
211
200
  getPluginHost: () => this.pluginHost,
212
201
  getRenderer: (name) => this.getRenderer(name),
213
202
  useRenderer: (plugin, config) => this.useRenderer(plugin, config),
@@ -218,40 +207,41 @@ export class Chart {
218
207
  upsertPane: (def) => this.layoutManager.upsertPane(def),
219
208
  removePaneDefinition: (paneId) => this.layoutManager.removePaneDefinition(paneId),
220
209
  getPaneSpecs: () => this.layoutManager.getPaneSpecs(),
221
- getPaneRatiosSignal: () => this._paneRatiosSignal,
210
+ getPaneRatiosSignal: () => this.state.signals.paneRatios,
222
211
  getInternalPaneRatios: () => this.layoutManager.getInternalPaneRatios(),
223
212
  setInternalPaneRatio: (paneId, ratio) => this.layoutManager.setInternalPaneRatio(paneId, ratio),
224
213
  deleteInternalPaneRatio: (paneId) => this.layoutManager.deleteInternalPaneRatio(paneId),
225
214
  applyPaneLayoutSpecs: (specs) => this.layoutManager.applyPaneLayoutSpecs(specs),
226
- getLastVisibleRange: () => this.dataManager.lastVisibleRange,
215
+ getLastVisibleRange: () => this.dataManager.getCurrentVisibleRange() ?? { start: 0, end: 0 },
227
216
  getCrosshairPos: () => this.interaction.crosshairPos,
228
217
  getCrosshairPrice: () => this.interaction.crosshairPrice,
229
218
  getActivePaneId: () => this.interaction.activePaneId,
230
219
  scheduleDraw: (level) => this.scheduleDraw(level),
231
- setPendingIndicatorDataUpdate: (v) => {
232
- this.dataManager.pendingIndicatorDataUpdate = v;
233
- },
234
220
  getRenderContext: (paneId) => this.renderer?.getPaneCtxMap()?.get(paneId) ?? null,
235
221
  addLayer: (layer) => this.renderer?.getScene()?.addLayer(layer),
236
222
  removeLayer: (id) => this.renderer?.getScene()?.removeLayer(id) ?? false,
237
223
  getLayer: (id) => this.renderer?.getScene()?.getLayer(id) ?? null,
238
224
  setLayerVisibility: (id, visible) => this.renderer?.getScene()?.setLayerVisibility(id, visible),
225
+ getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
226
+ getRightAxisWidth: () => this._optionsSignal.peek().rightAxisWidth,
227
+ getPriceLabelWidth: () => this._optionsSignal.peek().priceLabelWidth ?? 60,
228
+ getYPaddingPx: () => this._optionsSignal.peek().yPaddingPx,
239
229
  });
240
230
  // Worker 异步结果就绪后串联 Alert 管线
241
231
  this.indicatorManager.indicatorSchedulerAccessor.setOnResultsApplied(() => {
242
232
  const data = this.dataManager.getInternalData();
243
- this.evaluateAlerts(data, this.dataManager.lastVisibleRange);
233
+ this.evaluateAlerts(data, this.dataManager.getCurrentVisibleRange() ?? { start: 0, end: 0 });
244
234
  });
245
235
  // 初始化渲染器
246
236
  this.renderer = new ChartRenderer({
247
237
  getDom: () => this.dom,
248
- getOption: () => this.opt,
238
+ getOption: () => this._optionsSignal.peek(),
249
239
  getPaneRenderers: () => this.paneRenderers,
250
240
  getInteraction: () => this.interaction,
251
241
  getSharedWebGLSurface: () => this.sharedWebGLSurface,
252
242
  getPluginHost: () => this.pluginHost,
253
243
  getRendererPluginManager: () => this.rendererPluginManager,
254
- getTheme: () => this._themeSignal.peek(),
244
+ getTheme: () => this.state.signals.theme.peek(),
255
245
  getCurrentZoomLevel: () => this.zoomController.currentZoomLevel,
256
246
  getZoomLevelCount: () => this.zoomController.zoomLevelCount,
257
247
  getViewportManager: () => this.viewportManager,
@@ -261,6 +251,7 @@ export class Chart {
261
251
  });
262
252
  this.renderer.registerDrawingPlugins();
263
253
  this.renderer.initCoreRenderers();
254
+ this.syncKWidthKGap();
264
255
  this.viewportManager.init();
265
256
  }
266
257
  getViewport() {
@@ -276,26 +267,34 @@ export class Chart {
276
267
  return;
277
268
  const prev = this._activeMode;
278
269
  if (mode === this._timeShareMode) {
279
- this._modeSavedKWidth = this.opt.kWidth;
280
- this._modeSavedKGap = this.opt.kGap;
281
- this._modeSavedZoomLevel = this.zoomController.currentZoomLevel;
282
- this._savedScaleTypes = new Map();
270
+ this._savedTimeShareState = {
271
+ zoomLevel: this.zoomController.currentZoomLevel,
272
+ scaleTypes: new Map(),
273
+ mainIndicators: [],
274
+ subPanes: [],
275
+ };
283
276
  for (const r of this.paneRenderers) {
284
- this._savedScaleTypes.set(r.getPane().id, r.getPane().yAxis.getScaleType());
277
+ this._savedTimeShareState.scaleTypes.set(r.getPane().id, r.getPane().yAxis.getScaleType());
285
278
  }
279
+ for (const [id, entry] of this.indicatorManager.mainIndicatorsSignalPeek) {
280
+ this._savedTimeShareState.mainIndicators.push({ id, params: { ...entry.params } });
281
+ }
282
+ this._savedTimeShareState.subPanes = this.indicatorManager.getSubPaneEntries().map((e) => ({
283
+ id: e.indicatorId,
284
+ params: { ...e.params },
285
+ }));
286
286
  }
287
287
  else if (prev === this._timeShareMode) {
288
288
  for (const renderer of this.paneRenderers) {
289
289
  const p = renderer.getPane();
290
- const saved = this._savedScaleTypes?.get(p.id) ?? 'linear';
290
+ const saved = this._savedTimeShareState?.scaleTypes.get(p.id) ?? 'linear';
291
291
  p.yAxis.setScaleType(saved);
292
292
  p.yAxis.setBasePrice(null);
293
293
  }
294
- this._savedScaleTypes = undefined;
295
294
  }
296
- if (this._modeSavedKWidth > 0 && mode !== this._timeShareMode) {
297
- this.applyRenderState(this._modeSavedKWidth, this._modeSavedKGap, this._modeSavedZoomLevel);
298
- this._modeSavedKWidth = 0;
295
+ if (this._savedTimeShareState && mode !== this._timeShareMode) {
296
+ this.zoomController.setZoomLevel(this._savedTimeShareState.zoomLevel);
297
+ this.syncKWidthKGap();
299
298
  }
300
299
  prev.onDeactivate({
301
300
  enableMainIndicator: (id, p) => this.enableMainIndicator(id, p),
@@ -311,6 +310,28 @@ export class Chart {
311
310
  dataManager: this.dataManager,
312
311
  currentPeriod: this.dataManager.currentPeriod,
313
312
  }, prev);
313
+ if (mode === this._timeShareMode) {
314
+ for (const { id } of this._savedTimeShareState.mainIndicators) {
315
+ if (id !== 'TIMESHARE') {
316
+ this.indicatorManager.disableMainIndicator(id);
317
+ }
318
+ }
319
+ this.indicatorManager.clearSubPanes();
320
+ }
321
+ else if (prev === this._timeShareMode) {
322
+ const saved = this._savedTimeShareState;
323
+ if (saved) {
324
+ for (const { id, params } of saved.mainIndicators) {
325
+ if (id !== 'TIMESHARE') {
326
+ this.indicatorManager.enableMainIndicator(id, params);
327
+ }
328
+ }
329
+ for (const { id, params } of saved.subPanes) {
330
+ this.indicatorManager.addIndicator(id, 'sub', params);
331
+ }
332
+ }
333
+ this._savedTimeShareState = null;
334
+ }
314
335
  }
315
336
  getCurrentDpr() {
316
337
  return this.viewportManager.getEffectiveDpr();
@@ -391,27 +412,34 @@ export class Chart {
391
412
  this.renderer.draw(level);
392
413
  }
393
414
  // ========== Render State API (Vue SSOT) ==========
415
+ syncKWidthKGap() {
416
+ const kw = this.zoomController.currentKWidth;
417
+ const kg = this.zoomController.currentKGap;
418
+ const current = this._optionsSignal.peek();
419
+ if (current.kWidth !== kw || current.kGap !== kg) {
420
+ this._optionsSignal.set({ ...current, kWidth: kw, kGap: kg });
421
+ }
422
+ this.scheduleDraw();
423
+ }
394
424
  /**
395
- * 应用渲染状态(由 Vue/Store 层在状态更新后调用)
396
- * Chart 不拥有业务 SSOT,只负责接收参数并渲染
397
- * 这是写入 opt.kWidth/kGap currentZoomLevel 的唯一入口
425
+ * 应用渲染状态
426
+ * kWidth/kGap/zoomLevel zoomController 统一管理(SSOT)。
427
+ * zoomLevel 提供时,kWidth/kGap zoomController 从 zoomLevel 推导。
428
+ * 当 zoomLevel 不提供时(如分时图),直接设置 kWidth/kGap。
398
429
  */
399
430
  applyRenderState(kWidth, kGap, zoomLevel) {
400
- const nextZoomLevel = zoomLevel !== undefined
401
- ? Math.max(1, Math.min(this.zoomController.zoomLevelCount, zoomLevel))
402
- : this.zoomController.currentZoomLevel;
403
- const renderStateChanged = this.opt.kWidth !== kWidth ||
404
- this.opt.kGap !== kGap ||
405
- this.zoomController.currentZoomLevel !== nextZoomLevel;
406
- if (!renderStateChanged) {
407
- return;
408
- }
409
- this.opt = { ...this.opt, kWidth, kGap };
410
431
  if (zoomLevel !== undefined) {
411
- this.zoomController.setZoomLevel(nextZoomLevel);
432
+ const clamped = Math.max(1, Math.min(this.zoomController.zoomLevelCount, zoomLevel));
433
+ if (this.zoomController.currentZoomLevel === clamped)
434
+ return;
435
+ this.zoomController.setZoomLevel(clamped);
412
436
  }
413
- this.updateViewportSignal();
414
- this.scheduleDraw();
437
+ else {
438
+ if (this.zoomController.currentKWidth === kWidth && this.zoomController.currentKGap === kGap)
439
+ return;
440
+ this.zoomController.setKWidthKGap(kWidth, kGap);
441
+ }
442
+ this.syncKWidthKGap();
415
443
  }
416
444
  /** 获取总缩放级别数 */
417
445
  getZoomLevelCount() {
@@ -441,7 +469,7 @@ export class Chart {
441
469
  }
442
470
  /** 获取当前 ChartOptions(返回内部当前快照) */
443
471
  getOption() {
444
- return this.opt;
472
+ return this._optionsSignal.peek();
445
473
  }
446
474
  /**
447
475
  * 更新配置并触发布局/重绘
@@ -458,11 +486,11 @@ export class Chart {
458
486
  }
459
487
  if (partial.panes) {
460
488
  const nextPanes = partial.panes.map((pane) => ({ ...pane }));
461
- this.opt = { ...this.opt, ...partial, panes: nextPanes };
489
+ this._optionsSignal.set({ ...this._optionsSignal.peek(), ...partial, panes: nextPanes });
462
490
  this.layoutManager.applyPaneLayoutSpecs(nextPanes);
463
491
  return;
464
492
  }
465
- this.opt = { ...this.opt, ...partial };
493
+ this._optionsSignal.set({ ...this._optionsSignal.peek(), ...partial });
466
494
  this.resize();
467
495
  }
468
496
  updatePaneLayout(panes) {
@@ -483,7 +511,7 @@ export class Chart {
483
511
  /** 更新绘图对象 */
484
512
  setDrawings(drawings) {
485
513
  this.renderer.getDrawingStore().setAll(drawings);
486
- this._drawingsSignal.set(drawings);
514
+ this.state.signals.drawings.set(drawings);
487
515
  this.scheduleDraw();
488
516
  }
489
517
  /** 更新选中的绘图 ID */
@@ -719,7 +747,7 @@ export class Chart {
719
747
  }
720
748
  /** 容器尺寸变化时调用 */
721
749
  resize() {
722
- if (this._activeMode.debugName === 'TimeShare') {
750
+ if (this._activeMode === this._timeShareMode) {
723
751
  const tsData = this.dataManager.getTimeShareData();
724
752
  const vp = this.viewportManager.computeViewport();
725
753
  if (!vp || vp.plotWidth <= 0)
@@ -728,12 +756,8 @@ export class Chart {
728
756
  const result = this._activeMode.computeKWidth(tsData.length, vp.plotWidth, vp.dpr);
729
757
  if (result) {
730
758
  this.applyRenderState(result.kWidth, result.kGap);
731
- const container = this.dom.container;
732
- if (container) {
733
- const leftBuffer = this.dataManager.getLeftLoadBufferWidth();
734
- this.viewportManager.setScrollLeft(leftBuffer);
735
- this.viewportManager.applyPendingScrollLeft(container);
736
- }
759
+ const leftBuffer = this.dataManager.getLeftLoadBufferWidth();
760
+ this.viewportManager.setScrollLeft(leftBuffer);
737
761
  }
738
762
  }
739
763
  this.renderer.clearCachedFrame();
@@ -774,26 +798,28 @@ export class Chart {
774
798
  return this.viewportManager.computeViewport();
775
799
  }
776
800
  // ==================== Facade API (High-level interface for adapters) ====================
777
- _themeSignal = createSignal('light');
778
- _drawingToolSignal = createSignal(null);
779
- _drawingsSignal = createSignal([]);
780
- _paneRatiosSignal = createSignal({});
781
- _paneLayoutSignal = createSignal([]);
782
- _interactionSignal = createSignal({
783
- crosshairPos: null,
784
- crosshairIndex: null,
785
- crosshairPrice: null,
786
- hoveredIndex: null,
787
- activePaneId: null,
788
- tooltipPos: { x: 0, y: 0 },
789
- tooltipAnchorPlacement: 'right-bottom',
790
- hoveredMarkerData: null,
791
- hoveredCustomMarker: null,
792
- isDragging: false,
793
- isResizingPaneBoundary: false,
794
- isHoveringPaneBoundary: false,
795
- hoveredPaneBoundaryId: null,
796
- isHoveringRightAxis: false,
801
+ state = createStateStore({
802
+ theme: 'light',
803
+ drawingTool: null,
804
+ drawings: [],
805
+ paneRatios: {},
806
+ paneLayout: [],
807
+ interaction: {
808
+ crosshairPos: null,
809
+ crosshairIndex: null,
810
+ crosshairPrice: null,
811
+ hoveredIndex: null,
812
+ activePaneId: null,
813
+ tooltipPos: { x: 0, y: 0 },
814
+ tooltipAnchorPlacement: 'right-bottom',
815
+ hoveredMarkerData: null,
816
+ hoveredCustomMarker: null,
817
+ isDragging: false,
818
+ isResizingPaneBoundary: false,
819
+ isHoveringPaneBoundary: false,
820
+ hoveredPaneBoundaryId: null,
821
+ isHoveringRightAxis: false,
822
+ },
797
823
  });
798
824
  /** 视口状态信号 */
799
825
  get viewport() {
@@ -829,7 +855,7 @@ export class Chart {
829
855
  }
830
856
  /** 主题信号 */
831
857
  get theme() {
832
- return this._themeSignal;
858
+ return this.state.signals.theme;
833
859
  }
834
860
  /** 指标实例列表信号(派生信号,自动随主/副图状态更新) */
835
861
  get indicators() {
@@ -841,22 +867,22 @@ export class Chart {
841
867
  }
842
868
  /** 当前绘图工具信号 */
843
869
  get drawingTool() {
844
- return this._drawingToolSignal;
870
+ return this.state.signals.drawingTool;
845
871
  }
846
872
  /** 绘图对象列表信号 */
847
873
  get drawings() {
848
- return this._drawingsSignal;
874
+ return this.state.signals.drawings;
849
875
  }
850
876
  /** 面板比例信号 */
851
877
  get paneRatios() {
852
- return this._paneRatiosSignal;
878
+ return this.state.signals.paneRatios;
853
879
  }
854
880
  get paneLayout() {
855
- return this._paneLayoutSignal;
881
+ return this.state.signals.paneLayout;
856
882
  }
857
883
  /** 交互状态信号 */
858
884
  get interactionState() {
859
- return this._interactionSignal;
885
+ return this.state.signals.interaction;
860
886
  }
861
887
  // ---------- Data ----------
862
888
  setData(data) {
@@ -879,9 +905,17 @@ export class Chart {
879
905
  this._lastAlertTimestamp = null;
880
906
  const primaryPeriod = specs[0]?.period;
881
907
  if (primaryPeriod) {
908
+ // ⚠️ setActiveMode 必须在 dataManager.setSymbols 之前调用,
909
+ // 以确保 kWidth/kGap(从 zoom level 恢复)先写入 _optionsSignal,
910
+ // 后续 scrollLeft 恢复才能正确反推物理像素偏移。
882
911
  this.setActiveMode(primaryPeriod === 'timeshare' ? this._timeShareMode : this._kLineMode);
883
912
  }
884
913
  this.dataManager.setSymbols(specs);
914
+ // Scroll position 恢复必须放在 setActiveMode + setSymbols 之后,
915
+ // 此时 kWidth/kGap 已由 zoom level 恢复写回,计算不出错。
916
+ if (primaryPeriod && primaryPeriod !== 'timeshare') {
917
+ this.dataManager.tryRestoreScrollFromSnapshot();
918
+ }
885
919
  }
886
920
  addComparisonSymbol(spec) {
887
921
  this.dataManager.addComparisonSymbol(spec);
@@ -918,7 +952,7 @@ export class Chart {
918
952
  * 设置主题(高层 API)
919
953
  */
920
954
  setTheme(theme) {
921
- this._themeSignal.set(theme);
955
+ this.state.set.theme(theme);
922
956
  this.scheduleDraw();
923
957
  }
924
958
  // ---------- Zoom ----------
@@ -1030,7 +1064,7 @@ export class Chart {
1030
1064
  * 更新缓存的 scrollLeft 并触发交互 controller
1031
1065
  */
1032
1066
  handleScrollEvent() {
1033
- this.interaction.onScroll({ scheduleDraw: !this.dataManager.pendingIndicatorDataUpdate });
1067
+ this.interaction.onScroll({ scheduleDraw: !this.indicatorManager.indicatorSchedulerAccessor.busySignal.peek() });
1034
1068
  // 更新 viewport signal 中的 visible range
1035
1069
  this.updateViewportSignal();
1036
1070
  }
@@ -1086,7 +1120,7 @@ export class Chart {
1086
1120
  * @param tool 工具类型或 null 取消选择
1087
1121
  */
1088
1122
  setDrawingTool(tool) {
1089
- this._drawingToolSignal.set(tool);
1123
+ this.state.set.drawingTool(tool);
1090
1124
  // TODO: 当 Chart 支持绘图工具切换时,在这里调用相应方法
1091
1125
  }
1092
1126
  /**