@363045841yyt/klinechart-core 0.8.1-alpha.3 → 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 (130) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +30 -4
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +9 -2
  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 +6 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +88 -47
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/index.d.ts +1 -0
  13. package/dist/data-fetchers/index.d.ts.map +1 -1
  14. package/dist/data-fetchers/index.js +1 -0
  15. package/dist/data-fetchers/index.js.map +1 -1
  16. package/dist/data-fetchers/router.d.ts.map +1 -1
  17. package/dist/data-fetchers/router.js +3 -0
  18. package/dist/data-fetchers/router.js.map +1 -1
  19. package/dist/data-fetchers/tradingview.d.ts +3 -0
  20. package/dist/data-fetchers/tradingview.d.ts.map +1 -0
  21. package/dist/data-fetchers/tradingview.js +45 -0
  22. package/dist/data-fetchers/tradingview.js.map +1 -0
  23. package/dist/engine/chart.d.ts +34 -351
  24. package/dist/engine/chart.d.ts.map +1 -1
  25. package/dist/engine/chart.js +246 -1716
  26. package/dist/engine/chart.js.map +1 -1
  27. package/dist/engine/chartContext.d.ts +24 -0
  28. package/dist/engine/chartContext.d.ts.map +1 -0
  29. package/dist/engine/chartContext.js +19 -0
  30. package/dist/engine/chartContext.js.map +1 -0
  31. package/dist/engine/chartTypes.d.ts +77 -0
  32. package/dist/engine/chartTypes.d.ts.map +1 -0
  33. package/dist/engine/chartTypes.js +2 -0
  34. package/dist/engine/chartTypes.js.map +1 -0
  35. package/dist/engine/controller/interaction.d.ts +1 -0
  36. package/dist/engine/controller/interaction.d.ts.map +1 -1
  37. package/dist/engine/controller/interaction.js +9 -2
  38. package/dist/engine/controller/interaction.js.map +1 -1
  39. package/dist/engine/data/chartDataManager.d.ts +102 -0
  40. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  41. package/dist/engine/data/chartDataManager.js +590 -0
  42. package/dist/engine/data/chartDataManager.js.map +1 -0
  43. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  44. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  45. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  46. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  47. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  48. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  49. package/dist/engine/layout/chartPaneLayout.js +388 -0
  50. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  51. package/dist/engine/render/chartRenderer.d.ts +86 -0
  52. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  53. package/dist/engine/render/chartRenderer.js +438 -0
  54. package/dist/engine/render/chartRenderer.js.map +1 -0
  55. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  56. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  57. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  58. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  59. package/dist/engine/renderers/comparisonLine.js +25 -11
  60. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  61. package/dist/engine/subPaneManager.d.ts +27 -6
  62. package/dist/engine/subPaneManager.d.ts.map +1 -1
  63. package/dist/engine/subPaneManager.js +54 -56
  64. package/dist/engine/subPaneManager.js.map +1 -1
  65. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  66. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  67. package/dist/engine/utils/chartZoomController.js +66 -0
  68. package/dist/engine/utils/chartZoomController.js.map +1 -0
  69. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  70. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  71. package/dist/engine/viewport/chartViewportManager.js +249 -0
  72. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  73. package/dist/engine/viewport/viewport.js +1 -1
  74. package/dist/engine/viewport/viewport.js.map +1 -1
  75. package/dist/plugin/types.d.ts +1 -0
  76. package/dist/plugin/types.d.ts.map +1 -1
  77. package/dist/plugin/types.js.map +1 -1
  78. package/dist/tokens/theme-china.d.ts.map +1 -1
  79. package/dist/tokens/theme-china.js +0 -4
  80. package/dist/tokens/theme-china.js.map +1 -1
  81. package/dist/tokens/theme-dark.d.ts.map +1 -1
  82. package/dist/tokens/theme-dark.js +0 -4
  83. package/dist/tokens/theme-dark.js.map +1 -1
  84. package/dist/tokens/theme-light.d.ts.map +1 -1
  85. package/dist/tokens/theme-light.js +1 -5
  86. package/dist/tokens/theme-light.js.map +1 -1
  87. package/dist/tokens/types.d.ts +0 -4
  88. package/dist/tokens/types.d.ts.map +1 -1
  89. package/dist/types/price.d.ts +2 -0
  90. package/dist/types/price.d.ts.map +1 -1
  91. package/dist/types/price.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.d.ts.map +1 -1
  94. package/dist/version.js +1 -1
  95. package/dist/version.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/controllers/createChartController.ts +49 -13
  98. package/src/controllers/types.ts +9 -2
  99. package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
  100. package/src/data-fetchers/baostock.ts +3 -3
  101. package/src/data-fetchers/dataBuffer.ts +70 -22
  102. package/src/data-fetchers/index.ts +1 -0
  103. package/src/data-fetchers/router.ts +3 -0
  104. package/src/data-fetchers/tradingview.ts +48 -0
  105. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  106. package/src/engine/chart.ts +260 -2103
  107. package/src/engine/chartContext.ts +34 -0
  108. package/src/engine/chartTypes.ts +88 -0
  109. package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
  110. package/src/engine/controller/interaction.ts +10 -2
  111. package/src/engine/data/chartDataManager.ts +691 -0
  112. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  113. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  114. package/src/engine/layout/chartPaneLayout.ts +474 -0
  115. package/src/engine/render/chartRenderer.ts +579 -0
  116. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  117. package/src/engine/renderers/comparisonLine.ts +25 -11
  118. package/src/engine/subPaneManager.ts +75 -59
  119. package/src/engine/utils/chartZoomController.ts +104 -0
  120. package/src/engine/viewport/chartViewportManager.ts +310 -0
  121. package/src/engine/viewport/viewport.ts +1 -1
  122. package/src/plugin/types.ts +1 -0
  123. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  124. package/src/tokens/theme-china.ts +0 -4
  125. package/src/tokens/theme-dark.ts +0 -4
  126. package/src/tokens/theme-light.ts +2 -6
  127. package/src/tokens/types.ts +0 -4
  128. package/src/types/price.ts +2 -0
  129. package/src/version.ts +1 -1
  130. package/src/engine/chart.d.ts +0 -619
@@ -1,116 +1,39 @@
1
- import { createSignal, computed } from '../reactivity/signal';
2
- import { DataBuffer } from '../data-fetchers/dataBuffer';
3
- import { getVisibleRange } from './viewport/viewport';
4
- import { Pane, UpdateLevel } from './layout/pane';
1
+ import { createSignal } from '../reactivity/signal';
2
+ import { ChartDataManager } from './data/chartDataManager';
3
+ import { ChartPaneLayout } from './layout/chartPaneLayout';
4
+ import { UpdateLevel } from './layout/pane';
5
5
  import { InteractionController } from './controller/interaction';
6
- import { PaneRenderer } from './paneRenderer';
7
6
  import { SharedWebGLSurface } from './renderers/webgl/sharedWebGLSurface';
8
- import { MarkerManager } from './marker/registry';
9
- import { getPhysicalKLineConfig, calcKWidthPx } from './utils/klineConfig';
10
- import { computeZoom } from './utils/zoom';
11
- import { IndicatorScheduler } from './indicators/scheduler';
12
- import { getBuiltinIndicatorDefinitions } from './indicators/registerBuiltins';
13
- import { getRegisteredIndicatorDefinitions } from './indicators/indicatorDefinitionRegistry';
14
- import { SubPaneManager } from './subPaneManager';
7
+ import { getPhysicalKLineConfig } from './utils/klineConfig';
8
+ import { ChartZoomController } from './utils/chartZoomController';
9
+ import { ChartViewportManager } from './viewport/chartViewportManager';
10
+ import { ChartIndicatorManager } from './indicators/chartIndicatorManager';
11
+ import { ChartRenderer } from './render/chartRenderer';
15
12
  import { createPluginHost, RendererPluginManager, wrapPaneInfo, } from '../plugin';
16
- import { createSubIndicatorRenderer } from './renderers/Indicator';
17
- import { createMainIndicatorLegendRendererPlugin } from './renderers/Indicator/mainIndicatorLegend';
18
- import { DrawingStore } from './drawing';
19
- import { createDrawingRendererPlugin, createDrawingLabelOverlayPlugin } from './drawing/plugin';
20
- import { createGridLinesRendererPlugin } from './renderers/gridLines';
21
- import { createCandleRenderer } from './renderers/candle';
22
- import { createComparisonLineRenderer } from './renderers/comparisonLine';
23
- import { createLastPriceLineRendererPlugin, createLastPriceLabelRegistrarPlugin } from './renderers/lastPrice';
24
- import { createCustomMarkersRenderer } from './renderers/customMarkers';
25
- import { createExtremaMarkersRendererPlugin } from './renderers/extremaMarkers';
26
- import { createYAxisRendererPlugin } from './renderers/yAxis';
27
- import { createCrosshairRendererPlugin } from './renderers/crosshair';
28
- import { createTimeAxisRendererPlugin } from './renderers/timeAxis';
29
13
  // 重新导出以保持向后兼容
