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