@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.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +30 -4
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +9 -2
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +3 -3
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +6 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +88 -47
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +1 -0
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +1 -0
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +3 -0
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts +3 -0
- package/dist/data-fetchers/tradingview.d.ts.map +1 -0
- package/dist/data-fetchers/tradingview.js +45 -0
- package/dist/data-fetchers/tradingview.js.map +1 -0
- package/dist/engine/chart.d.ts +34 -351
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +246 -1716
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartContext.d.ts +24 -0
- package/dist/engine/chartContext.d.ts.map +1 -0
- package/dist/engine/chartContext.js +19 -0
- package/dist/engine/chartContext.js.map +1 -0
- package/dist/engine/chartTypes.d.ts +77 -0
- package/dist/engine/chartTypes.d.ts.map +1 -0
- package/dist/engine/chartTypes.js +2 -0
- package/dist/engine/chartTypes.js.map +1 -0
- package/dist/engine/controller/interaction.d.ts +1 -0
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +9 -2
- package/dist/engine/controller/interaction.js.map +1 -1
- package/dist/engine/data/chartDataManager.d.ts +102 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +590 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +438 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- package/dist/engine/viewport/viewport.js +1 -1
- package/dist/engine/viewport/viewport.js.map +1 -1
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +49 -13
- package/src/controllers/types.ts +9 -2
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +70 -22
- package/src/data-fetchers/index.ts +1 -0
- package/src/data-fetchers/router.ts +3 -0
- package/src/data-fetchers/tradingview.ts +48 -0
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +260 -2103
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
- package/src/engine/controller/interaction.ts +10 -2
- package/src/engine/data/chartDataManager.ts +691 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +579 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- package/src/engine/renderers/comparisonLine.ts +25 -11
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/engine/viewport/viewport.ts +1 -1
- package/src/plugin/types.ts +1 -0
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -619
package/dist/engine/chart.js
CHANGED
|
@@ -1,116 +1,39 @@
|
|
|
1
|
-
import { createSignal
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
14
|
+
export { getPhysicalKLineConfig };
|
|
31
15
|
export class Chart {
|
|
32
16
|
dom;
|
|
33
17
|
opt;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
/**
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
53
|
+
return this.indicatorManager.getActiveMainIndicators();
|
|
193
54
|
}
|
|
194
|
-
/**
|
|
195
|
-
* 检查主图指标是否激活
|
|
196
|
-
* @param indicatorId 指标ID
|
|
197
|
-
*/
|
|
198
55
|
isMainIndicatorActive(indicatorId) {
|
|
199
|
-
return this.
|
|
56
|
+
return this.indicatorManager.isMainIndicatorActive(indicatorId);
|
|
200
57
|
}
|
|
201
|
-
/**
|
|
202
|
-
* 更新主图指标参数
|
|
203
|
-
* @param indicatorId 指标ID
|
|
204
|
-
* @param params 参数对象
|
|
205
|
-
*/
|
|
206
58
|
updateMainIndicatorParams(indicatorId, params) {
|
|
207
|
-
|
|
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.
|
|
62
|
+
return this.indicatorManager.getMainIndicatorParams(indicatorId);
|
|
232
63
|
}
|
|
233
|
-
/**
|
|
234
|
-
* 清除所有主图指标
|
|
235
|
-
*/
|
|
236
64
|
clearMainIndicators() {
|
|
237
|
-
|
|
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.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this.
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
441
|
-
this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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.
|
|
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.
|
|
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
|
|
252
|
+
this.renderer.updateSettings(settings);
|
|
514
253
|
this.interaction.updateSettings(settings);
|
|
515
254
|
// 同步刻度类型设置到所有 pane(百分比仅用于主图)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
308
|
+
return this.renderer.getMarkerManager();
|
|
812
309
|
}
|
|
813
310
|
/** 更新自定义标记 */
|
|
814
311
|
updateCustomMarkers(markers) {
|
|
815
|
-
this.
|
|
312
|
+
this.renderer.getMarkerManager().setCustomMarkers(markers);
|
|
816
313
|
this.scheduleDraw();
|
|
817
314
|
}
|
|
818
315
|
/** 清除自定义标记 */
|
|
819
316
|
clearCustomMarkers() {
|
|
820
|
-
this.
|
|
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.
|
|
885
|
-
this.applyPaneLayoutSpecs(panes);
|
|
351
|
+
this.layoutManager.updatePaneLayout(panes);
|
|
886
352
|
}
|
|
887
353
|
setPaneDefinitions(defs) {
|
|
888
|
-
this.
|
|
354
|
+
this.layoutManager.setPaneDefinitions(defs);
|
|
889
355
|
}
|
|
890
356
|
upsertPane(def) {
|
|
891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
373
|
+
const store = this.renderer.getDrawingStore();
|
|
374
|
+
if (store.getSelectedId() === id)
|
|
934
375
|
return;
|
|
935
|
-
|
|
376
|
+
store.setSelectedId(id);
|
|
936
377
|
this.scheduleDraw();
|
|
937
378
|
}
|
|
938
|
-
/** 获取当前 pane 布局快照(含 ratio) */
|
|
939
379
|
getPaneLayoutSpecs() {
|
|
940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
405
|
+
this.indicatorManager.updateSubPaneParams(paneId, params);
|
|
1142
406
|
}
|
|
1143
|
-
/**
|
|
1144
|
-
* 清除所有副图面板
|
|
1145
|
-
*/
|
|
1146
407
|
clearSubPanes() {
|
|
1147
|
-
|
|
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.
|
|
411
|
+
return this.indicatorManager.getSubPaneIndicators();
|
|
1166
412
|
}
|
|
1167
|
-
/**
|
|
1168
|
-
* 获取所有副图条目
|
|
1169
|
-
*/
|
|
1170
413
|
getSubPaneEntries() {
|
|
1171
|
-
return this.
|
|
414
|
+
return this.indicatorManager.getSubPaneEntries();
|
|
1172
415
|
}
|
|
1173
|
-
/**
|
|
1174
|
-
* 根据 paneId 获取副图条目
|
|
1175
|
-
* @param paneId 副图实例标识符
|
|
1176
|
-
*/
|
|
1177
416
|
getSubPaneEntry(paneId) {
|
|
1178
|
-
return this.
|
|
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.
|
|
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.
|
|
520
|
+
return this.dataManager.getData();
|
|
1316
521
|
}
|
|
1317
522
|
/** 获取指标调度器(供外部控制器更新指标配置) */
|
|
1318
523
|
getIndicatorScheduler() {
|
|
1319
|
-
return this.
|
|
1320
|
-
}
|
|
1321
|
-
getTrailingSlotCount() {
|
|
1322
|
-
return 24;
|
|
524
|
+
return this.indicatorManager.indicatorSchedulerAccessor;
|
|
1323
525
|
}
|
|
1324
526
|
getLogicalSlotCount() {
|
|
1325
|
-
return this.
|
|
527
|
+
return this.dataManager.getLogicalSlotCount();
|
|
1326
528
|
}
|
|
1327
529
|
getTimestampAtLogicalIndex(index) {
|
|
1328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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.
|
|
1375
|
-
this.layoutPanes();
|
|
1376
|
-
this.
|
|
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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
604
|
+
return this.viewportManager.viewportSignal;
|
|
1793
605
|
}
|
|
1794
606
|
/** 数据信号 */
|
|
1795
607
|
get data() {
|
|
1796
|
-
return this.
|
|
608
|
+
return this.dataManager.data;
|
|
1797
609
|
}
|
|
1798
610
|
/** 符号信号 */
|
|
1799
611
|
get symbols() {
|
|
1800
|
-
return this.
|
|
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.
|
|
628
|
+
return this.indicatorManager.indicatorsComputed;
|
|
1809
629
|
}
|
|
1810
630
|
/** 子图信息信号(派生信号,自动随副图条目/比例更新) */
|
|
1811
631
|
get subPanes() {
|
|
1812
|
-
return this.
|
|
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.
|
|
655
|
+
this.dataManager.setData(data);
|
|
1840
656
|
}
|
|
1841
|
-
/**
|
|
1842
|
-
* 追加数据(高层 API)
|
|
1843
|
-
* 合并现有数据并更新
|
|
1844
|
-
*/
|
|
1845
657
|
appendData(newData) {
|
|
1846
|
-
|
|
1847
|
-
this.setData(merged);
|
|
658
|
+
this.dataManager.appendData(newData);
|
|
1848
659
|
}
|
|
1849
|
-
/**
|
|
1850
|
-
* 设置数据获取器适配器
|
|
1851
|
-
*/
|
|
1852
660
|
setDataFetcher(fetcher) {
|
|
1853
|
-
this.
|
|
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.
|
|
664
|
+
return this.dataManager.dataBuffer;
|
|
1861
665
|
}
|
|
1862
666
|
checkVisibleRangeGap() {
|
|
1863
|
-
|
|
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
|
-
|
|
1941
|
-
this.
|
|
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
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
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.
|
|
698
|
+
this.zoomController.zoomIn(anchorX);
|
|
2056
699
|
}
|
|
2057
700
|
/**
|
|
2058
701
|
* 缩小(高层 API)
|
|
2059
702
|
*/
|
|
2060
703
|
zoomOut(anchorX) {
|
|
2061
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
800
|
+
* 更新 viewport signal(用于滚动事件)
|
|
2202
801
|
*/
|
|
2203
802
|
updateViewportSignal() {
|
|
2204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|