30
- export { getPhysicalKLineConfig, calcKWidthPx };
14
+ export { getPhysicalKLineConfig };
31
15
  export class Chart {
32
16
  dom;
33
17
  opt;
34
- _internalData = [];
35
- _dataFetcher = null;
36
- _dataBuffer = new DataBuffer();
37
- _dataBufferUnsub = null;
38
- _comparisonSpecs = [];
39
- _comparisonData = new Map();
40
- _comparisonBuffers = new Map();
41
- _comparisonBufferUnsubs = new Map();
42
- raf = null;
43
- pendingUpdateLevel = UpdateLevel.All;
44
- _internalViewport = null;
45
- paneRenderers = [];
46
- markerManager;
47
- drawingStore = new DrawingStore();
18
+ dataManager;
19
+ viewportManager;
20
+ layoutManager;
21
+ get paneRenderers() {
22
+ return this.layoutManager.getPaneRenderers();
23
+ }
48
24
  interaction;
49
25
  /** 插件宿主 */
50
26
  pluginHost;
51
27
  /** 渲染器插件管理器 */
52
28
  rendererPluginManager;
53
- /** 精确 DPR(来自 ResizeObserver 的 devicePixelContentBoxSize) */
54
- preciseDpr = 0;
55
- /** 统一监听容器尺寸与 DPR 变化 */
56
- resizeObserver;
57
- /** scroll 事件处理器引用(用于 cleanup) */
58
- onScroll;
59
- /** 最近一次观测到的容器尺寸 */
60
- observedSize = { width: 0, height: 0 };
61
- /** 缓存的 scrollLeft(通过 scroll 事件同步,避免每帧读取 DOM 触发强制回流) */
62
- cachedScrollLeft = 0;
63
- /** overlay 上一帧是否有十字线(用于判断何时需要清除) */
64
- overlayHadCrosshair = false;
65
- /** 用户设置配置(传递给渲染器) */
66
- settings = {};
67
- /** pane ratio 状态(按 paneId 维护,sum=1 仅对可见 pane) */
68
- _internalPaneRatios = new Map();
69
- /** 共享 X 轴上下文缓存 */
70
- xAxisCtx = null;
71
29
  /** Chart 级共享 WebGL canvas/context */
72
30
  sharedWebGLSurface;
73
- /** 当前缩放级别(1 ~ zoomLevelCount) */
74
- currentZoomLevel = 1;
75
- /** 缩放级别总数 */
76
- zoomLevelCount;
77
- /** 指标调度器(负责计算 MA 等指标并写入 StateStore)
78
- * TODO: 阶段5迁移为插件注册,Scheduler 通过事件监听 data/viewport 变更,Chart 不直接持有
79
- */
80
- indicatorScheduler;
81
- /** 数据已更新但 Worker 指标尚未回写,期间避免用旧指标 state 绘制中间帧 */
82
- pendingIndicatorDataUpdate = false;
83
- /** 上次可见范围(用于检测视口变化) */
84
- lastVisibleRange = { start: 0, end: 0 };
85
- /** Overlay 帧复用的最近主渲染结果 */
86
- cachedDrawFrame = null;
87
- /** 副图管理器 */
88
- subPaneManager = new SubPaneManager();
89
- /** 主图指标激活状态与参数(存在即激活,默认参数在 enable 时初始化) */
90
- _mainIndicatorsSignal = createSignal(new Map());
91
- /** 主图指标默认参数(从注册表中懒加载) */
92
- static _defaultMainParamsCache = null;
93
- static get DEFAULT_MAIN_PARAMS() {
94
- if (Chart._defaultMainParamsCache === null) {
95
- Chart._defaultMainParamsCache = {};
96
- for (const def of getRegisteredIndicatorDefinitions()) {
97
- if (def.category === 'main') {
98
- Chart._defaultMainParamsCache[def.displayName.toUpperCase()] = (def.runtime?.defaultConfig ?? {});
99
- }
100
- }
101
- }
102
- return Chart._defaultMainParamsCache;
103
- }
104
- /** 可启用的主图指标白名单(从注册表中懒加载) */
105
- static _enableMainIndicatorsCache = null;
106
- static get ENABLE_MAIN_INDICATORS() {
107
- if (Chart._enableMainIndicatorsCache === null) {
108
- Chart._enableMainIndicatorsCache = getRegisteredIndicatorDefinitions()
109
- .filter(d => d.category === 'main')
110
- .map(d => d.displayName.toUpperCase());
111
- }
112
- return Chart._enableMainIndicatorsCache;
113
- }
31
+ /** 缩放控制器 */
32
+ zoomController;
33
+ /** 指标管理器 */
34
+ indicatorManager;
35
+ /** 渲染器 */
36
+ renderer;
114
37
  /**
115
38
  * 启用主图指标
116
39
  * @param indicatorId 指标ID
@@ -118,189 +41,34 @@ export class Chart {
118
41
  * @returns 是否成功启用
119
42
  */
120
43
  enableMainIndicator(indicatorId, params) {
121
- const id = indicatorId.toUpperCase();
122
- if (!Chart.ENABLE_MAIN_INDICATORS.includes(id)) {
123
- console.warn(`[Chart] 未知的主图指标: ${indicatorId}`);
124
- return false;
125
- }
126
- const map = this._mainIndicatorsSignal.peek();
127
- const existing = map.get(id);
128
- if (existing) {
129
- // 已启用,更新参数
130
- if (params) {
131
- const next = new Map(map);
132
- next.set(id, { params: { ...existing.params, ...params } });
133
- this._mainIndicatorsSignal.set(next);
134
- this.updateIndicatorSchedulerConfig(id);
135
- }
136
- return true;
137
- }
138
- // 合并默认参数和传入参数
139
- const defaults = Chart.DEFAULT_MAIN_PARAMS[id] ?? {};
140
- const merged = params ? { ...defaults, ...params } : defaults;
141
- const next = new Map(map);
142
- next.set(id, { params: merged });
143
- this._mainIndicatorsSignal.set(next);
144
- // 启用对应的渲染器
145
- this.enableMainIndicatorRenderer(id);
146
- // 更新调度器配置(触发异步重算)
147
- this.updateIndicatorSchedulerConfig(id);
148
- // 同步重算主图状态:latestResult 已有该指标的 series,只是没注册到 registry
149
- // 补调 updateVisibleRange 使其走 updateVisibleStatesOnly,立即从 latestResult 合成极值
150
- this.indicatorScheduler.updateVisibleRange(this.lastVisibleRange);
151
- this.scheduleDraw();
152
- return true;
44
+ return this.indicatorManager.enableMainIndicator(indicatorId, params);
153
45
  }
154
- /**
155
- * 禁用主图指标
156
- * @param indicatorId 指标ID
157
- * @returns 是否成功禁用
158
- */
159
46
  disableMainIndicator(indicatorId) {
160
- const id = indicatorId.toUpperCase();
161
- const map = this._mainIndicatorsSignal.peek();
162
- if (!map.has(id))
163
- return false;
164
- const next = new Map(map);
165
- next.delete(id);
166
- this._mainIndicatorsSignal.set(next);
167
- // 禁用对应的渲染器
168
- this.disableMainIndicatorRenderer(id);
169
- // 更新调度器配置
170
- this.updateIndicatorSchedulerConfig(id);
171
- this.scheduleDraw();
172
- return true;
47
+ return this.indicatorManager.disableMainIndicator(indicatorId);
173
48
  }
174
- /**
175
- * 切换主图指标启用状态
176
- * @param indicatorId 指标ID
177
- * @param enabled 是否启用
178
- */
179
49
  toggleMainIndicator(indicatorId, enabled) {
180
- if (enabled) {
181
- this.enableMainIndicator(indicatorId);
182
- }
183
- else {
184
- this.disableMainIndicator(indicatorId);
185
- }
50
+ this.indicatorManager.toggleMainIndicator(indicatorId, enabled);
186
51
  }
187
- /**
188
- * 获取当前激活的主图指标列表
189
- * @returns 激活的指标ID数组
190
- */
191
52
  getActiveMainIndicators() {
192
- return [...this._mainIndicatorsSignal.peek().keys()];
53
+ return this.indicatorManager.getActiveMainIndicators();
193
54
  }
194
- /**
195
- * 检查主图指标是否激活
196
- * @param indicatorId 指标ID
197
- */
198
55
  isMainIndicatorActive(indicatorId) {
199
- return this._mainIndicatorsSignal.peek().has(indicatorId.toUpperCase());
56
+ return this.indicatorManager.isMainIndicatorActive(indicatorId);
200
57
  }
201
- /**
202
- * 更新主图指标参数
203
- * @param indicatorId 指标ID
204
- * @param params 参数对象
205
- */
206
58
  updateMainIndicatorParams(indicatorId, params) {
207
- const id = indicatorId.toUpperCase();
208
- const map = this._mainIndicatorsSignal.peek();
209
- const entry = map.get(id);
210
- if (!entry)
211
- return;
212
- const merged = { ...entry.params, ...params };
213
- const next = new Map(map);
214
- next.set(id, { params: merged });
215
- this._mainIndicatorsSignal.set(next);
216
- // 同步更新渲染器配置
217
- const rendererName = id.toLowerCase();
218
- const renderer = this.getRenderer(rendererName);
219
- if (renderer && renderer.setConfig) {
220
- renderer.setConfig(merged);
221
- }
222
- // 更新调度器
223
- this.updateIndicatorSchedulerConfig(id);
224
- this.scheduleDraw();
59
+ this.indicatorManager.updateMainIndicatorParams(indicatorId, params);
225
60
  }
226
- /**
227
- * 获取主图指标参数
228
- * @param indicatorId 指标ID
229
- */
230
61
  getMainIndicatorParams(indicatorId) {
231
- return this._mainIndicatorsSignal.peek().get(indicatorId.toUpperCase())?.params ?? null;
62
+ return this.indicatorManager.getMainIndicatorParams(indicatorId);
232
63
  }
233
- /**
234
- * 清除所有主图指标
235
- */
236
64
  clearMainIndicators() {
237
- const map = this._mainIndicatorsSignal.peek();
238
- for (const id of map.keys()) {
239
- this.disableMainIndicatorRenderer(id);
240
- }
241
- this._mainIndicatorsSignal.set(new Map());
242
- this.scheduleDraw();
243
- }
244
- /**
245
- * 启用主图指标渲染器(内部方法)
246
- */
247
- enableMainIndicatorRenderer(indicatorId) {
248
- const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId);
249
- const mainPane = definition?.mainPane;
250
- if (!definition || !mainPane)
251
- return;
252
- if (!this.getRenderer(mainPane.rendererName)) {
253
- this.useRenderer(definition.rendererFactory({ paneId: 'main', indicatorId }));
254
- }
255
- this.setRendererEnabled(mainPane.rendererName, true);
256
- if (!this.getRenderer('mainIndicatorLegend')) {
257
- this.useRenderer(createMainIndicatorLegendRendererPlugin({ yPaddingPx: this.opt.yPaddingPx }));
258
- }
259
- }
260
- /**
261
- * 禁用主图指标渲染器(内部方法)
262
- */
263
- disableMainIndicatorRenderer(indicatorId) {
264
- const rendererName = this.indicatorScheduler.getIndicatorMetadata(indicatorId)?.mainPane?.rendererName;
265
- if (rendererName) {
266
- this.setRendererEnabled(rendererName, false);
267
- }
268
- }
269
- /**
270
- * 更新调度器配置(内部方法)
271
- */
272
- updateIndicatorSchedulerConfig(indicatorId) {
273
- const entry = this._mainIndicatorsSignal.peek().get(indicatorId);
274
- const isActive = entry !== undefined;
275
- const params = entry?.params ?? {};
276
- const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId);
277
- const toActiveConfig = definition?.mainPane?.toActiveConfig;
278
- if (!definition?.updateConfig || !toActiveConfig)
279
- return;
280
- const config = toActiveConfig(params, isActive);
281
- if (config !== null) {
282
- definition.updateConfig(this.indicatorScheduler, config, 'main');
283
- }
65
+ this.indicatorManager.clearMainIndicators();
284
66
  }
285
67
  /**
286
68
  * @deprecated 使用 enableMainIndicator/disableMainIndicator 替代
287
69
  */
288
70
  setActiveMainIndicators(indicators) {
289
- // 计算需要启用和禁用的指标
290
- const newSet = new Set(indicators.map(i => i.toUpperCase()));
291
- const currentSet = new Set(this._mainIndicatorsSignal.peek().keys());
292
- // 禁用不再激活的
293
- for (const id of currentSet) {
294
- if (!newSet.has(id)) {
295
- this.disableMainIndicator(id);
296
- }
297
- }
298
- // 启用新激活的
299
- for (const id of newSet) {
300
- if (!currentSet.has(id)) {
301
- this.enableMainIndicator(id);
302
- }
303
- }
71
+ this.indicatorManager.setActiveMainIndicators(indicators);
304
72
  }
305
73
  /**
306
74
  * 创建图表实例
@@ -316,165 +84,136 @@ export class Chart {
316
84
  this.interaction.setOnInteractionChange((snapshot) => {
317
85
  this._interactionSignal.set(snapshot);
318
86
  });
319
- this.markerManager = new MarkerManager();
320
87
  this.pluginHost = createPluginHost();
321
88
  this.rendererPluginManager = new RendererPluginManager();
322
89
  this.sharedWebGLSurface = new SharedWebGLSurface();
323
90
  // 注入依赖
324
91
  this.rendererPluginManager.setPluginHost(this.pluginHost);
325
92
  this.rendererPluginManager.setInvalidateCallback(() => this.scheduleDraw());
326
- this.syncPaneRatiosFromSpecs(this.opt.panes);
327
- // 缩放级别由外部 SSOT 管理,Chart 只接收不计算
328
- this.zoomLevelCount = Math.max(2, Math.round(this.opt.zoomLevels ?? 20));
329
- this.currentZoomLevel = this.opt.initialZoomLevel ?? 1;
330
- this.currentZoomLevel = Math.max(1, Math.min(this.zoomLevelCount, this.currentZoomLevel));
331
- // 注意:初始 kWidth/kGap 应由外部通过 applyRenderState() 传入
332
- // 初始化指标调度器
333
- this.indicatorScheduler = new IndicatorScheduler();
334
- this.indicatorScheduler.setPluginHost(this.pluginHost);
335
- for (const definition of getBuiltinIndicatorDefinitions()) {
336
- this.indicatorScheduler.registerIndicator(definition);
337
- }
338
- this.indicatorScheduler.setInvalidateCallback(() => {
339
- this.pendingIndicatorDataUpdate = false;
340
- this.scheduleDraw();
93
+ this.viewportManager = new ChartViewportManager({
94
+ getDom: () => this.dom,
95
+ getBottomAxisHeight: () => this.opt.bottomAxisHeight,
96
+ getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
97
+ getZoomLevel: () => this.zoomController.currentZoomLevel,
98
+ getLastVisibleRange: () => this.dataManager.lastVisibleRange,
99
+ getKWidth: () => this.opt.kWidth,
100
+ getKGap: () => this.opt.kGap,
101
+ scheduleDraw: (level) => this.scheduleDraw(level),
102
+ onResizeCompleted: () => { this.resize(); },
103
+ resizeSharedWebGLSurface: (plotWidth, plotHeight, dpr) => this.sharedWebGLSurface.resize(plotWidth, plotHeight, dpr),
341
104
  });
342
- // 注册副图活跃列表提供者,调度器据此只计算启用的副图
343
- this.indicatorScheduler.setActiveSubPaneProvider(() => this.subPaneManager.getPaneIds());
344
- this.initPanes();
345
- // dev: 主副图状态变更日志
346
- if (import.meta.env?.MODE !== 'production') {
347
- this._indicatorsComputed.subscribe(() => {
348
- const instances = this._indicatorsComputed.peek();
349
- console.log('[Chart] indicators signal changed:', instances);
350
- });
351
- this._subPanesComputed.subscribe(() => {
352
- const subPanes = this._subPanesComputed.peek();
353
- console.log('[Chart] subPanes signal changed:', subPanes);
354
- });
355
- }
356
- // 注册绘图主插件(负责绘制 shape,layer: 'main')
357
- this.useRenderer(createDrawingRendererPlugin({ store: this.drawingStore }));
358
- // 注册绘图标签插件(负责推送选中绘图的轴标签,layer: 'overlay')
359
- // 注意:此插件依赖 overlay 更新级别,若将来添加 Main 级别需调整
360
- this.useRenderer(createDrawingLabelOverlayPlugin({ store: this.drawingStore }));
361
- this.initCoreRenderers();
362
- this.initResizeObserver();
363
- }
364
- initCoreRenderers() {
365
- const axisWidth = this.opt.rightAxisWidth + (this.opt.priceLabelWidth ?? 0);
366
- this.useRenderer(createGridLinesRendererPlugin());
367
- this.useRenderer(createCandleRenderer());
368
- this.useRenderer(createComparisonLineRenderer());
369
- this.useRenderer(createLastPriceLineRendererPlugin());
370
- this.useRenderer(createLastPriceLabelRegistrarPlugin());
371
- this.useRenderer(createCustomMarkersRenderer());
372
- this.useRenderer(createExtremaMarkersRendererPlugin());
373
- this.useRenderer(createMainIndicatorLegendRendererPlugin({
374
- yPaddingPx: this.opt.yPaddingPx,
375
- }));
376
- this.useRenderer(createYAxisRendererPlugin({
377
- axisWidth,
378
- yPaddingPx: this.opt.yPaddingPx,
379
- getCrosshair: () => {
380
- const pos = this.interaction.crosshairPos;
381
- const price = this.interaction.crosshairPrice;
382
- const activePaneId = this.interaction.activePaneId;
383
- if (pos && price !== null) {
384
- return { y: pos.y, price, activePaneId };
385
- }
386
- return null;
387
- },
388
- }));
389
- this.useRenderer(createCrosshairRendererPlugin({
390
- getCrosshairState: () => ({
391
- pos: this.interaction.crosshairPos,
392
- activePaneId: this.interaction.activePaneId,
393
- isDragging: this.interaction.isDraggingState(),
394
- price: this.interaction.crosshairPrice,
105
+ this.layoutManager = new ChartPaneLayout(this.opt.panes, {
106
+ getDom: () => this.dom,
107
+ getOption: () => ({
108
+ rightAxisWidth: this.opt.rightAxisWidth,
109
+ yPaddingPx: this.opt.yPaddingPx,
110
+ priceLabelWidth: this.opt.priceLabelWidth,
111
+ paneGap: this.opt.paneGap,
112
+ defaultPaneMinHeightPx: this.opt.defaultPaneMinHeightPx,
395
113
  }),
396
- }));
397
- this.useRenderer(createTimeAxisRendererPlugin({
398
- height: this.opt.bottomAxisHeight,
399
- getCrosshair: () => {
400
- const pos = this.interaction.crosshairPos;
401
- const idx = this.interaction.crosshairIndex;
402
- if (pos && idx !== null) {
403
- return { x: pos.x, index: idx };
404
- }
405
- return null;
114
+ getViewport: () => this.viewportManager.getViewport(),
115
+ getSharedWebGLSurface: () => this.sharedWebGLSurface,
116
+ setKnownPaneIds: (ids) => this.rendererPluginManager.setKnownPaneIds(ids),
117
+ notifyPaneResize: (paneId, pane) => this.rendererPluginManager.notifyResize(paneId, wrapPaneInfo(pane)),
118
+ scheduleDraw: (level) => this.scheduleDraw(level),
119
+ onLayoutChange: (ratios, specs) => {
120
+ this._paneRatiosSignal.set(ratios);
121
+ this._paneLayoutSignal.set(specs);
122
+ this.opt = { ...this.opt, panes: specs };
406
123
  },
407
- }));
408
- }
409
- initResizeObserver() {
410
- if (typeof ResizeObserver === 'undefined')
411
- return;
412
- const target = this.dom.container;
413
- if (!target)
414
- return;
415
- // 初始化 scrollLeft 缓存
416
- this.cachedScrollLeft = target.scrollLeft;
417
- this.onScroll = () => { this.cachedScrollLeft = target.scrollLeft; };
418
- target.addEventListener('scroll', this.onScroll, { passive: true });
419
- this.resizeObserver = new ResizeObserver((entries) => {
420
- const entry = entries[0];
421
- if (!entry)
422
- return;
423
- const prevWidth = this.observedSize.width;
424
- const prevHeight = this.observedSize.height;
425
- const prevDpr = this.preciseDpr;
426
- this.updateObservedMetrics(entry);
427
- const widthChanged = this.observedSize.width !== prevWidth;
428
- const heightChanged = this.observedSize.height !== prevHeight;
429
- const dprChanged = this.preciseDpr !== prevDpr;
430
- if (import.meta.env?.MODE !== 'production') {
431
- console.log(`[Chart] resize observer: ` +
432
- `size ${prevWidth}x${prevHeight} -> ${this.observedSize.width}x${this.observedSize.height} ` +
433
- `dpr ${prevDpr} -> ${this.preciseDpr} ` +
434
- `changed: ${widthChanged || heightChanged ? 'size' : ''}${widthChanged || heightChanged && dprChanged ? '+' : ''}${dprChanged ? 'dpr' : ''}`);
435
- }
436
- if (widthChanged || heightChanged || dprChanged) {
437
- this.resize();
438
- }
439
124
  });
440
- try {
441
- this.resizeObserver.observe(target, { box: 'device-pixel-content-box' });
442
- }
443
- catch {
444
- this.resizeObserver.observe(target);
445
- }
446
- }
447
- updateObservedMetrics(entry) {
448
- const cssWidth = Math.max(1, Math.round(entry.contentRect.width));
449
- const cssHeight = Math.max(1, Math.round(entry.contentRect.height));
450
- this.observedSize.width = cssWidth;
451
- this.observedSize.height = cssHeight;
452
- const pixelSize = entry.devicePixelContentBoxSize?.[0];
453
- const cssSize = entry.contentBoxSize?.[0];
454
- if (!pixelSize || !cssSize || cssSize.inlineSize <= 0) {
455
- this.preciseDpr = 0;
456
- return;
457
- }
458
- const raw = pixelSize.inlineSize / cssSize.inlineSize;
459
- this.preciseDpr = Math.round(raw * 64) / 64;
460
- }
461
- getEffectiveDpr() {
462
- let dpr = this.preciseDpr > 0
463
- ? this.preciseDpr
464
- : Math.round((window.devicePixelRatio || 1) * 64) / 64;
465
- if (dpr < 1)
466
- dpr = 1;
467
- return dpr;
125
+ this.dataManager = new ChartDataManager({
126
+ getOption: () => this.opt,
127
+ getEffectiveDpr: () => this.viewportManager.getEffectiveDpr(),
128
+ getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
129
+ getCachedScrollLeft: () => this.viewportManager.getCachedScrollLeft(),
130
+ setCachedScrollLeft: (v) => { this.viewportManager.setCachedScrollLeft(v); },
131
+ setPendingScrollLeft: (v) => { this.viewportManager.setPendingScrollLeft(v); },
132
+ getDom: () => this.dom,
133
+ getObservedSize: () => this.viewportManager.getObservedSize(),
134
+ getViewport: () => this.viewportManager.getViewport(),
135
+ scheduleDraw: (level) => this.scheduleDraw(level),
136
+ resetInteraction: () => this.interaction.reset(),
137
+ getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
138
+ setPendingIndicatorDataUpdate: (v) => { this.dataManager.pendingIndicatorDataUpdate = v; },
139
+ isPointerDown: () => this.interaction.isPointerDown(),
140
+ });
141
+ this.zoomController = new ChartZoomController({
142
+ getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
143
+ getCurrentDpr: () => this.viewportManager.getEffectiveDpr(),
144
+ getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
145
+ setScrollLeft: (v) => { this.viewportManager.setScrollLeft(v); },
146
+ onZoomCommitted: (result) => {
147
+ this.opt = { ...this.opt, kWidth: result.kWidth, kGap: result.kGap };
148
+ this.updateViewportSignal();
149
+ this.scheduleDraw();
150
+ },
151
+ getKWidth: () => this.opt.kWidth,
152
+ getKGap: () => this.opt.kGap,
153
+ getMinKWidth: () => this.opt.minKWidth,
154
+ getMaxKWidth: () => this.opt.maxKWidth,
155
+ zoomLevelCount: Math.max(2, Math.round(this.opt.zoomLevels ?? 20)),
156
+ initialZoomLevel: this.opt.initialZoomLevel ?? 1,
157
+ });
158
+ // 注意:初始 kWidth/kGap 应由外部通过 applyRenderState() 传入
159
+ // 初始化指标管理器
160
+ this.indicatorManager = new ChartIndicatorManager({
161
+ getOption: () => this.opt,
162
+ getPluginHost: () => this.pluginHost,
163
+ getRenderer: (name) => this.getRenderer(name),
164
+ useRenderer: (plugin, config) => this.useRenderer(plugin, config),
165
+ removeRenderer: (name) => this.removeRenderer(name),
166
+ updateRendererConfig: (name, config) => this.updateRendererConfig(name, config),
167
+ setRendererEnabled: (name, enabled) => this.setRendererEnabled(name, enabled),
168
+ hasPane: (paneId) => this.layoutManager.hasPane(paneId),
169
+ upsertPane: (def) => this.layoutManager.upsertPane(def),
170
+ removePaneDefinition: (paneId) => this.layoutManager.removePaneDefinition(paneId),
171
+ getPaneSpecs: () => this.layoutManager.getPaneSpecs(),
172
+ getPaneRatiosSignal: () => this._paneRatiosSignal,
173
+ getInternalPaneRatios: () => this.layoutManager.getInternalPaneRatios(),
174
+ setInternalPaneRatio: (paneId, ratio) => this.layoutManager.setInternalPaneRatio(paneId, ratio),
175
+ deleteInternalPaneRatio: (paneId) => this.layoutManager.deleteInternalPaneRatio(paneId),
176
+ applyPaneLayoutSpecs: (specs) => this.layoutManager.applyPaneLayoutSpecs(specs),
177
+ getLastVisibleRange: () => this.dataManager.lastVisibleRange,
178
+ getCrosshairPos: () => this.interaction.crosshairPos,
179
+ getCrosshairPrice: () => this.interaction.crosshairPrice,
180
+ getActivePaneId: () => this.interaction.activePaneId,
181
+ scheduleDraw: (level) => this.scheduleDraw(level),
182
+ setPendingIndicatorDataUpdate: (v) => { this.dataManager.pendingIndicatorDataUpdate = v; },
183
+ });
184
+ // 初始化渲染器
185
+ this.renderer = new ChartRenderer({
186
+ getDom: () => this.dom,
187
+ getOption: () => this.opt,
188
+ getPaneRenderers: () => this.paneRenderers,
189
+ getInteraction: () => this.interaction,
190
+ getSharedWebGLSurface: () => this.sharedWebGLSurface,
191
+ getPluginHost: () => this.pluginHost,
192
+ getRendererPluginManager: () => this.rendererPluginManager,
193
+ getTheme: () => this._themeSignal.peek(),
194
+ getCurrentZoomLevel: () => this.zoomController.currentZoomLevel,
195
+ getZoomLevelCount: () => this.zoomController.zoomLevelCount,
196
+ getViewportManager: () => this.viewportManager,
197
+ getDataManager: () => this.dataManager,
198
+ getIndicatorManager: () => this.indicatorManager,
199
+ });
200
+ this.renderer.registerDrawingPlugins();
201
+ this.renderer.initCoreRenderers();
202
+ this.viewportManager.init();
468
203
  }
469
204
  getViewport() {
470
- return this._internalViewport;
205
+ return this.viewportManager.getViewport();
471
206
  }
472
207
  getCurrentDpr() {
473
- return this.getEffectiveDpr();
208
+ return this.viewportManager.getEffectiveDpr();
474
209
  }
475
210
  /** 获取缓存的 scrollLeft(避免读取 DOM 触发强制回流) */
476
211
  getCachedScrollLeft() {
477
- return this.cachedScrollLeft;
212
+ return this.viewportManager.getCachedScrollLeft();
213
+ }
214
+ /** 获取逻辑 scrollLeft(减去左侧加载缓冲宽度,可为负值) */
215
+ getLogicalScrollLeft() {
216
+ return this.viewportManager.getLogicalScrollLeft();
478
217
  }
479
218
  /** 获取插件宿主 */
480
219
  get plugin() {
@@ -510,14 +249,19 @@ export class Chart {
510
249
  }
511
250
  /** 更新用户设置(触发重绘) */
512
251
  updateSettings(settings) {
513
- this.settings = { ...settings };
252
+ this.renderer.updateSettings(settings);
514
253
  this.interaction.updateSettings(settings);
515
254
  // 同步刻度类型设置到所有 pane(百分比仅用于主图)
516
- const axisType = settings.axisType ?? 'linear';
517
- for (const renderer of this.paneRenderers) {
518
- const pane = renderer.getPane();
519
- const scaleType = axisType === 'percent' && pane.role !== 'price' ? 'linear' : axisType;
520
- pane.yAxis.setScaleType(scaleType);
255
+ if ('axisType' in settings) {
256
+ const axisType = settings.axisType ?? 'linear';
257
+ const currentType = this.paneRenderers[0]?.getPane().yAxis.getScaleType();
258
+ if (axisType !== currentType) {
259
+ for (const renderer of this.paneRenderers) {
260
+ const pane = renderer.getPane();
261
+ const scaleType = axisType === 'percent' && pane.role !== 'price' ? 'linear' : axisType;
262
+ pane.yAxis.setScaleType(scaleType);
263
+ }
264
+ }
521
265
  }
522
266
  this.scheduleDraw();
523
267
  }
@@ -526,254 +270,7 @@ export class Chart {
526
270
  * @param level 更新级别,决定渲染哪些层
527
271
  */
528
272
  draw(level = UpdateLevel.All) {
529
- // 1. 重置 Marker 标记
530
- this.markerManager.clear();
531
- // 2. 准备帧数据(视口 / 可见范围 / K 线坐标,优先走缓存)
532
- const frame = this.prepareFrameData(level);
533
- if (!frame) {
534
- if (this._internalData.length === 0)
535
- this.clearAllCanvases();
536
- return;
537
- }
538
- const { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame } = frame;
539
- // 3. 更新交互控制器坐标映射
540
- this.interaction.setKLinePositions(kLinePositions, range, kWidthPx);
541
- // 4. 通知调度器当前活跃主图指标 + 获取价格范围
542
- this.indicatorScheduler.setActiveMainIndicators([...this._mainIndicatorsSignal.peek().entries()].map(([id, entry]) => ({ id, params: entry.params })));
543
- const mainIndicatorRange = useCachedFrame ? null : this.indicatorScheduler.getMainIndicatorPriceRange();
544
- const hasCrosshair = this.interaction.getCrosshairIndex() !== null;
545
- // 5. 遍历所有 Pane 渲染主层 / overlay / Y 轴
546
- const { sharedXAxisLabels, sharedXAxisRanges } = this.renderPanes(vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, mainIndicatorRange, hasCrosshair, useCachedFrame, level);
547
- // 6. 持久化十字线状态供下帧判断清除
548
- this.overlayHadCrosshair = hasCrosshair;
549
- // 7. 渲染 X 轴时间轴
550
- this.renderXAxis(vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, sharedXAxisLabels, sharedXAxisRanges);
551
- }
552
- prepareFrameData(level) {
553
- const useCachedFrame = level === UpdateLevel.Overlay && this.cachedDrawFrame !== null;
554
- const vp = useCachedFrame ? this.cachedDrawFrame.viewport : this.computeViewport();
555
- if (!vp)
556
- return null;
557
- if (this._internalData.length === 0)
558
- return null;
559
- const range = useCachedFrame
560
- ? this.cachedDrawFrame.range
561
- : (() => {
562
- const { start, end } = getVisibleRange(vp.scrollLeft, vp.plotWidth, this.opt.kWidth, this.opt.kGap, this._internalData.length, vp.dpr);
563
- return { start, end };
564
- })();
565
- if (!useCachedFrame && (range.start !== this.lastVisibleRange.start || range.end !== this.lastVisibleRange.end)) {
566
- this.indicatorScheduler.updateVisibleRange(range);
567
- this.lastVisibleRange = range;
568
- this.checkVisibleRangeGap();
569
- }
570
- const kLinePositions = useCachedFrame
571
- ? this.cachedDrawFrame.kLinePositions
572
- : this.calcKLinePositions(range);
573
- let kLineCenters;
574
- let kBarRects;
575
- let kWidthPx;
576
- if (useCachedFrame) {
577
- kLineCenters = this.cachedDrawFrame.kLineCenters;
578
- kBarRects = this.cachedDrawFrame.kBarRects;
579
- kWidthPx = this.cachedDrawFrame.kWidthPx;
580
- }
581
- else {
582
- const physConfig = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, vp.dpr);
583
- let barWidthPx = Math.max(1, physConfig.unitPx - 1);
584
- if (barWidthPx % 2 === 0)
585
- barWidthPx -= 1;
586
- kLineCenters = new Array(kLinePositions.length);
587
- kBarRects = new Array(kLinePositions.length);
588
- for (let i = 0; i < kLinePositions.length; i++) {
589
- const x = kLinePositions[i];
590
- const leftPx = Math.round(x * vp.dpr);
591
- const wickXPx = leftPx + (physConfig.kWidthPx - 1) / 2;
592
- kLineCenters[i] = wickXPx / vp.dpr;
593
- const barLeftPx = wickXPx - (barWidthPx - 1) / 2;
594
- kBarRects[i] = { x: barLeftPx / vp.dpr, width: barWidthPx / vp.dpr };
595
- }
596
- kWidthPx = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, vp.dpr).kWidthPx;
597
- this.cachedDrawFrame = {
598
- viewport: { ...vp },
599
- range: { ...range },
600
- kLinePositions,
601
- kLineCenters,
602
- kBarRects,
603
- kWidthPx,
604
- };
605
- }
606
- return { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame };
607
- }
608
- clearAllCanvases() {
609
- const vp = this.computeViewport();
610
- if (!vp)
611
- return;
612
- for (const r of this.paneRenderers) {
613
- const { mainCtx, overlayCtx, yAxisCtx } = r.getContexts();
614
- const pane = r.getPane();
615
- mainCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr);
616
- overlayCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr);
617
- yAxisCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr);
618
- }
619
- const xCtx = this.xAxisCtx;
620
- if (xCtx) {
621
- const xW = xCtx.canvas.width;
622
- const xH = xCtx.canvas.height;
623
- xCtx.clearRect(0, 0, xW, xH);
624
- }
625
- }
626
- renderPanes(vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, mainIndicatorRange, hasCrosshair, useCachedFrame, level) {
627
- const sharedYAxisLabels = [];
628
- const sharedXAxisLabels = [];
629
- const sharedYAxisRanges = [];
630
- const sharedXAxisRanges = [];
631
- for (const renderer of this.paneRenderers) {
632
- const pane = renderer.getPane();
633
- const { mainCtx, overlayCtx, yAxisCtx } = renderer.getContexts();
634
- const { candleSurface, lineSurface } = renderer.getWebGL();
635
- if (!useCachedFrame) {
636
- const indicatorRange = pane.role === 'price' ? mainIndicatorRange : null;
637
- const comparisonRange = pane.id === 'main' ? this.getComparisonEquivalentPriceRange(range) : null;
638
- const mergedRange = this.mergeNumericRanges(indicatorRange, comparisonRange);
639
- pane.updateRange(this._internalData, range, mergedRange);
640
- if (pane.id === 'main' && this.settings.disableMainPaneVerticalScroll) {
641
- pane.yAxis.resetTransform();
642
- }
643
- }
644
- const shouldUpdateMain = level === UpdateLevel.Main || level === UpdateLevel.All;
645
- const shouldUpdateOverlay = level === UpdateLevel.All || (level === UpdateLevel.Overlay && (hasCrosshair || this.overlayHadCrosshair));
646
- if (shouldUpdateMain && mainCtx) {
647
- mainCtx.setTransform(1, 0, 0, 1, 0, 0);
648
- mainCtx.scale(vp.dpr, vp.dpr);
649
- mainCtx.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr);
650
- candleSurface?.clear();
651
- lineSurface?.clear();
652
- }
653
- if (shouldUpdateOverlay && overlayCtx) {
654
- const overlayWidth = overlayCtx.canvas.width / vp.dpr;
655
- overlayCtx.setTransform(1, 0, 0, 1, 0, 0);
656
- overlayCtx.scale(vp.dpr, vp.dpr);
657
- overlayCtx.clearRect(0, 0, overlayWidth + 1, pane.height + 2 / vp.dpr);
658
- }
659
- if (yAxisCtx && !useCachedFrame) {
660
- const yAxisWidth = yAxisCtx.canvas.width / vp.dpr;
661
- yAxisCtx.setTransform(1, 0, 0, 1, 0, 0);
662
- yAxisCtx.scale(vp.dpr, vp.dpr);
663
- yAxisCtx.clearRect(0, 0, yAxisWidth, pane.height + 2 / vp.dpr);
664
- }
665
- const context = {
666
- ctx: mainCtx,
667
- overlayCtx: overlayCtx ?? undefined,
668
- pane: wrapPaneInfo(pane),
669
- data: this._internalData,
670
- comparisonData: this._comparisonData,
671
- comparisonSymbols: this._comparisonSpecs,
672
- range,
673
- scrollLeft: vp.scrollLeft,
674
- kWidth: this.opt.kWidth,
675
- kGap: this.opt.kGap,
676
- dpr: vp.dpr,
677
- paneWidth: vp.plotWidth,
678
- kLinePositions,
679
- kLineCenters,
680
- kBarRects,
681
- markerManager: this.markerManager,
682
- crosshairIndex: this.interaction.getCrosshairIndex(),
683
- yAxisCtx: yAxisCtx ?? undefined,
684
- candleWebGLSurface: candleSurface ?? undefined,
685
- lineWebGLSurface: lineSurface ?? undefined,
686
- zoomLevel: this.currentZoomLevel,
687
- zoomLevelCount: this.zoomLevelCount,
688
- viewport: {
689
- scrollLeft: vp.scrollLeft,
690
- plotWidth: vp.plotWidth,
691
- plotHeight: vp.plotHeight,
692
- },
693
- settings: this.settings,
694
- yAxisLabels: sharedYAxisLabels,
695
- xAxisLabels: sharedXAxisLabels,
696
- yAxisRanges: sharedYAxisRanges,
697
- xAxisRanges: sharedXAxisRanges,
698
- theme: this._themeSignal.peek(),
699
- isAsiaMarket: this.settings.isAsiaMarket,
700
- colorPresetSettings: this.settings.colorPresetSettings,
701
- };
702
- if (shouldUpdateMain || shouldUpdateOverlay) {
703
- const errors = this.rendererPluginManager.render(pane.id, context, level);
704
- if (errors.length > 0) {
705
- this.pluginHost.events.emit('renderer:error', { paneId: pane.id, errors });
706
- }
707
- const yAxisErrors = this.rendererPluginManager.renderPlugin('yAxis', context);
708
- if (yAxisErrors.length > 0) {
709
- this.pluginHost.events.emit('renderer:error', { paneId: pane.id, errors: yAxisErrors });
710
- }
711
- }
712
- }
713
- return { sharedXAxisLabels, sharedXAxisRanges };
714
- }
715
- renderXAxis(vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, sharedXAxisLabels, sharedXAxisRanges) {
716
- const xAxisCtx = this.xAxisCtx ?? this.dom.xAxisCanvas.getContext('2d');
717
- if (!this.xAxisCtx) {
718
- this.xAxisCtx = xAxisCtx;
719
- }
720
- if (xAxisCtx) {
721
- const timeAxisContext = {
722
- ctx: xAxisCtx,
723
- pane: {
724
- id: 'xAxis',
725
- role: 'auxiliary',
726
- capabilities: {
727
- showPriceAxisTicks: false,
728
- showCrosshairPriceLabel: false,
729
- candleHitTest: false,
730
- supportsPriceTranslate: false,
731
- },
732
- top: 0,
733
- height: this.opt.bottomAxisHeight,
734
- yAxis: {
735
- priceToY: () => 0,
736
- yToPrice: () => 0,
737
- getPaddingTop: () => 0,
738
- getPaddingBottom: () => 0,
739
- getPriceOffset: () => 0,
740
- getDisplayRange: (baseRange) => baseRange ?? { maxPrice: 0, minPrice: 0 },
741
- getScaleType: () => 'linear',
742
- getBasePrice: () => null,
743
- toPercent: () => 0,
744
- fromPercent: () => 0,
745
- getDisplayPercentRange: () => ({ minPct: 0, maxPct: 0 }),
746
- },
747
- priceRange: { maxPrice: 0, minPrice: 0 },
748
- },
749
- data: this._internalData,
750
- range,
751
- scrollLeft: vp.scrollLeft,
752
- kWidth: this.opt.kWidth,
753
- kGap: this.opt.kGap,
754
- dpr: vp.dpr,
755
- paneWidth: vp.plotWidth,
756
- kLinePositions,
757
- kLineCenters,
758
- kBarRects,
759
- xAxisCtx,
760
- viewport: {
761
- scrollLeft: vp.scrollLeft,
762
- plotWidth: vp.plotWidth,
763
- plotHeight: vp.plotHeight,
764
- },
765
- yAxisLabels: [],
766
- xAxisLabels: sharedXAxisLabels,
767
- xAxisRanges: sharedXAxisRanges,
768
- theme: this._themeSignal.peek(),
769
- isAsiaMarket: this.settings.isAsiaMarket,
770
- colorPresetSettings: this.settings.colorPresetSettings,
771
- };
772
- const errors = this.rendererPluginManager.renderPlugin('timeAxis', timeAxisContext);
773
- if (errors.length > 0) {
774
- this.pluginHost.events.emit('renderer:error', { paneId: 'timeAxis', errors });
775
- }
776
- }
273
+ this.renderer.draw(level);
777
274
  }
778
275
  // ========== Render State API (Vue SSOT) ==========
779
276
  /**
@@ -783,24 +280,24 @@ export class Chart {
783
280
  */
784
281
  applyRenderState(kWidth, kGap, zoomLevel) {
785
282
  const nextZoomLevel = zoomLevel !== undefined
786
- ? Math.max(1, Math.min(this.zoomLevelCount, zoomLevel))
787
- : this.currentZoomLevel;
283
+ ? Math.max(1, Math.min(this.zoomController.zoomLevelCount, zoomLevel))
284
+ : this.zoomController.currentZoomLevel;
788
285
  const renderStateChanged = this.opt.kWidth !== kWidth
789
286
  || this.opt.kGap !== kGap
790
- || this.currentZoomLevel !== nextZoomLevel;
287
+ || this.zoomController.currentZoomLevel !== nextZoomLevel;
791
288
  if (!renderStateChanged) {
792
289
  return;
793
290
  }
794
291
  this.opt = { ...this.opt, kWidth, kGap };
795
292
  if (zoomLevel !== undefined) {
796
- this.currentZoomLevel = nextZoomLevel;
293
+ this.zoomController.setZoomLevel(nextZoomLevel);
797
294
  }
798
295
  this.updateViewportSignal();
799
296
  this.scheduleDraw();
800
297
  }
801
298
  /** 获取总缩放级别数 */
802
299
  getZoomLevelCount() {
803
- return this.zoomLevelCount;
300
+ return this.zoomController.zoomLevelCount;
804
301
  }
805
302
  /** 获取所有 PaneRenderer */
806
303
  getPaneRenderers() {
@@ -808,16 +305,16 @@ export class Chart {
808
305
  }
809
306
  /** 获取 MarkerManager(供 InteractionController 使用) */
810
307
  getMarkerManager() {
811
- return this.markerManager;
308
+ return this.renderer.getMarkerManager();
812
309
  }
813
310
  /** 更新自定义标记 */
814
311
  updateCustomMarkers(markers) {
815
- this.markerManager.setCustomMarkers(markers);
312
+ this.renderer.getMarkerManager().setCustomMarkers(markers);
816
313
  this.scheduleDraw();
817
314
  }
818
315
  /** 清除自定义标记 */
819
316
  clearCustomMarkers() {
820
- this.markerManager.clearCustomMarkers();
317
+ this.renderer.getMarkerManager().clearCustomMarkers();
821
318
  this.scheduleDraw();
822
319
  }
823
320
  /** 获取 ChartDom(供 InteractionController 使用) */
@@ -828,29 +325,6 @@ export class Chart {
828
325
  getOption() {
829
326
  return this.opt;
830
327
  }
831
- /**
832
- * 计算 K 线起始 x 坐标数组,与 candle.ts 的像素对齐方式保持一致
833
- * @param range 可见 K 线索引范围
834
- * @returns x 坐标数组(逻辑像素,经过物理像素对齐)
835
- */
836
- calcKLinePositions(range) {
837
- const { start, end } = range;
838
- const count = end - start;
839
- // 边界检查:防止负数或零长度数组
840
- if (count <= 0) {
841
- return [];
842
- }
843
- const dpr = this.getEffectiveDpr();
844
- // 统一使用 getPhysicalKLineConfig,确保与渲染完全一致
845
- const { unitPx, startXPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr);
846
- const positions = new Array(count);
847
- for (let i = 0; i < count; i++) {
848
- const dataIndex = start + i;
849
- const leftPx = startXPx + dataIndex * unitPx;
850
- positions[i] = leftPx / dpr;
851
- }
852
- return positions;
853
- }
854
328
  /**
855
329
  * 更新配置并触发布局/重绘
856
330
  * @param partial 部分配置项
@@ -867,315 +341,80 @@ export class Chart {
867
341
  if (partial.panes) {
868
342
  const nextPanes = partial.panes.map((pane) => ({ ...pane }));
869
343
  this.opt = { ...this.opt, ...partial, panes: nextPanes };
870
- this.applyPaneLayoutSpecs(nextPanes);
344
+ this.layoutManager.applyPaneLayoutSpecs(nextPanes);
871
345
  return;
872
346
  }
873
347
  this.opt = { ...this.opt, ...partial };
874
348
  this.resize();
875
349
  }
876
- /** 更新 pane 布局配置
877
- * @param panes 新的 pane 配置数组
878
- *
879
- * 显式整盘替换:清空之前 user-resize 留下的 paneRatios 缓存,让 spec 中的 ratio
880
- * 真正生效。`addPane`/`upsertPane`/`removePaneDefinition` 走 `applyPaneLayoutSpecs`
881
- * 时仍保留 prev 值以记住用户拖拽过的高度——只有显式的 layout replacement 才重置。
882
- */
883
350
  updatePaneLayout(panes) {
884
- this._internalPaneRatios.clear();
885
- this.applyPaneLayoutSpecs(panes);
351
+ this.layoutManager.updatePaneLayout(panes);
886
352
  }
887
353
  setPaneDefinitions(defs) {
888
- this.applyPaneLayoutSpecs(defs);
354
+ this.layoutManager.setPaneDefinitions(defs);
889
355
  }
890
356
  upsertPane(def) {
891
- const idx = this.opt.panes.findIndex((pane) => pane.id === def.id);
892
- if (idx === -1) {
893
- this.applyPaneLayoutSpecs([...this.opt.panes, { ...def }]);
894
- return;
895
- }
896
- const next = [...this.opt.panes];
897
- next[idx] = { ...next[idx], ...def };
898
- this.applyPaneLayoutSpecs(next);
357
+ this.layoutManager.upsertPane(def);
899
358
  }
900
359
  removePaneDefinition(paneId) {
901
- if (!this.opt.panes.some((pane) => pane.id === paneId))
902
- return;
903
- this._internalPaneRatios.delete(paneId);
904
- this.applyPaneLayoutSpecs(this.opt.panes.filter((pane) => pane.id !== paneId));
360
+ this.layoutManager.removePaneDefinition(paneId);
905
361
  }
906
362
  bindIndicatorToPane(paneId, indicatorId, params) {
907
- const paneExists = this.opt.panes.some((pane) => pane.id === paneId);
908
- if (!paneExists) {
909
- this.upsertPane({ id: paneId, ratio: 1, visible: true, role: 'indicator' });
910
- }
911
- const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId);
912
- if (!definition) {
913
- throw new Error(`[Chart] Unknown indicator: ${indicatorId}`);
914
- }
915
- const renderer = createSubIndicatorRenderer({ indicatorId, paneId, definition, params });
916
- const rendererName = renderer.name;
917
- const existing = this.getRenderer(rendererName);
918
- if (existing) {
919
- if (params)
920
- this.updateRendererConfig(rendererName, params);
921
- return;
922
- }
923
- this.useRenderer(renderer, params);
363
+ this.indicatorManager.bindIndicatorToPane(paneId, indicatorId, params);
924
364
  }
925
365
  /** 更新绘图对象 */
926
366
  setDrawings(drawings) {
927
- this.drawingStore.setAll(drawings);
367
+ this.renderer.getDrawingStore().setAll(drawings);
928
368
  this._drawingsSignal.set(drawings);
929
369
  this.scheduleDraw();
930
370
  }
931
371
  /** 更新选中的绘图 ID */
932
372
  setSelectedDrawingId(id) {
933
- if (this.drawingStore.getSelectedId() === id)
373
+ const store = this.renderer.getDrawingStore();
374
+ if (store.getSelectedId() === id)
934
375
  return;
935
- this.drawingStore.setSelectedId(id);
376
+ store.setSelectedId(id);
936
377
  this.scheduleDraw();
937
378
  }
938
- /** 获取当前 pane 布局快照(含 ratio) */
939
379
  getPaneLayoutSpecs() {
940
- const visible = this.opt.panes.filter(p => p.visible !== false);
941
- const sum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0);
942
- const safeSum = sum > 0 ? sum : 1;
943
- return this.opt.panes.map((spec) => {
944
- const base = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0;
945
- const ratio = spec.visible === false ? base : base / safeSum;
946
- const pane = this.paneRenderers.find((r) => r.getPane().id === spec.id)?.getPane();
947
- return {
948
- ...spec,
949
- ratio,
950
- role: pane?.role ?? spec.role,
951
- capabilities: pane ? { ...pane.capabilities } : spec.capabilities,
952
- };
953
- });
954
- }
955
- emitPaneLayoutChange() {
956
- // 同步 pane ratios 到 signal
957
- const ratios = {};
958
- this._internalPaneRatios.forEach((ratio, id) => {
959
- ratios[id] = ratio;
960
- });
961
- this._paneRatiosSignal.set(ratios);
962
- this._paneLayoutSignal.set(this.getPaneLayoutSpecs());
963
- }
964
- applyPaneLayoutSpecs(panes) {
965
- this.opt.panes = panes.map((spec) => ({ ...spec }));
966
- this.syncPaneRatiosFromSpecs(this.opt.panes);
967
- this.initPanes();
968
- this.layoutPanes();
969
- this.emitPaneLayoutChange();
970
- this.scheduleDraw();
380
+ return this.layoutManager.getPaneLayoutSpecs();
971
381
  }
972
- /**
973
- * 调整相邻 pane 边界(支持连锁挤压)
974
- * @param upperPaneId 上方 pane ID(边界位于此 pane 与其下方邻居之间)
975
- * @param deltaY Y 方向位移(逻辑像素,正数表示边界向下,upper 增大;负数表示向上,upper 减小)
976
- */
977
382
  resizePaneBoundary(upperPaneId, deltaY) {
978
- // === 1. 参数校验 ===
979
- if (!Number.isFinite(deltaY) || deltaY === 0)
980
- return false;
981
- const vp = this._internalViewport;
982
- if (!vp)
983
- return false;
984
- // === 2. 定位相邻 pane 对(边界两侧) ===
985
- const visibleSpecs = this.opt.panes.filter(p => p.visible !== false);
986
- const boundaryIndex = visibleSpecs.findIndex(p => p.id === upperPaneId);
987
- if (boundaryIndex < 0 || boundaryIndex >= visibleSpecs.length - 1)
988
- return false;
989
- const upperSpec = visibleSpecs[boundaryIndex];
990
- const lowerSpec = visibleSpecs[boundaryIndex + 1];
991
- if (!upperSpec || !lowerSpec)
992
- return false;
993
- // === 3. 收集所有 pane 当前高度 ===
994
- const heights = new Map();
995
- for (const spec of visibleSpecs) {
996
- const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id);
997
- if (renderer) {
998
- heights.set(spec.id, renderer.getPane().height);
999
- }
1000
- }
1001
- // === 4. 连锁挤压/扩展 ===
1002
- // deltaY > 0: 边界下移,upper expand,lower shrink
1003
- // deltaY < 0: 边界上移,upper shrink,lower expand
1004
- const expandIdx = deltaY > 0 ? boundaryIndex : boundaryIndex + 1;
1005
- const shrinkIdx = deltaY > 0 ? boundaryIndex + 1 : boundaryIndex;
1006
- const expandDir = deltaY > 0 ? -1 : 1; // expand 方向(向边界方向找)
1007
- const shrinkDir = deltaY > 0 ? 1 : -1; // shrink 方向(远离边界方向找)
1008
- let remaining = Math.abs(deltaY);
1009
- // 先尝试 shrink(从 shrinkIdx 开始,沿 shrinkDir 方向连锁)
1010
- let shrinkCursor = shrinkIdx;
1011
- while (remaining > 0 && shrinkCursor >= 0 && shrinkCursor < visibleSpecs.length) {
1012
- const spec = visibleSpecs[shrinkCursor];
1013
- if (!spec)
1014
- break;
1015
- const currentH = heights.get(spec.id) ?? 0;
1016
- const minH = this.getPaneMinHeight(spec, vp.plotHeight);
1017
- const canShrink = Math.max(0, currentH - minH);
1018
- if (canShrink > 0) {
1019
- const shrink = Math.min(canShrink, remaining);
1020
- heights.set(spec.id, currentH - shrink);
1021
- remaining -= shrink;
1022
- }
1023
- // 继续向 shrinkDir 方向找下一个可 shrink 的 pane
1024
- if (remaining > 0) {
1025
- shrinkCursor += shrinkDir;
1026
- }
1027
- }
1028
- // 如果还有剩余(无法完全 shrink),说明拖拽无效
1029
- if (remaining > 0)
1030
- return false;
1031
- // 将节省的高度全部加到 expand 方
1032
- const expandSpec = visibleSpecs[expandIdx];
1033
- if (!expandSpec)
1034
- return false;
1035
- const expandCurrentH = heights.get(expandSpec.id) ?? 0;
1036
- heights.set(expandSpec.id, expandCurrentH + Math.abs(deltaY));
1037
- // === 5. 将像素高度转换为 ratio ===
1038
- const gap = Math.max(0, this.opt.paneGap ?? 0);
1039
- const totalGaps = gap * Math.max(0, visibleSpecs.length - 1);
1040
- const availableH = Math.max(1, vp.plotHeight - totalGaps);
1041
- for (const spec of visibleSpecs) {
1042
- const h = heights.get(spec.id) ?? 0;
1043
- this._internalPaneRatios.set(spec.id, h / availableH);
1044
- }
1045
- // === 6. 归一化并同步 ===
1046
- this.normalizeVisiblePaneRatios(visibleSpecs);
1047
- this.syncPaneRatiosToSpecs();
1048
- // === 7. 应用布局 ===
1049
- this.layoutPanes();
1050
- this.emitPaneLayoutChange();
1051
- this.scheduleDraw();
1052
- return true;
1053
- }
1054
- resolvePaneRole(spec, index) {
1055
- if (spec.role)
1056
- return spec.role;
1057
- return index === 0 ? 'price' : 'indicator';
383
+ return this.layoutManager.resizePaneBoundary(upperPaneId, deltaY);
1058
384
  }
1059
385
  addPane(paneId) {
1060
- if (this.opt.panes.some((spec) => spec.id === paneId)) {
1061
- console.warn(`Pane "${paneId}" already exists`);
1062
- return;
1063
- }
1064
- const hasPricePane = this.opt.panes.some((spec, index) => this.resolvePaneRole(spec, index) === 'price');
1065
- const role = hasPricePane ? 'indicator' : 'price';
1066
- this.applyPaneLayoutSpecs([
1067
- ...this.opt.panes,
1068
- { id: paneId, ratio: 1, visible: true, role },
1069
- ]);
386
+ this.layoutManager.addPane(paneId);
1070
387
  }
1071
- /**
1072
- * 动态移除 pane
1073
- * @param paneId pane 标识符
1074
- */
1075
388
  removePane(paneId) {
1076
- if (!this.opt.panes.some((spec) => spec.id === paneId))
1077
- return;
1078
- const next = this.opt.panes.filter((spec) => spec.id !== paneId);
1079
- this._internalPaneRatios.delete(paneId);
1080
- this.applyPaneLayoutSpecs(next);
389
+ this.layoutManager.removePane(paneId);
1081
390
  }
1082
- /**
1083
- * 检查 pane 是否存在
1084
- * @param paneId pane 标识符
1085
- */
1086
391
  hasPane(paneId) {
1087
- return this.opt.panes.some((spec) => spec.id === paneId);
392
+ return this.layoutManager.hasPane(paneId);
1088
393
  }
1089
394
  // ========== 副图管理 API ==========
1090
- /**
1091
- * 创建副图面板并注册指标渲染器
1092
- * @param paneId 副图实例标识符(如 'RSI_0', 'MACD_0')
1093
- * @param indicatorId 指标类型
1094
- * @param params 指标参数
1095
- * @returns 是否创建成功
1096
- */
1097
395
  createSubPane(paneId, indicatorId, params) {
1098
- // 调整 pane ratios:主图占 3,副图各占 1
1099
- const visibleSpecs = this.opt.panes.filter((pane) => pane.visible !== false);
1100
- const pricePanes = visibleSpecs.filter((pane, index) => this.resolvePaneRole(pane, index) === 'price');
1101
- const indicatorPanes = visibleSpecs.filter((pane, index) => this.resolvePaneRole(pane, index) === 'indicator');
1102
- if (pricePanes.length === 1) {
1103
- const pricePane = pricePanes[0];
1104
- if (pricePane) {
1105
- this._internalPaneRatios.set(pricePane.id, 3);
1106
- }
1107
- for (const pane of indicatorPanes) {
1108
- this._internalPaneRatios.set(pane.id, 1);
1109
- }
1110
- this._internalPaneRatios.set(paneId, 1);
1111
- }
1112
- else {
1113
- this._internalPaneRatios.set(paneId, 1);
1114
- }
1115
- this.upsertPane({ id: paneId, ratio: this._internalPaneRatios.get(paneId) ?? 1, visible: true, role: 'indicator' });
1116
- const success = this.subPaneManager.create(this, paneId, indicatorId, params ?? this.getDefaultSubPaneParams(indicatorId));
1117
- return success;
396
+ return this.indicatorManager.createSubPane(paneId, indicatorId, params);
1118
397
  }
1119
- /**
1120
- * 移除副图面板及其渲染器
1121
- * @param paneId 副图实例标识符
1122
- */
1123
398
  removeSubPane(paneId) {
1124
- this.subPaneManager.remove(this, paneId);
399
+ this.indicatorManager.removeSubPane(paneId);
1125
400
  }
1126
- /**
1127
- * 替换副图的指标类型
1128
- * @param paneId 副图实例标识符
1129
- * @param newIndicatorId 新的指标类型
1130
- * @param params 新指标参数
1131
- */
1132
401
  replaceSubPaneIndicator(paneId, newIndicatorId, params) {
1133
- this.subPaneManager.replaceIndicator(this, paneId, newIndicatorId, params ?? this.getDefaultSubPaneParams(newIndicatorId));
402
+ this.indicatorManager.replaceSubPaneIndicator(paneId, newIndicatorId, params);
1134
403
  }
1135
- /**
1136
- * 更新副图指标参数
1137
- * @param paneId 副图实例标识符
1138
- * @param params 新参数
1139
- */
1140
404
  updateSubPaneParams(paneId, params) {
1141
- this.subPaneManager.updateParams(this, paneId, params);
405
+ this.indicatorManager.updateSubPaneParams(paneId, params);
1142
406
  }
1143
- /**
1144
- * 清除所有副图面板
1145
- */
1146
407
  clearSubPanes() {
1147
- // 获取所有副图 paneId
1148
- const subPaneIds = this.subPaneManager.getPaneIds();
1149
- if (subPaneIds.length === 0)
1150
- return;
1151
- // 移除所有副图
1152
- this.subPaneManager.clear(this);
1153
- // 清理 pane ratios
1154
- for (const paneId of subPaneIds) {
1155
- this._internalPaneRatios.delete(paneId);
1156
- }
1157
- // 更新布局,移除所有副图 pane
1158
- this.applyPaneLayoutSpecs(this.opt.panes.filter((spec) => !subPaneIds.includes(spec.id)));
408
+ this.indicatorManager.clearSubPanes();
1159
409
  }
1160
- /**
1161
- * 获取当前所有副图指标类型
1162
- * @deprecated 使用 getSubPaneEntries 获取完整信息
1163
- */
1164
410
  getSubPaneIndicators() {
1165
- return this.subPaneManager.getAll().map((entry) => entry.indicatorId);
411
+ return this.indicatorManager.getSubPaneIndicators();
1166
412
  }
1167
- /**
1168
- * 获取所有副图条目
1169
- */
1170
413
  getSubPaneEntries() {
1171
- return this.subPaneManager.getAll();
414
+ return this.indicatorManager.getSubPaneEntries();
1172
415
  }
1173
- /**
1174
- * 根据 paneId 获取副图条目
1175
- * @param paneId 副图实例标识符
1176
- */
1177
416
  getSubPaneEntry(paneId) {
1178
- return this.subPaneManager.getByPaneId(paneId);
417
+ return this.indicatorManager.getSubPaneEntry(paneId);
1179
418
  }
1180
419
  getDefaultSubPaneParams(indicatorId) {
1181
420
  // 默认参数定义在 SubPaneManager 中,这里导入使用
@@ -1219,8 +458,6 @@ export class Chart {
1219
458
  };
1220
459
  return { ...(defaults[indicatorId] ?? {}) };
1221
460
  }
1222
- /** 副图渲染器名称前缀(保留向后兼容) */
1223
- static SUB_PANE_PREFIX = 'sub_';
1224
461
  /**
1225
462
  * 平移价格轴(用于主图区域上下拖动)
1226
463
  * @param paneId 目标 pane ID
@@ -1276,105 +513,48 @@ export class Chart {
1276
513
  * @param data K 线数据数组
1277
514
  */
1278
515
  updateData(data) {
1279
- this._internalData = data ?? [];
1280
- this._dataSignal.set([...this._internalData]);
1281
- // 重算 DOM scrollLeft 状态, 防止左右滚动超出数据长度范围
1282
- const container = this.dom.container;
1283
- if (container) {
1284
- const contentWidth = this.getContentWidth();
1285
- const maxScrollLeft = Math.max(0, contentWidth - container.clientWidth);
1286
- if (this.cachedScrollLeft > maxScrollLeft) {
1287
- container.scrollLeft = maxScrollLeft;
1288
- this.cachedScrollLeft = maxScrollLeft;
1289
- }
1290
- }
1291
- // 重置交互状态
1292
- this.interaction.reset();
1293
- // 如果 visibleRange 还是 {0,0},先从 viewport 算一个初步范围
1294
- // 避免 scheduler 第一次计算时用 {0,0} 产出 Infinity 极值
1295
- if (this.lastVisibleRange.start === 0 && this.lastVisibleRange.end === 0 && this._internalData.length > 0) {
1296
- const plotWidth = this.observedSize.width > 0
1297
- ? this.observedSize.width
1298
- : Math.max(1, Math.round(this.dom.container?.clientWidth ?? 800));
1299
- const dpr = this.getEffectiveDpr();
1300
- const { start, end } = getVisibleRange(this.cachedScrollLeft, plotWidth, this.opt.kWidth, this.opt.kGap, this._internalData.length, dpr);
1301
- this.lastVisibleRange = { start, end };
1302
- }
1303
- // 触发指标计算(在 scheduleDraw 之前,确保渲染器读到最新状态)
1304
- const indicatorsReady = this.indicatorScheduler.update(this._internalData, this.lastVisibleRange);
1305
- if (indicatorsReady) {
1306
- this.pendingIndicatorDataUpdate = false;
1307
- this.scheduleDraw();
1308
- }
1309
- else {
1310
- this.pendingIndicatorDataUpdate = true;
1311
- }
516
+ this.dataManager.updateData(data);
1312
517
  }
1313
518
  /** 获取当前数据源(供 renderers 和 interaction 使用) */
1314
519
  getData() {
1315
- return this._internalData;
520
+ return this.dataManager.getData();
1316
521
  }
1317
522
  /** 获取指标调度器(供外部控制器更新指标配置) */
1318
523
  getIndicatorScheduler() {
1319
- return this.indicatorScheduler;
1320
- }
1321
- getTrailingSlotCount() {
1322
- return 24;
524
+ return this.indicatorManager.indicatorSchedulerAccessor;
1323
525
  }
1324
526
  getLogicalSlotCount() {
1325
- return this._internalData.length + this.getTrailingSlotCount();
527
+ return this.dataManager.getLogicalSlotCount();
1326
528
  }
1327
529
  getTimestampAtLogicalIndex(index) {
1328
- if (!Number.isInteger(index) || index < 0 || index >= this._internalData.length)
1329
- return null;
1330
- return this._internalData[index]?.timestamp ?? null;
530
+ return this.dataManager.getTimestampAtLogicalIndex(index);
1331
531
  }
1332
532
  /** 根据视口内 X 坐标反查逻辑索引(允许超出最后一根 K 线) */
1333
533
  getLogicalIndexAtX(mouseX) {
1334
- const vp = this._internalViewport;
1335
- if (!vp || this._internalData.length === 0)
1336
- return null;
1337
- const dpr = this.getEffectiveDpr();
1338
- const { startXPx, unitPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr);
1339
- const worldX = Math.round((vp.scrollLeft + mouseX) * dpr);
1340
- const index = Math.floor((worldX - startXPx) / unitPx);
1341
- if (index < 0)
1342
- return null;
1343
- return index;
534
+ return this.dataManager.getLogicalIndexAtX(mouseX);
1344
535
  }
1345
536
  /** 根据视口内 X 坐标反查数据索引(用于绘图落点) */
1346
537
  getDataIndexAtX(mouseX) {
1347
- const index = this.getLogicalIndexAtX(mouseX);
1348
- if (index === null || index >= this._internalData.length)
1349
- return null;
1350
- return index;
538
+ return this.dataManager.getDataIndexAtX(mouseX);
1351
539
  }
1352
- static LEADING_SLOTS = 60;
1353
- static TRAILING_DRAWING_SLOTS = 24;
1354
540
  /** 获取内容总宽度(用于外部 scroll-content 撑开 scrollWidth) */
1355
541
  getContentWidth() {
1356
- const dataLength = this._internalData.length;
1357
- if (dataLength === 0)
1358
- return 0;
1359
- const kWidth = this.opt.kWidth;
1360
- const kGap = this.opt.kGap;
1361
- const viewWidth = this._internalViewport?.plotWidth ?? 0;
1362
- const dpr = this.getEffectiveDpr();
1363
- const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr);
1364
- const dataPlotWidth = (startXPx + (Chart.LEADING_SLOTS + dataLength + Chart.TRAILING_DRAWING_SLOTS) * unitPx) / dpr;
1365
- return Math.max(dataPlotWidth, viewWidth);
542
+ return this.dataManager.getContentWidth();
543
+ }
544
+ /** 滚动到最右侧(最新数据位置) */
545
+ scrollToRight() {
546
+ this.dataManager.scrollToRight();
1366
547
  }
1367
548
  /** 容器尺寸变化时调用 */
1368
549
  resize() {
1369
- const vp = this.computeViewport();
550
+ const vp = this.viewportManager.computeViewport();
1370
551
  // 防御性检查:容器尺寸无效时跳过布局
1371
552
  if (!vp || vp.viewWidth < 10 || vp.viewHeight < 10) {
1372
553
  return;
1373
554
  }
1374
- this.cachedDrawFrame = null;
1375
- this.layoutPanes();
1376
- this.emitPaneLayoutChange();
1377
- this.updateViewportSignal();
555
+ this.renderer.clearCachedFrame();
556
+ this.layoutManager.layoutPanes();
557
+ this.viewportManager.updateViewportSignal();
1378
558
  this.scheduleDraw();
1379
559
  }
1380
560
  /**
@@ -1382,361 +562,22 @@ export class Chart {
1382
562
  * @param level 更新级别,默认为 All
1383
563
  */
1384
564
  scheduleDraw(level = UpdateLevel.All) {
1385
- // 合并更新级别:如果已有更高级别的调度,保持高级别
1386
- if (this.raf !== null) {
1387
- // 已有 All 级别调度,任何新请求都忽略
1388
- if (this.pendingUpdateLevel === UpdateLevel.All)
1389
- return;
1390
- // 新请求是 All,覆盖之前的 Main/Overlay
1391
- if (level === UpdateLevel.All) {
1392
- this.pendingUpdateLevel = UpdateLevel.All;
1393
- return;
1394
- }
1395
- // Main + Overlay = All
1396
- if ((this.pendingUpdateLevel === UpdateLevel.Main && level === UpdateLevel.Overlay) ||
1397
- (this.pendingUpdateLevel === UpdateLevel.Overlay && level === UpdateLevel.Main)) {
1398
- this.pendingUpdateLevel = UpdateLevel.All;
1399
- return;
1400
- }
1401
- // 同级别或更低级别,忽略
1402
- return;
1403
- }
1404
- this.pendingUpdateLevel = level;
1405
- this.raf = requestAnimationFrame(() => {
1406
- this.raf = null;
1407
- const levelToDraw = this.pendingUpdateLevel;
1408
- this.pendingUpdateLevel = UpdateLevel.All; // 重置为默认值
1409
- this.draw(levelToDraw);
1410
- });
565
+ this.renderer.scheduleDraw(level);
1411
566
  }
1412
567
  /** 销毁图表实例 */
1413
568
  async destroy() {
1414
- if (this.raf !== null) {
1415
- cancelAnimationFrame(this.raf);
1416
- this.raf = null;
1417
- }
1418
- if (this._dataBufferUnsub) {
1419
- this._dataBufferUnsub();
1420
- this._dataBufferUnsub = null;
1421
- }
1422
- this._dataBuffer.dispose();
1423
- this.clearComparisonBuffers();
1424
- // 清理尺寸观察器
1425
- this.resizeObserver?.disconnect();
1426
- this.resizeObserver = undefined;
1427
- this.preciseDpr = 0;
1428
- this.observedSize = { width: 0, height: 0 };
1429
- // 清理 scroll 监听
1430
- if (this.onScroll) {
1431
- this.dom.container?.removeEventListener('scroll', this.onScroll);
1432
- this.onScroll = undefined;
1433
- }
1434
- this._internalViewport = null;
1435
- this.cachedDrawFrame = null;
1436
- this.xAxisCtx = null;
1437
- this.paneRenderers.forEach((r) => r.destroy());
1438
- this.paneRenderers = [];
569
+ this.renderer.destroy();
570
+ this.dataManager.destroy();
571
+ this.viewportManager.destroy();
572
+ this.layoutManager.destroy();
1439
573
  this.sharedWebGLSurface.destroy();
1440
- // 清理渲染器插件管理器(会调用所有 onUninstall)
1441
- this.rendererPluginManager.clear();
1442
- this.indicatorScheduler.destroy();
574
+ this.indicatorManager.destroy();
1443
575
  await this.pluginHost.destroy();
1444
576
  }
1445
- /** 初始化所有 pane */
1446
- initPanes() {
1447
- this.paneRenderers = this.opt.panes.map((spec, index) => {
1448
- const pane = new Pane(spec.id, {
1449
- role: this.resolvePaneRole(spec, index),
1450
- capabilities: spec.capabilities,
1451
- });
1452
- const mainCanvas = document.createElement('canvas');
1453
- const overlayCanvas = document.createElement('canvas');
1454
- const yAxisCanvas = document.createElement('canvas');
1455
- const isMain = pane.role === 'price';
1456
- // Main Canvas - K线、指标、网格
1457
- mainCanvas.id = `${spec.id}-main`;
1458
- mainCanvas.className = isMain ? 'main-canvas main' : 'main-canvas sub';
1459
- mainCanvas.style.position = 'absolute';
1460
- mainCanvas.style.left = '0';
1461
- mainCanvas.style.top = '0';
1462
- // Overlay Canvas - 十字线、Tooltip(透明,事件穿透)
1463
- overlayCanvas.id = `${spec.id}-overlay`;
1464
- overlayCanvas.className = 'overlay-canvas';
1465
- overlayCanvas.style.position = 'absolute';
1466
- overlayCanvas.style.left = '0';
1467
- overlayCanvas.style.top = '0';
1468
- overlayCanvas.style.pointerEvents = 'none'; // 事件穿透到 mainCanvas
1469
- overlayCanvas.style.backgroundColor = 'transparent';
1470
- yAxisCanvas.id = `${spec.id}-yAxis`;
1471
- yAxisCanvas.className = 'right-axis';
1472
- yAxisCanvas.style.position = 'absolute';
1473
- yAxisCanvas.style.left = '0';
1474
- const renderer = new PaneRenderer({ mainCanvas, overlayCanvas, yAxisCanvas }, pane, {
1475
- rightAxisWidth: this.opt.rightAxisWidth,
1476
- yPaddingPx: this.opt.yPaddingPx,
1477
- priceLabelWidth: this.opt.priceLabelWidth,
1478
- }, this.sharedWebGLSurface);
1479
- return renderer;
1480
- });
1481
- const canvasLayer = this.dom.canvasLayer;
1482
- const rightAxisLayer = this.dom.rightAxisLayer;
1483
- if (canvasLayer) {
1484
- const existingCanvases = canvasLayer.querySelectorAll('canvas:not(.x-axis-canvas)');
1485
- existingCanvases.forEach((canvas) => canvas.remove());
1486
- }
1487
- if (rightAxisLayer) {
1488
- const existingAxisCanvases = rightAxisLayer.querySelectorAll('canvas.right-axis');
1489
- existingAxisCanvases.forEach((canvas) => canvas.remove());
1490
- }
1491
- this.paneRenderers.forEach((renderer) => {
1492
- const dom = renderer.getDom();
1493
- // 先添加 mainCanvas,再添加 overlayCanvas(overlay 在上层)
1494
- canvasLayer.appendChild(dom.mainCanvas);
1495
- canvasLayer.appendChild(dom.overlayCanvas);
1496
- rightAxisLayer.appendChild(dom.yAxisCanvas);
1497
- });
1498
- this.rendererPluginManager.setKnownPaneIds(this.paneRenderers.map((renderer) => renderer.getPane().id));
1499
- }
1500
- syncPaneRatiosFromSpecs(specs) {
1501
- const next = new Map();
1502
- for (const spec of specs) {
1503
- const prev = this._internalPaneRatios.get(spec.id);
1504
- const incoming = Number.isFinite(spec.ratio) ? spec.ratio : 0;
1505
- const ratio = prev !== undefined ? prev : (incoming > 0 ? incoming : 1);
1506
- next.set(spec.id, ratio);
1507
- }
1508
- this._internalPaneRatios = next;
1509
- this.normalizeVisiblePaneRatios(specs);
1510
- this.syncPaneRatiosToSpecs();
1511
- }
1512
- syncPaneRatiosToSpecs() {
1513
- const visible = this.opt.panes.filter(p => p.visible !== false);
1514
- const visibleSum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0);
1515
- const safeVisibleSum = visibleSum > 0 ? visibleSum : 1;
1516
- this.opt.panes = this.opt.panes.map((spec) => {
1517
- const ratio = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0;
1518
- if (spec.visible === false) {
1519
- return { ...spec, ratio };
1520
- }
1521
- return { ...spec, ratio: ratio / safeVisibleSum };
1522
- });
1523
- }
1524
- normalizeVisiblePaneRatios(specs) {
1525
- const visible = specs.filter(p => p.visible !== false);
1526
- if (visible.length === 0)
1527
- return;
1528
- let sum = 0;
1529
- for (const spec of visible) {
1530
- const raw = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0;
1531
- const safe = Number.isFinite(raw) && raw > 0 ? raw : 0;
1532
- this._internalPaneRatios.set(spec.id, safe);
1533
- sum += safe;
1534
- }
1535
- if (sum <= 0) {
1536
- const equal = 1 / visible.length;
1537
- for (const spec of visible) {
1538
- this._internalPaneRatios.set(spec.id, equal);
1539
- }
1540
- return;
1541
- }
1542
- for (const spec of visible) {
1543
- const v = this._internalPaneRatios.get(spec.id) ?? 0;
1544
- this._internalPaneRatios.set(spec.id, v / sum);
1545
- }
1546
- }
1547
- getPaneMinHeight(spec, plotHeight) {
1548
- const fallback = this.opt.defaultPaneMinHeightPx ?? 120; // 最小高度
1549
- const raw = spec.minHeightPx ?? fallback;
1550
- return Math.max(1, Math.min(Math.round(raw), Math.max(1, plotHeight)));
1551
- }
1552
- computePaneHeightsByRatio(visibleSpecs, availableH) {
1553
- if (visibleSpecs.length === 0)
1554
- return [];
1555
- const ratios = visibleSpecs.map(spec => this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0);
1556
- const ratioSum = ratios.reduce((s, r) => s + (r > 0 ? r : 0), 0);
1557
- const safeRatios = ratioSum > 0
1558
- ? ratios.map(r => (r > 0 ? r : 0) / ratioSum)
1559
- : visibleSpecs.map(() => 1 / visibleSpecs.length);
1560
- const heights = safeRatios.map(r => Math.max(1, Math.round(availableH * r)));
1561
- const mins = visibleSpecs.map(spec => this.getPaneMinHeight(spec, availableH));
1562
- for (let i = 0; i < heights.length; i++) {
1563
- heights[i] = Math.max(heights[i], Math.min(mins[i], availableH));
1564
- }
1565
- let total = heights.reduce((s, h) => s + h, 0);
1566
- if (total > availableH) {
1567
- let overflow = total - availableH;
1568
- while (overflow > 0) {
1569
- let shrunk = false;
1570
- for (let i = heights.length - 1; i >= 0 && overflow > 0; i--) {
1571
- const minH = Math.max(1, Math.min(mins[i], availableH));
1572
- const h = heights[i];
1573
- if (h > minH) {
1574
- heights[i] = h - 1;
1575
- overflow--;
1576
- shrunk = true;
1577
- }
1578
- }
1579
- if (!shrunk)
1580
- break;
1581
- }
1582
- }
1583
- else if (total < availableH) {
1584
- heights[heights.length - 1] = (heights[heights.length - 1] ?? 1) + (availableH - total);
1585
- }
1586
- total = heights.reduce((s, h) => s + h, 0);
1587
- if (total !== availableH && heights.length > 0) {
1588
- heights[heights.length - 1] = Math.max(1, (heights[heights.length - 1] ?? 1) + (availableH - total));
1589
- }
1590
- return heights;
1591
- }
1592
- /** 计算每个 pane 的布局(top 和 height) */
1593
- layoutPanes() {
1594
- const vp = this._internalViewport;
1595
- if (!vp)
1596
- return;
1597
- const visibleSpecs = this.opt.panes.filter(p => p.visible !== false);
1598
- if (visibleSpecs.length === 0)
1599
- return;
1600
- const gap = Math.max(0, this.opt.paneGap ?? 0);
1601
- let y = 0;
1602
- const totalGaps = gap * Math.max(0, visibleSpecs.length - 1);
1603
- const availableH = Math.max(1, vp.plotHeight - totalGaps);
1604
- this.normalizeVisiblePaneRatios(visibleSpecs);
1605
- const paneHeights = this.computePaneHeightsByRatio(visibleSpecs, availableH);
1606
- for (let i = 0; i < visibleSpecs.length; i++) {
1607
- const spec = visibleSpecs[i];
1608
- if (!spec)
1609
- continue;
1610
- const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id);
1611
- if (!renderer)
1612
- continue;
1613
- const pane = renderer.getPane();
1614
- const h = paneHeights[i] ?? 1;
1615
- pane.setLayout(y, h);
1616
- pane.setPadding(this.opt.yPaddingPx, this.opt.yPaddingPx);
1617
- renderer.resize(vp.plotWidth, h, vp.dpr);
1618
- renderer.setWebGLRegion({
1619
- x: 0,
1620
- y,
1621
- width: vp.plotWidth,
1622
- height: h,
1623
- dpr: vp.dpr,
1624
- });
1625
- this.rendererPluginManager.notifyResize(pane.id, wrapPaneInfo(pane));
1626
- const dom = renderer.getDom();
1627
- dom.mainCanvas.style.top = `${y}px`;
1628
- dom.overlayCanvas.style.top = `${y}px`;
1629
- dom.yAxisCanvas.style.top = `${y}px`;
1630
- dom.yAxisCanvas.style.left = '0px';
1631
- y += h + gap;
1632
- }
1633
- // 按实际像素高度回写 ratio,确保后续 resize 视觉比例稳定
1634
- const finalAvailable = Math.max(1, availableH);
1635
- for (const spec of visibleSpecs) {
1636
- const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id);
1637
- if (!renderer)
1638
- continue;
1639
- const h = renderer.getPane().height;
1640
- this._internalPaneRatios.set(spec.id, h / finalAvailable);
1641
- }
1642
- this.normalizeVisiblePaneRatios(visibleSpecs);
1643
- this.syncPaneRatiosToSpecs();
1644
- }
1645
577
  computeViewport() {
1646
- const container = this.dom.container;
1647
- if (!container)
1648
- return null;
1649
- const observedWidth = this.observedSize.width;
1650
- const observedHeight = this.observedSize.height;
1651
- const viewWidth = observedWidth > 0
1652
- ? observedWidth
1653
- : Math.max(1, Math.round(container.clientWidth));
1654
- const viewHeight = observedHeight > 0
1655
- ? observedHeight
1656
- : Math.max(1, Math.round(container.clientHeight));
1657
- const plotWidth = Math.round(viewWidth);
1658
- const plotHeight = Math.round(viewHeight - this.opt.bottomAxisHeight);
1659
- let dpr = this.getEffectiveDpr();
1660
- const MAX_CANVAS_PIXELS = 16 * 1024 * 1024;
1661
- const requestedPixels = viewWidth * dpr * (viewHeight * dpr);
1662
- if (requestedPixels > MAX_CANVAS_PIXELS) {
1663
- dpr = Math.sqrt(MAX_CANVAS_PIXELS / (viewWidth * viewHeight));
1664
- }
1665
- // 对齐 scrollLeft,消除 translate 亚像素偏移
1666
- const scrollLeft = Math.round(this.cachedScrollLeft * dpr) / dpr;
1667
- const canvasLayerWidth = `${viewWidth}px`;
1668
- if (this.dom.canvasLayer.style.width !== canvasLayerWidth) {
1669
- this.dom.canvasLayer.style.width = canvasLayerWidth;
1670
- }
1671
- const canvasLayerHeight = `${viewHeight}px`;
1672
- if (this.dom.canvasLayer.style.height !== canvasLayerHeight) {
1673
- this.dom.canvasLayer.style.height = canvasLayerHeight;
1674
- }
1675
- const xAxisWidth = Math.round(plotWidth * dpr);
1676
- if (this.dom.xAxisCanvas.width !== xAxisWidth) {
1677
- this.dom.xAxisCanvas.width = xAxisWidth;
1678
- }
1679
- const xAxisHeight = Math.round(this.opt.bottomAxisHeight * dpr);
1680
- if (this.dom.xAxisCanvas.height !== xAxisHeight) {
1681
- this.dom.xAxisCanvas.height = xAxisHeight;
1682
- }
1683
- const xAxisCssWidth = `${xAxisWidth / dpr}px`;
1684
- if (this.dom.xAxisCanvas.style.width !== xAxisCssWidth) {
1685
- this.dom.xAxisCanvas.style.width = xAxisCssWidth;
1686
- }
1687
- const xAxisCssHeight = `${xAxisHeight / dpr}px`;
1688
- if (this.dom.xAxisCanvas.style.height !== xAxisCssHeight) {
1689
- this.dom.xAxisCanvas.style.height = xAxisCssHeight;
1690
- }
1691
- this.sharedWebGLSurface.resize(plotWidth, plotHeight, dpr);
1692
- const vp = {
1693
- viewWidth,
1694
- viewHeight,
1695
- plotWidth,
1696
- plotHeight,
1697
- scrollLeft,
1698
- dpr,
1699
- };
1700
- const prevViewport = this._internalViewport;
1701
- const viewportChanged = !prevViewport
1702
- || prevViewport.viewWidth !== vp.viewWidth
1703
- || prevViewport.viewHeight !== vp.viewHeight
1704
- || prevViewport.plotWidth !== vp.plotWidth
1705
- || prevViewport.plotHeight !== vp.plotHeight
1706
- || prevViewport.scrollLeft !== vp.scrollLeft
1707
- || prevViewport.dpr !== vp.dpr;
1708
- this._internalViewport = vp;
1709
- if (viewportChanged) {
1710
- const current = this._viewportSignal.peek();
1711
- this._viewportSignal.set({
1712
- zoomLevel: current.zoomLevel,
1713
- plotWidth: vp.plotWidth,
1714
- plotHeight: vp.plotHeight,
1715
- dpr: vp.dpr > 0 ? vp.dpr : current.dpr,
1716
- visibleFrom: current.visibleFrom,
1717
- visibleTo: current.visibleTo,
1718
- desiredScrollLeft: current.desiredScrollLeft,
1719
- kWidth: current.kWidth,
1720
- kGap: current.kGap,
1721
- });
1722
- }
1723
- return vp;
578
+ return this.viewportManager.computeViewport();
1724
579
  }
1725
580
  // ==================== Facade API (High-level interface for adapters) ====================
1726
- // ---------- Signals ----------
1727
- _viewportSignal = createSignal({
1728
- zoomLevel: 1,
1729
- plotWidth: 0,
1730
- plotHeight: 0,
1731
- dpr: 1,
1732
- visibleFrom: 0,
1733
- visibleTo: 0,
1734
- desiredScrollLeft: undefined,
1735
- kWidth: 0,
1736
- kGap: 1,
1737
- });
1738
- _dataSignal = createSignal([]);
1739
- _symbolsSignal = createSignal([]);
1740
581
  _themeSignal = createSignal('light');
1741
582
  _drawingToolSignal = createSignal(null);
1742
583
  _drawingsSignal = createSignal([]);
@@ -1758,46 +599,25 @@ export class Chart {
1758
599
  hoveredPaneBoundaryId: null,
1759
600
  isHoveringRightAxis: false,
1760
601
  });
1761
- _indicatorsComputed = computed(() => {
1762
- const mainIndicators = [...this._mainIndicatorsSignal().entries()].map(([id, entry]) => ({
1763
- id,
1764
- definitionId: id,
1765
- label: id,
1766
- name: id,
1767
- role: 'main',
1768
- params: { ...entry.params },
1769
- }));
1770
- const subIndicators = this.subPaneManager.entriesSignal().map(entry => ({
1771
- id: entry.paneId,
1772
- definitionId: entry.indicatorId,
1773
- label: entry.indicatorId,
1774
- name: entry.indicatorId,
1775
- role: 'sub',
1776
- paneId: entry.paneId,
1777
- params: { ...entry.params },
1778
- }));
1779
- return [...mainIndicators, ...subIndicators];
1780
- });
1781
- _subPanesComputed = computed(() => {
1782
- const ratios = this._paneRatiosSignal();
1783
- return this.subPaneManager.entriesSignal().map(entry => ({
1784
- paneId: entry.paneId,
1785
- indicatorId: entry.indicatorId,
1786
- params: { ...entry.params },
1787
- ratio: ratios[entry.paneId] ?? 1,
1788
- }));
1789
- });
1790
602
  /** 视口状态信号 */
1791
603
  get viewport() {
1792
- return this._viewportSignal;
604
+ return this.viewportManager.viewportSignal;
1793
605
  }
1794
606
  /** 数据信号 */
1795
607
  get data() {
1796
- return this._dataSignal;
608
+ return this.dataManager.data;
1797
609
  }
1798
610
  /** 符号信号 */
1799
611
  get symbols() {
1800
- return this._symbolsSignal;
612
+ return this.dataManager.symbols;
613
+ }
614
+ /** 比较商品颜色信号 */
615
+ get comparisonColors() {
616
+ return this.dataManager.comparisonColors;
617
+ }
618
+ /** 比较商品加载信号 */
619
+ get comparisonLoading() {
620
+ return this.dataManager.comparisonLoading;
1801
621
  }
1802
622
  /** 主题信号 */
1803
623
  get theme() {
@@ -1805,11 +625,11 @@ export class Chart {
1805
625
  }
1806
626
  /** 指标实例列表信号(派生信号,自动随主/副图状态更新) */
1807
627
  get indicators() {
1808
- return this._indicatorsComputed;
628
+ return this.indicatorManager.indicatorsComputed;
1809
629
  }
1810
630
  /** 子图信息信号(派生信号,自动随副图条目/比例更新) */
1811
631
  get subPanes() {
1812
- return this._subPanesComputed;
632
+ return this.indicatorManager.subPanesComputed;
1813
633
  }
1814
634
  /** 当前绘图工具信号 */
1815
635
  get drawingTool() {
@@ -1831,205 +651,29 @@ export class Chart {
1831
651
  return this._interactionSignal;
1832
652
  }
1833
653
  // ---------- Data ----------
1834
- /**
1835
- * 设置数据(高层 API)
1836
- * 内部调用 updateData,并更新 data signal
1837
- */
1838
654
  setData(data) {
1839
- this.updateData(data);
655
+ this.dataManager.setData(data);
1840
656
  }
1841
- /**
1842
- * 追加数据(高层 API)
1843
- * 合并现有数据并更新
1844
- */
1845
657
  appendData(newData) {
1846
- const merged = [...this._internalData, ...newData];
1847
- this.setData(merged);
658
+ this.dataManager.appendData(newData);
1848
659
  }
1849
- /**
1850
- * 设置数据获取器适配器
1851
- */
1852
660
  setDataFetcher(fetcher) {
1853
- this._dataFetcher = fetcher;
1854
- this._dataBuffer.setFetcher(fetcher);
1855
- for (const buffer of this._comparisonBuffers.values()) {
1856
- buffer.setFetcher(fetcher);
1857
- }
661
+ this.dataManager.setDataFetcher(fetcher);
1858
662
  }
1859
663
  get dataBuffer() {
1860
- return this._dataBuffer;
664
+ return this.dataManager.dataBuffer;
1861
665
  }
1862
666
  checkVisibleRangeGap() {
1863
- if (this._internalData.length === 0)
1864
- return;
1865
- const window = this._dataBuffer.loadedWindow;
1866
- if (!window)
1867
- return;
1868
- const range = this.lastVisibleRange;
1869
- if (range.start <= 5 && this._dataFetcher) {
1870
- const MS_PER_DAY = 86_400_000;
1871
- const earlierThanEarliest = window.earliestTs - 90 * MS_PER_DAY;
1872
- this._dataBuffer.ensureRange(earlierThanEarliest, window.earliestTs);
1873
- return;
1874
- }
1875
- if (range.start >= this._internalData.length)
1876
- return;
1877
- const firstVisibleTs = this._internalData[Math.max(0, range.start)]?.timestamp;
1878
- if (firstVisibleTs === undefined)
1879
- return;
1880
- if (firstVisibleTs < window.earliestTs) {
1881
- this._dataBuffer.ensureRange(firstVisibleTs, window.earliestTs);
1882
- }
1883
- }
1884
- getComparisonEquivalentPriceRange(range) {
1885
- if (this._comparisonSpecs.length === 0 || this._comparisonData.size === 0)
1886
- return null;
1887
- const baseIndex = Math.max(0, range.start);
1888
- const mainBase = this._internalData[baseIndex]?.close;
1889
- const baseTimestamp = this._internalData[baseIndex]?.timestamp;
1890
- if (!Number.isFinite(mainBase) || mainBase <= 0 || baseTimestamp === undefined)
1891
- return null;
1892
- let min = Number.POSITIVE_INFINITY;
1893
- let max = Number.NEGATIVE_INFINITY;
1894
- for (const spec of this._comparisonSpecs) {
1895
- const data = this._comparisonData.get(spec.symbol);
1896
- if (!data?.length)
1897
- continue;
1898
- const baseline = this.findComparisonBaseline(data, baseTimestamp);
1899
- if (!baseline || !Number.isFinite(baseline.close) || baseline.close <= 0)
1900
- continue;
1901
- const byTimestamp = new Map();
1902
- for (const item of data)
1903
- byTimestamp.set(item.timestamp, item);
1904
- for (let i = range.start; i < range.end && i < this._internalData.length; i++) {
1905
- const mainItem = this._internalData[i];
1906
- if (!mainItem)
1907
- continue;
1908
- const item = byTimestamp.get(mainItem.timestamp);
1909
- if (!item || !Number.isFinite(item.close))
1910
- continue;
1911
- const pct = (item.close - baseline.close) / baseline.close;
1912
- const equivalentPrice = mainBase * (1 + pct);
1913
- if (!Number.isFinite(equivalentPrice))
1914
- continue;
1915
- min = Math.min(min, equivalentPrice);
1916
- max = Math.max(max, equivalentPrice);
1917
- }
1918
- }
1919
- if (!Number.isFinite(min) || !Number.isFinite(max))
1920
- return null;
1921
- return { min, max };
1922
- }
1923
- findComparisonBaseline(data, timestamp) {
1924
- for (const item of data) {
1925
- if (item.timestamp >= timestamp)
1926
- return item;
1927
- }
1928
- return null;
1929
- }
1930
- mergeNumericRanges(left, right) {
1931
- if (!left)
1932
- return right ?? null;
1933
- if (!right)
1934
- return left;
1935
- return {
1936
- min: Math.min(left.min, right.min),
1937
- max: Math.max(left.max, right.max),
1938
- };
667
+ this.dataManager.checkVisibleRangeGap();
1939
668
  }
1940
- syncComparisonBuffers(specs) {
1941
- this._comparisonSpecs = [...specs];
1942
- const nextKeys = new Set(specs.map((spec) => spec.symbol));
1943
- for (const [key, buffer] of this._comparisonBuffers) {
1944
- if (nextKeys.has(key))
1945
- continue;
1946
- this._comparisonBufferUnsubs.get(key)?.();
1947
- this._comparisonBufferUnsubs.delete(key);
1948
- buffer.dispose();
1949
- this._comparisonBuffers.delete(key);
1950
- this._comparisonData.delete(key);
1951
- }
1952
- if (!this._dataFetcher)
1953
- return;
1954
- for (const spec of specs) {
1955
- const key = spec.symbol;
1956
- let buffer = this._comparisonBuffers.get(key);
1957
- if (!buffer) {
1958
- const newBuffer = new DataBuffer();
1959
- newBuffer.setFetcher(this._dataFetcher);
1960
- this._comparisonBuffers.set(key, newBuffer);
1961
- const unsubscribe = newBuffer.data.subscribe(() => {
1962
- this._comparisonData.set(key, [...newBuffer.data.peek()]);
1963
- this.scheduleDraw();
1964
- });
1965
- this._comparisonBufferUnsubs.set(key, unsubscribe);
1966
- buffer = newBuffer;
1967
- }
1968
- else {
1969
- buffer.setFetcher(this._dataFetcher);
1970
- }
1971
- buffer.setSymbol(spec);
1972
- }
669
+ setSymbols(specs) {
670
+ this.dataManager.setSymbols(specs);
1973
671
  }
1974
- clearComparisonBuffers() {
1975
- for (const unsubscribe of this._comparisonBufferUnsubs.values())
1976
- unsubscribe();
1977
- this._comparisonBufferUnsubs.clear();
1978
- for (const buffer of this._comparisonBuffers.values())
1979
- buffer.dispose();
1980
- this._comparisonBuffers.clear();
1981
- this._comparisonData.clear();
1982
- this._comparisonSpecs = [];
672
+ addComparisonSymbol(spec) {
673
+ this.dataManager.addComparisonSymbol(spec);
1983
674
  }
1984
- /**
1985
- * 设置当前符号并触发数据加载
1986
- */
1987
- setSymbols(specs) {
1988
- this._symbolsSignal.set(specs);
1989
- if (specs.length === 0) {
1990
- this.clearComparisonBuffers();
1991
- return;
1992
- }
1993
- const spec = specs[0];
1994
- this.syncComparisonBuffers(specs.slice(1));
1995
- if (!this._dataFetcher)
1996
- return;
1997
- this._dataBuffer.setFetcher(this._dataFetcher);
1998
- this._dataBuffer.onPrepend = (count) => {
1999
- const dpr = this.getEffectiveDpr();
2000
- const { unitPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr);
2001
- const compensation = (count * unitPx) / dpr;
2002
- const container = this.dom.container;
2003
- if (container) {
2004
- container.scrollLeft += compensation;
2005
- this.cachedScrollLeft = container.scrollLeft;
2006
- }
2007
- };
2008
- if (!this._dataBufferUnsub) {
2009
- this._dataBufferUnsub = this._dataBuffer.data.subscribe(() => {
2010
- const bufferData = this._dataBuffer.data.peek();
2011
- this._internalData = [...bufferData];
2012
- this._dataSignal.set([...this._internalData]);
2013
- this.interaction.reset();
2014
- if (this.lastVisibleRange.start === 0 && this.lastVisibleRange.end === 0 && this._internalData.length > 0) {
2015
- const plotWidth = this.observedSize.width > 0
2016
- ? this.observedSize.width
2017
- : Math.max(1, Math.round(this.dom.container?.clientWidth ?? 800));
2018
- const dpr = this.getEffectiveDpr();
2019
- const { start, end } = getVisibleRange(this.cachedScrollLeft, plotWidth, this.opt.kWidth, this.opt.kGap, this._internalData.length, dpr);
2020
- this.lastVisibleRange = { start, end };
2021
- }
2022
- const indicatorsReady = this.indicatorScheduler.update(this._internalData, this.lastVisibleRange);
2023
- if (indicatorsReady) {
2024
- this.pendingIndicatorDataUpdate = false;
2025
- this.scheduleDraw();
2026
- }
2027
- else {
2028
- this.pendingIndicatorDataUpdate = true;
2029
- }
2030
- });
2031
- }
2032
- this._dataBuffer.setSymbol(spec);
675
+ removeComparisonSymbol(symbol) {
676
+ this.dataManager.removeComparisonSymbol(symbol);
2033
677
  }
2034
678
  // ---------- Theme ----------
2035
679
  /**
@@ -2045,54 +689,19 @@ export class Chart {
2045
689
  * 计算并应用新的 render state,更新 viewport signal
2046
690
  */
2047
691
  zoomToLevel(level, anchorX) {
2048
- const clamped = Math.max(1, Math.min(this.zoomLevelCount, Math.round(level)));
2049
- this.applyZoom(clamped, anchorX);
692
+ this.zoomController.zoomToLevel(level, anchorX);
2050
693
  }
2051
694
  /**
2052
695
  * 放大(高层 API)
2053
696
  */
2054
697
  zoomIn(anchorX) {
2055
- this.zoomToLevel(this.currentZoomLevel + 1, anchorX);
698
+ this.zoomController.zoomIn(anchorX);
2056
699
  }
2057
700
  /**
2058
701
  * 缩小(高层 API)
2059
702
  */
2060
703
  zoomOut(anchorX) {
2061
- this.zoomToLevel(this.currentZoomLevel - 1, anchorX);
2062
- }
2063
- /**
2064
- * 内部缩放实现
2065
- * 使用 computeZoom 纯函数计算精确的 scrollLeft
2066
- */
2067
- applyZoom(targetLevel, anchorViewportX) {
2068
- if (targetLevel === this.currentZoomLevel)
2069
- return;
2070
- const delta = targetLevel - this.currentZoomLevel;
2071
- const scrollLeft = this.getCachedScrollLeft();
2072
- const dpr = this.getCurrentDpr();
2073
- const result = computeZoom(delta, anchorViewportX ?? 0, scrollLeft, this.currentZoomLevel, this.opt.kWidth, this.opt.kGap, {
2074
- minKWidth: this.opt.minKWidth,
2075
- maxKWidth: this.opt.maxKWidth,
2076
- zoomLevelCount: this.zoomLevelCount,
2077
- dpr,
2078
- });
2079
- if (!result)
2080
- return;
2081
- // 应用 render state
2082
- this.currentZoomLevel = result.targetLevel;
2083
- this.applyRenderState(result.newKWidth, result.newKGap, result.targetLevel);
2084
- // 更新 viewport signal
2085
- this._viewportSignal.set({
2086
- zoomLevel: result.targetLevel,
2087
- plotWidth: this._internalViewport?.plotWidth ?? 0,
2088
- plotHeight: this._internalViewport?.plotHeight ?? 0,
2089
- dpr,
2090
- visibleFrom: this.lastVisibleRange.start,
2091
- visibleTo: this.lastVisibleRange.end,
2092
- desiredScrollLeft: result.newScrollLeft,
2093
- kWidth: result.newKWidth,
2094
- kGap: result.newKGap,
2095
- });
704
+ this.zoomController.zoomOut(anchorX);
2096
705
  }
2097
706
  // ---------- Interaction (Zero-config unified entry) ----------
2098
707
  /**
@@ -2167,21 +776,15 @@ export class Chart {
2167
776
  * 使用 computeZoom 计算精确的 scrollLeft,更新 viewport signal
2168
777
  */
2169
778
  handleWheelEvent(e) {
2170
- const delta = e.deltaY > 0 ? -1 : 1;
2171
- const targetLevel = Math.max(1, Math.min(this.zoomLevelCount, this.currentZoomLevel + delta));
2172
- if (targetLevel === this.currentZoomLevel)
2173
- return;
2174
- // 获取鼠标在视口中的位置作为缩放锚点(视口局部坐标)
2175
779
  const rect = this.dom.container.getBoundingClientRect();
2176
- const mouseX = e.clientX - rect.left;
2177
- this.applyZoom(targetLevel, mouseX);
780
+ this.zoomController.handleWheel(e.deltaY, e.clientX - rect.left);
2178
781
  }
2179
782
  /**
2180
783
  * 滚动事件处理(高层 API)
2181
784
  * 更新缓存的 scrollLeft 并触发交互 controller
2182
785
  */
2183
786
  handleScrollEvent() {
2184
- this.interaction.onScroll({ scheduleDraw: !this.pendingIndicatorDataUpdate });
787
+ this.interaction.onScroll({ scheduleDraw: !this.dataManager.pendingIndicatorDataUpdate });
2185
788
  // 更新 viewport signal 中的 visible range
2186
789
  this.updateViewportSignal();
2187
790
  }
@@ -2191,31 +794,13 @@ export class Chart {
2191
794
  * @param centerClientX 捏合中心在视口中的 X 坐标
2192
795
  */
2193
796
  handlePinchZoom(delta, centerClientX) {
2194
- const targetLevel = Math.max(1, Math.min(this.zoomLevelCount, this.currentZoomLevel + delta));
2195
- if (targetLevel === this.currentZoomLevel)
2196
- return;
2197
- // centerClientX 已经是视口局部坐标,直接使用
2198
- this.applyZoom(targetLevel, centerClientX);
797
+ this.zoomController.handlePinch(delta, centerClientX);
2199
798
  }
2200
799
  /**
2201
- * 更新 viewport signal(用于滚动事件,不更新 desiredScrollLeft)
800
+ * 更新 viewport signal(用于滚动事件)
2202
801
  */
2203
802
  updateViewportSignal() {
2204
- const vp = this._internalViewport;
2205
- if (!vp)
2206
- return;
2207
- this._viewportSignal.set({
2208
- zoomLevel: this.currentZoomLevel,
2209
- plotWidth: vp.plotWidth,
2210
- plotHeight: vp.plotHeight,
2211
- dpr: vp.dpr,
2212
- visibleFrom: this.lastVisibleRange.start,
2213
- visibleTo: this.lastVisibleRange.end,
2214
- // 滚动事件不设置 desiredScrollLeft
2215
- desiredScrollLeft: undefined,
2216
- kWidth: this.opt.kWidth,
2217
- kGap: this.opt.kGap,
2218
- });
803
+ this.viewportManager.updateViewportSignal();
2219
804
  }
2220
805
  // ---------- Indicators (Explicit role) ----------
2221
806
  /**
@@ -2226,71 +811,16 @@ export class Chart {
2226
811
  * @returns 实例 ID(成功)或 null(失败)
2227
812
  */
2228
813
  addIndicator(definitionId, role, params) {
2229
- if (role === 'main') {
2230
- const success = this.enableMainIndicator(definitionId, params);
2231
- if (!success)
2232
- return null;
2233
- return definitionId.toUpperCase();
2234
- }
2235
- else {
2236
- // 副图指标
2237
- const paneId = `${definitionId.toUpperCase()}_${Date.now()}`;
2238
- const success = this.createSubPane(paneId, definitionId, params);
2239
- if (!success)
2240
- return null;
2241
- return paneId;
2242
- }
814
+ return this.indicatorManager.addIndicator(definitionId, role, params);
2243
815
  }
2244
- /**
2245
- * 移除指标(高层 API)
2246
- * @param instanceId 指标实例 ID
2247
- * @returns 是否成功移除
2248
- */
2249
816
  removeIndicator(instanceId) {
2250
- const id = instanceId.toUpperCase();
2251
- // 先尝试作为主图指标移除
2252
- if (this._mainIndicatorsSignal.peek().has(id)) {
2253
- return this.disableMainIndicator(instanceId);
2254
- }
2255
- // 再尝试作为副图指标移除
2256
- const subPaneEntry = this.getSubPaneEntry(instanceId);
2257
- if (subPaneEntry) {
2258
- this.removeSubPane(instanceId);
2259
- return true;
2260
- }
2261
- return false;
817
+ return this.indicatorManager.removeIndicator(instanceId);
2262
818
  }
2263
- /**
2264
- * 更新指标参数(高层 API)
2265
- * @param instanceId 指标实例 ID
2266
- * @param params 新参数
2267
- * @returns 是否成功更新
2268
- */
2269
819
  updateIndicatorParams(instanceId, params) {
2270
- const id = instanceId.toUpperCase();
2271
- // 先尝试作为主图指标更新
2272
- if (this._mainIndicatorsSignal.peek().has(id)) {
2273
- this.updateMainIndicatorParams(instanceId, params);
2274
- return true;
2275
- }
2276
- // 再尝试作为副图指标更新
2277
- const subPaneEntry = this.getSubPaneEntry(instanceId);
2278
- if (subPaneEntry) {
2279
- this.updateSubPaneParams(instanceId, params);
2280
- return true;
2281
- }
2282
- return false;
820
+ return this.indicatorManager.updateIndicatorParams(instanceId, params);
2283
821
  }
2284
- /**
2285
- * 重新排序指标(高层 API)
2286
- * @param orderedInstanceIds 排序后的指标实例 ID 数组
2287
- * @returns 是否成功
2288
- */
2289
822
  reorderIndicators(orderedInstanceIds) {
2290
- // TODO: 实现副图指标的重新排序
2291
- // 需要调用 updatePaneLayout 来调整 pane 顺序
2292
- console.warn('[Chart] reorderIndicators not fully implemented yet');
2293
- return false;
823
+ return this.indicatorManager.reorderIndicators(orderedInstanceIds);
2294
824
  }
2295
825
  // ---------- Sub Panes ----------
2296
826
  /**