@363045841yyt/klinechart-core 0.8.1-alpha.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +82 -48
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +4 -5
- 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 +102 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +590 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +438 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +39 -10
- package/src/controllers/types.ts +6 -1
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +64 -23
- package/src/data-fetchers/tradingview.ts +4 -5
- 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 +691 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +579 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- package/src/engine/renderers/comparisonLine.ts +25 -11
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/plugin/types.ts +1 -0
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -626
package/src/engine/chart.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import type { KLineData } from '../types/price'
|
|
2
2
|
import type { ChartSettings } from '../config/chartSettings'
|
|
3
|
-
import { createSignal,
|
|
4
|
-
import type { SymbolSpec
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { createSignal, type Signal, type Computed } from '../reactivity/signal'
|
|
4
|
+
import type { SymbolSpec } from '../controllers/types'
|
|
5
|
+
import { ChartDataManager } from './data/chartDataManager'
|
|
6
|
+
import { ChartPaneLayout } from './layout/chartPaneLayout'
|
|
7
|
+
import { UpdateLevel } from './layout/pane'
|
|
8
8
|
import type { ScaleType } from './utils/tickPosition'
|
|
9
9
|
import { InteractionController, type InteractionSnapshot } from './controller/interaction'
|
|
10
10
|
export type { InteractionSnapshot }
|
|
11
|
+
import type { ChartDom, PaneSpec, ChartOptions, Viewport, ViewportState, IndicatorInstance, SubPaneInfo, DrawingToolType } from './chartTypes'
|
|
11
12
|
import { PaneRenderer } from './paneRenderer'
|
|
12
13
|
import { SharedWebGLSurface } from './renderers/webgl/sharedWebGLSurface'
|
|
13
|
-
import { MarkerManager,
|
|
14
|
-
import { getPhysicalKLineConfig
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
14
|
+
import type { MarkerManager, CustomMarkerEntity } from './marker/registry'
|
|
15
|
+
import { getPhysicalKLineConfig } from './utils/klineConfig'
|
|
16
|
+
import { ChartZoomController } from './utils/chartZoomController'
|
|
17
|
+
import { ChartViewportManager } from './viewport/chartViewportManager'
|
|
18
|
+
import { ChartIndicatorManager } from './indicators/chartIndicatorManager'
|
|
19
|
+
import type { IndicatorScheduler } from './indicators/scheduler'
|
|
20
|
+
import type { SubPaneEntry } from './subPaneManager'
|
|
21
|
+
import { ChartRenderer } from './render/chartRenderer'
|
|
20
22
|
|
|
21
23
|
import {
|
|
22
24
|
createPluginHost,
|
|
@@ -24,159 +26,30 @@ import {
|
|
|
24
26
|
RendererPluginManager,
|
|
25
27
|
type RendererPlugin,
|
|
26
28
|
type RendererPluginWithHost,
|
|
27
|
-
type RenderContext,
|
|
28
29
|
wrapPaneInfo,
|
|
29
|
-
type PaneRole,
|
|
30
|
-
type PaneCapabilities,
|
|
31
|
-
type YAxisLabel,
|
|
32
|
-
type XAxisLabel,
|
|
33
|
-
type YAxisRange,
|
|
34
|
-
type XAxisRange,
|
|
35
30
|
} from '../plugin'
|
|
36
|
-
import {
|
|
37
|
-
import { createMainIndicatorLegendRendererPlugin } from './renderers/Indicator/mainIndicatorLegend'
|
|
38
|
-
import { DrawingStore } from './drawing'
|
|
39
|
-
import { createDrawingRendererPlugin, createDrawingLabelOverlayPlugin } from './drawing/plugin'
|
|
40
|
-
import { createGridLinesRendererPlugin } from './renderers/gridLines'
|
|
41
|
-
import { createCandleRenderer } from './renderers/candle'
|
|
42
|
-
import { createComparisonLineRenderer } from './renderers/comparisonLine'
|
|
43
|
-
import { createLastPriceLineRendererPlugin, createLastPriceLabelRegistrarPlugin } from './renderers/lastPrice'
|
|
44
|
-
import { createCustomMarkersRenderer } from './renderers/customMarkers'
|
|
45
|
-
import { createExtremaMarkersRendererPlugin } from './renderers/extremaMarkers'
|
|
46
|
-
import { createYAxisRendererPlugin } from './renderers/yAxis'
|
|
47
|
-
import { createCrosshairRendererPlugin } from './renderers/crosshair'
|
|
48
|
-
import { createTimeAxisRendererPlugin } from './renderers/timeAxis'
|
|
31
|
+
import type { SubIndicatorType } from './renderers/Indicator'
|
|
49
32
|
|
|
50
33
|
|
|
51
34
|
// 重新导出以保持向后兼容
|
|
52
|
-
export { getPhysicalKLineConfig
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 图表 DOM 元素引用
|
|
56
|
-
* @property container 图表容器 div
|
|
57
|
-
* @property canvasLayer Canvas 层容器 div(包含所有绘制 canvas)
|
|
58
|
-
*/
|
|
59
|
-
/**
|
|
60
|
-
* 图表 DOM 元素引用
|
|
61
|
-
* @property container 图表容器 div
|
|
62
|
-
* @property canvasLayer Canvas 层容器 div(包含所有绘制 canvas)
|
|
63
|
-
* @property xAxisCanvas X 轴时间轴 canvas
|
|
64
|
-
*/
|
|
65
|
-
export type ChartDom = {
|
|
66
|
-
container: HTMLDivElement
|
|
67
|
-
scrollContent?: HTMLDivElement
|
|
68
|
-
canvasLayer: HTMLDivElement
|
|
69
|
-
rightAxisLayer: HTMLDivElement
|
|
70
|
-
xAxisCanvas: HTMLCanvasElement
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Pane 面板配置
|
|
75
|
-
* @property id Pane 标识符
|
|
76
|
-
* @property ratio Pane 高度占比
|
|
77
|
-
* @property visible 是否可见(默认 true)
|
|
78
|
-
*/
|
|
79
|
-
export type PaneSpec = {
|
|
80
|
-
id: string
|
|
81
|
-
ratio: number
|
|
82
|
-
visible?: boolean
|
|
83
|
-
minHeightPx?: number
|
|
84
|
-
role?: PaneRole
|
|
85
|
-
capabilities?: Partial<PaneCapabilities>
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export type PaneRendererDom = {
|
|
89
|
-
mainCanvas: HTMLCanvasElement
|
|
90
|
-
overlayCanvas: HTMLCanvasElement
|
|
91
|
-
yAxisCanvas: HTMLCanvasElement
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export type ChartOptions = {
|
|
95
|
-
/** K 线宽度(可选,由 zoomLevel 派生) */
|
|
96
|
-
kWidth?: number
|
|
97
|
-
/** K 线间隙(可选,由 DPR 计算) */
|
|
98
|
-
kGap?: number
|
|
99
|
-
yPaddingPx: number
|
|
100
|
-
rightAxisWidth: number
|
|
101
|
-
bottomAxisHeight: number
|
|
102
|
-
minKWidth: number
|
|
103
|
-
maxKWidth: number
|
|
104
|
-
panes: PaneSpec[]
|
|
105
|
-
|
|
106
|
-
/** pane 之间的真实分隔空隙(逻辑像素) */
|
|
107
|
-
paneGap?: number
|
|
108
|
-
|
|
109
|
-
/** 价格标签额外宽度(用于显示涨跌幅,默认 60px) */
|
|
110
|
-
priceLabelWidth?: number
|
|
111
|
-
|
|
112
|
-
/** pane 最小高度(逻辑像素,默认 60) */
|
|
113
|
-
defaultPaneMinHeightPx?: number
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 缩放级别数量(默认 10)
|
|
117
|
-
* - 将 minKWidth ~ maxKWidth 划分为多少个离散级别
|
|
118
|
-
* - 例如 10 表示有 10 个缩放级别(1-10)
|
|
119
|
-
*/
|
|
120
|
-
zoomLevels?: number
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 初始缩放级别(1 ~ zoomLevels,默认 1)
|
|
124
|
-
* 未指定时默认为最小级别
|
|
125
|
-
*/
|
|
126
|
-
initialZoomLevel?: number
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** K 线起始 x 坐标数组,positions[i] 表示第 i 根 K 线的起始 x 坐标(逻辑像素) */
|
|
130
|
-
export type KLinePositions = number[]
|
|
131
|
-
|
|
132
|
-
export type Viewport = {
|
|
133
|
-
viewWidth: number
|
|
134
|
-
viewHeight: number
|
|
135
|
-
plotWidth: number
|
|
136
|
-
plotHeight: number
|
|
137
|
-
scrollLeft: number
|
|
138
|
-
dpr: number
|
|
139
|
-
}
|
|
35
|
+
export { getPhysicalKLineConfig }
|
|
36
|
+
export type { ChartDom, PaneSpec, PaneRendererDom, ChartOptions, KLinePositions, Viewport, ViewportState, IndicatorRole, IndicatorInstance, SubPaneInfo, DrawingToolType, DrawingObject } from './chartTypes'
|
|
140
37
|
|
|
141
38
|
type ResolvedChartOptions = Omit<ChartOptions, 'kWidth' | 'kGap'> & {
|
|
142
39
|
kWidth: number
|
|
143
40
|
kGap: number
|
|
144
41
|
}
|
|
145
42
|
|
|
146
|
-
type FrameData = {
|
|
147
|
-
vp: Viewport
|
|
148
|
-
range: VisibleRange
|
|
149
|
-
kLinePositions: KLinePositions
|
|
150
|
-
kLineCenters: number[]
|
|
151
|
-
kBarRects: Array<{ x: number; width: number }>
|
|
152
|
-
kWidthPx: number
|
|
153
|
-
useCachedFrame: boolean
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 主图指标条目,存在 = 激活 */
|
|
157
|
-
interface MainIndicatorEntry {
|
|
158
|
-
params: Record<string, number | boolean | string>
|
|
159
|
-
}
|
|
160
|
-
|
|
161
43
|
export class Chart {
|
|
162
44
|
private dom: ChartDom
|
|
163
45
|
private opt: ResolvedChartOptions
|
|
164
|
-
private
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
private
|
|
168
|
-
private
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
private _comparisonBufferUnsubs: Map<string, () => void> = new Map()
|
|
172
|
-
|
|
173
|
-
private raf: number | null = null
|
|
174
|
-
private pendingUpdateLevel: UpdateLevel = UpdateLevel.All
|
|
175
|
-
private _internalViewport: Viewport | null = null
|
|
176
|
-
|
|
177
|
-
private paneRenderers: PaneRenderer[] = []
|
|
178
|
-
private markerManager: MarkerManager
|
|
179
|
-
private drawingStore = new DrawingStore()
|
|
46
|
+
private dataManager: ChartDataManager
|
|
47
|
+
|
|
48
|
+
private viewportManager: ChartViewportManager
|
|
49
|
+
private layoutManager: ChartPaneLayout
|
|
50
|
+
private get paneRenderers(): PaneRenderer[] {
|
|
51
|
+
return this.layoutManager.getPaneRenderers()
|
|
52
|
+
}
|
|
180
53
|
readonly interaction: InteractionController
|
|
181
54
|
|
|
182
55
|
/** 插件宿主 */
|
|
@@ -185,182 +58,17 @@ export class Chart {
|
|
|
185
58
|
/** 渲染器插件管理器 */
|
|
186
59
|
private rendererPluginManager: RendererPluginManager
|
|
187
60
|
|
|
188
|
-
/** 精确 DPR(来自 ResizeObserver 的 devicePixelContentBoxSize) */
|
|
189
|
-
private preciseDpr = 0
|
|
190
|
-
|
|
191
|
-
/** 统一监听容器尺寸与 DPR 变化 */
|
|
192
|
-
private resizeObserver?: ResizeObserver
|
|
193
|
-
|
|
194
|
-
/** scroll 事件处理器引用(用于 cleanup) */
|
|
195
|
-
private onScroll?: () => void
|
|
196
|
-
|
|
197
|
-
/** 最近一次观测到的容器尺寸 */
|
|
198
|
-
private observedSize = { width: 0, height: 0 }
|
|
199
|
-
|
|
200
|
-
/** 缓存的 scrollLeft(通过 scroll 事件同步,避免每帧读取 DOM 触发强制回流) */
|
|
201
|
-
private cachedScrollLeft = 0
|
|
202
|
-
|
|
203
|
-
/** 左侧加载缓冲宽度;DOM scrollLeft 不能为负,用它映射出逻辑负滚动 */
|
|
204
|
-
private leftLoadBufferWidth = 0
|
|
205
|
-
|
|
206
|
-
/** 待写入 DOM 的 scrollLeft(在 RAF 回调中应用,确保 Vue 已完成 DOM 更新) */
|
|
207
|
-
private _pendingScrollLeft: number | null = null
|
|
208
|
-
|
|
209
|
-
private incrementalLoadHintEl: HTMLDivElement | null = null
|
|
210
|
-
private incrementalLoadHintTimer: number | null = null
|
|
211
|
-
private pendingPrependedCount = 0
|
|
212
|
-
|
|
213
|
-
/** overlay 上一帧是否有十字线(用于判断何时需要清除) */
|
|
214
|
-
private overlayHadCrosshair = false
|
|
215
|
-
|
|
216
|
-
/** 用户设置配置(传递给渲染器) */
|
|
217
|
-
private settings: ChartSettings = {}
|
|
218
|
-
|
|
219
|
-
/** pane ratio 状态(按 paneId 维护,sum=1 仅对可见 pane) */
|
|
220
|
-
private _internalPaneRatios: Map<string, number> = new Map()
|
|
221
|
-
|
|
222
|
-
/** 共享 X 轴上下文缓存 */
|
|
223
|
-
private xAxisCtx: CanvasRenderingContext2D | null = null
|
|
224
|
-
|
|
225
61
|
/** Chart 级共享 WebGL canvas/context */
|
|
226
62
|
private sharedWebGLSurface: SharedWebGLSurface
|
|
227
63
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private ensureIncrementalLoadHint(): HTMLDivElement | null {
|
|
233
|
-
const host = this.getScrollContentHost()
|
|
234
|
-
if (!host) return null
|
|
235
|
-
|
|
236
|
-
if (this.incrementalLoadHintEl && this.incrementalLoadHintEl.isConnected) {
|
|
237
|
-
return this.incrementalLoadHintEl
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const ownerDoc = host.ownerDocument
|
|
241
|
-
if (!ownerDoc) return null
|
|
242
|
-
|
|
243
|
-
const hint = ownerDoc.createElement('div')
|
|
244
|
-
hint.className = 'klc-incremental-load-hint'
|
|
245
|
-
hint.style.position = 'absolute'
|
|
246
|
-
hint.style.left = '0'
|
|
247
|
-
hint.style.top = '0'
|
|
248
|
-
hint.style.height = '0px'
|
|
249
|
-
hint.style.width = '0px'
|
|
250
|
-
hint.style.pointerEvents = 'none'
|
|
251
|
-
hint.style.opacity = '0'
|
|
252
|
-
hint.style.filter = 'blur(10px)'
|
|
253
|
-
hint.style.transition = 'opacity 420ms ease, filter 420ms ease'
|
|
254
|
-
hint.style.background = 'rgba(71, 91, 132, 0.5)'
|
|
255
|
-
hint.style.zIndex = '3'
|
|
256
|
-
hint.style.willChange = 'opacity, filter, width'
|
|
257
|
-
host.appendChild(hint)
|
|
258
|
-
this.incrementalLoadHintEl = hint
|
|
259
|
-
return hint
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private clearIncrementalLoadHintTimer(): void {
|
|
263
|
-
if (this.incrementalLoadHintTimer !== null) {
|
|
264
|
-
window.clearTimeout(this.incrementalLoadHintTimer)
|
|
265
|
-
this.incrementalLoadHintTimer = null
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
private hideIncrementalLoadHint(): void {
|
|
270
|
-
const hint = this.incrementalLoadHintEl
|
|
271
|
-
if (!hint) return
|
|
272
|
-
hint.style.opacity = '0'
|
|
273
|
-
hint.style.filter = 'blur(10px)'
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private showIncrementalLoadHint(count: number): void {
|
|
277
|
-
if (count <= 0) return
|
|
278
|
-
const hint = this.ensureIncrementalLoadHint()
|
|
279
|
-
if (!hint) return
|
|
280
|
-
|
|
281
|
-
this.clearIncrementalLoadHintTimer()
|
|
282
|
-
|
|
283
|
-
const dpr = this.getEffectiveDpr()
|
|
284
|
-
const { unitPx, startXPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
|
|
285
|
-
const width = this.getLeftLoadBufferWidth() + (startXPx + count * unitPx) / dpr
|
|
286
|
-
hint.style.width = `${Math.max(0, width)}px`
|
|
287
|
-
hint.style.height = `${Math.max(
|
|
288
|
-
0,
|
|
289
|
-
this._internalViewport?.viewHeight ?? this.dom.container?.clientHeight ?? 0,
|
|
290
|
-
)}px`
|
|
291
|
-
|
|
292
|
-
hint.getBoundingClientRect()
|
|
293
|
-
hint.style.opacity = '1'
|
|
294
|
-
hint.style.filter = 'blur(0px)'
|
|
64
|
+
/** 缩放控制器 */
|
|
65
|
+
private zoomController: ChartZoomController
|
|
295
66
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.incrementalLoadHintTimer = null
|
|
299
|
-
}, 900)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** 当前缩放级别(1 ~ zoomLevelCount) */
|
|
303
|
-
private currentZoomLevel: number = 1
|
|
67
|
+
/** 指标管理器 */
|
|
68
|
+
private indicatorManager: ChartIndicatorManager
|
|
304
69
|
|
|
305
|
-
/**
|
|
306
|
-
private
|
|
307
|
-
|
|
308
|
-
/** 指标调度器(负责计算 MA 等指标并写入 StateStore)
|
|
309
|
-
* TODO: 阶段5迁移为插件注册,Scheduler 通过事件监听 data/viewport 变更,Chart 不直接持有
|
|
310
|
-
*/
|
|
311
|
-
private indicatorScheduler: IndicatorScheduler
|
|
312
|
-
|
|
313
|
-
/** 数据已更新但 Worker 指标尚未回写,期间避免用旧指标 state 绘制中间帧 */
|
|
314
|
-
private pendingIndicatorDataUpdate = false
|
|
315
|
-
|
|
316
|
-
/** 上次可见范围(用于检测视口变化) */
|
|
317
|
-
private lastVisibleRange: VisibleRange = { start: 0, end: 0 }
|
|
318
|
-
|
|
319
|
-
/** 原始可见范围可为负数,仅用于判断左侧空白区加载 */
|
|
320
|
-
private lastRawVisibleRange: VisibleRange = { start: 0, end: 0 }
|
|
321
|
-
|
|
322
|
-
/** Overlay 帧复用的最近主渲染结果 */
|
|
323
|
-
private cachedDrawFrame: {
|
|
324
|
-
viewport: Viewport
|
|
325
|
-
range: VisibleRange
|
|
326
|
-
kLinePositions: KLinePositions
|
|
327
|
-
kLineCenters: number[]
|
|
328
|
-
kBarRects: Array<{ x: number; width: number }>
|
|
329
|
-
kWidthPx: number
|
|
330
|
-
} | null = null
|
|
331
|
-
|
|
332
|
-
/** 副图管理器 */
|
|
333
|
-
private subPaneManager = new SubPaneManager()
|
|
334
|
-
|
|
335
|
-
/** 主图指标激活状态与参数(存在即激活,默认参数在 enable 时初始化) */
|
|
336
|
-
private _mainIndicatorsSignal: Signal<Map<string, MainIndicatorEntry>> = createSignal<Map<string, MainIndicatorEntry>>(new Map())
|
|
337
|
-
|
|
338
|
-
/** 主图指标默认参数(从注册表中懒加载) */
|
|
339
|
-
private static _defaultMainParamsCache: Record<string, Record<string, number | boolean | string>> | null = null
|
|
340
|
-
|
|
341
|
-
private static get DEFAULT_MAIN_PARAMS(): Record<string, Record<string, number | boolean | string>> {
|
|
342
|
-
if (Chart._defaultMainParamsCache === null) {
|
|
343
|
-
Chart._defaultMainParamsCache = {}
|
|
344
|
-
for (const def of getRegisteredIndicatorDefinitions()) {
|
|
345
|
-
if (def.category === 'main') {
|
|
346
|
-
Chart._defaultMainParamsCache[def.displayName.toUpperCase()] = (def.runtime?.defaultConfig ?? {}) as Record<string, number | boolean | string>
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return Chart._defaultMainParamsCache
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** 可启用的主图指标白名单(从注册表中懒加载) */
|
|
354
|
-
private static _enableMainIndicatorsCache: string[] | null = null
|
|
355
|
-
|
|
356
|
-
private static get ENABLE_MAIN_INDICATORS(): string[] {
|
|
357
|
-
if (Chart._enableMainIndicatorsCache === null) {
|
|
358
|
-
Chart._enableMainIndicatorsCache = getRegisteredIndicatorDefinitions()
|
|
359
|
-
.filter(d => d.category === 'main')
|
|
360
|
-
.map(d => d.displayName.toUpperCase())
|
|
361
|
-
}
|
|
362
|
-
return Chart._enableMainIndicatorsCache
|
|
363
|
-
}
|
|
70
|
+
/** 渲染器 */
|
|
71
|
+
private renderer: ChartRenderer
|
|
364
72
|
|
|
365
73
|
/**
|
|
366
74
|
* 启用主图指标
|
|
@@ -369,216 +77,42 @@ export class Chart {
|
|
|
369
77
|
* @returns 是否成功启用
|
|
370
78
|
*/
|
|
371
79
|
enableMainIndicator(indicatorId: string, params?: Record<string, number | boolean | string>): boolean {
|
|
372
|
-
|
|
373
|
-
if (!Chart.ENABLE_MAIN_INDICATORS.includes(id)) {
|
|
374
|
-
console.warn(`[Chart] 未知的主图指标: ${indicatorId}`)
|
|
375
|
-
return false
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const map = this._mainIndicatorsSignal.peek()
|
|
379
|
-
const existing = map.get(id)
|
|
380
|
-
|
|
381
|
-
if (existing) {
|
|
382
|
-
// 已启用,更新参数
|
|
383
|
-
if (params) {
|
|
384
|
-
const next = new Map(map)
|
|
385
|
-
next.set(id, { params: { ...existing.params, ...params } })
|
|
386
|
-
this._mainIndicatorsSignal.set(next)
|
|
387
|
-
this.updateIndicatorSchedulerConfig(id)
|
|
388
|
-
}
|
|
389
|
-
return true
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// 合并默认参数和传入参数
|
|
393
|
-
const defaults = Chart.DEFAULT_MAIN_PARAMS[id] ?? {}
|
|
394
|
-
const merged = params ? { ...defaults, ...params } : defaults
|
|
395
|
-
const next = new Map(map)
|
|
396
|
-
next.set(id, { params: merged })
|
|
397
|
-
this._mainIndicatorsSignal.set(next)
|
|
398
|
-
|
|
399
|
-
// 启用对应的渲染器
|
|
400
|
-
this.enableMainIndicatorRenderer(id)
|
|
401
|
-
|
|
402
|
-
// 更新调度器配置(触发异步重算)
|
|
403
|
-
this.updateIndicatorSchedulerConfig(id)
|
|
404
|
-
|
|
405
|
-
// 同步重算主图状态:latestResult 已有该指标的 series,只是没注册到 registry
|
|
406
|
-
// 补调 updateVisibleRange 使其走 updateVisibleStatesOnly,立即从 latestResult 合成极值
|
|
407
|
-
this.indicatorScheduler.updateVisibleRange(this.lastVisibleRange)
|
|
408
|
-
|
|
409
|
-
this.scheduleDraw()
|
|
410
|
-
return true
|
|
80
|
+
return this.indicatorManager.enableMainIndicator(indicatorId, params)
|
|
411
81
|
}
|
|
412
82
|
|
|
413
|
-
/**
|
|
414
|
-
* 禁用主图指标
|
|
415
|
-
* @param indicatorId 指标ID
|
|
416
|
-
* @returns 是否成功禁用
|
|
417
|
-
*/
|
|
418
83
|
disableMainIndicator(indicatorId: string): boolean {
|
|
419
|
-
|
|
420
|
-
const map = this._mainIndicatorsSignal.peek()
|
|
421
|
-
if (!map.has(id)) return false
|
|
422
|
-
|
|
423
|
-
const next = new Map(map)
|
|
424
|
-
next.delete(id)
|
|
425
|
-
this._mainIndicatorsSignal.set(next)
|
|
426
|
-
|
|
427
|
-
// 禁用对应的渲染器
|
|
428
|
-
this.disableMainIndicatorRenderer(id)
|
|
429
|
-
|
|
430
|
-
// 更新调度器配置
|
|
431
|
-
this.updateIndicatorSchedulerConfig(id)
|
|
432
|
-
|
|
433
|
-
this.scheduleDraw()
|
|
434
|
-
return true
|
|
84
|
+
return this.indicatorManager.disableMainIndicator(indicatorId)
|
|
435
85
|
}
|
|
436
86
|
|
|
437
|
-
/**
|
|
438
|
-
* 切换主图指标启用状态
|
|
439
|
-
* @param indicatorId 指标ID
|
|
440
|
-
* @param enabled 是否启用
|
|
441
|
-
*/
|
|
442
87
|
toggleMainIndicator(indicatorId: string, enabled: boolean): void {
|
|
443
|
-
|
|
444
|
-
this.enableMainIndicator(indicatorId)
|
|
445
|
-
} else {
|
|
446
|
-
this.disableMainIndicator(indicatorId)
|
|
447
|
-
}
|
|
88
|
+
this.indicatorManager.toggleMainIndicator(indicatorId, enabled)
|
|
448
89
|
}
|
|
449
90
|
|
|
450
|
-
/**
|
|
451
|
-
* 获取当前激活的主图指标列表
|
|
452
|
-
* @returns 激活的指标ID数组
|
|
453
|
-
*/
|
|
454
91
|
getActiveMainIndicators(): string[] {
|
|
455
|
-
return
|
|
92
|
+
return this.indicatorManager.getActiveMainIndicators()
|
|
456
93
|
}
|
|
457
94
|
|
|
458
|
-
/**
|
|
459
|
-
* 检查主图指标是否激活
|
|
460
|
-
* @param indicatorId 指标ID
|
|
461
|
-
*/
|
|
462
95
|
isMainIndicatorActive(indicatorId: string): boolean {
|
|
463
|
-
return this.
|
|
96
|
+
return this.indicatorManager.isMainIndicatorActive(indicatorId)
|
|
464
97
|
}
|
|
465
98
|
|
|
466
|
-
/**
|
|
467
|
-
* 更新主图指标参数
|
|
468
|
-
* @param indicatorId 指标ID
|
|
469
|
-
* @param params 参数对象
|
|
470
|
-
*/
|
|
471
99
|
updateMainIndicatorParams(indicatorId: string, params: Record<string, number | boolean | string>): void {
|
|
472
|
-
|
|
473
|
-
const map = this._mainIndicatorsSignal.peek()
|
|
474
|
-
const entry = map.get(id)
|
|
475
|
-
if (!entry) return
|
|
476
|
-
|
|
477
|
-
const merged = { ...entry.params, ...params }
|
|
478
|
-
const next = new Map(map)
|
|
479
|
-
next.set(id, { params: merged })
|
|
480
|
-
this._mainIndicatorsSignal.set(next)
|
|
481
|
-
|
|
482
|
-
// 同步更新渲染器配置
|
|
483
|
-
const rendererName = id.toLowerCase()
|
|
484
|
-
const renderer = this.getRenderer(rendererName)
|
|
485
|
-
if (renderer && renderer.setConfig) {
|
|
486
|
-
renderer.setConfig(merged)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 更新调度器
|
|
490
|
-
this.updateIndicatorSchedulerConfig(id)
|
|
491
|
-
this.scheduleDraw()
|
|
100
|
+
this.indicatorManager.updateMainIndicatorParams(indicatorId, params)
|
|
492
101
|
}
|
|
493
102
|
|
|
494
|
-
/**
|
|
495
|
-
* 获取主图指标参数
|
|
496
|
-
* @param indicatorId 指标ID
|
|
497
|
-
*/
|
|
498
103
|
getMainIndicatorParams(indicatorId: string): Record<string, number | boolean | string> | null {
|
|
499
|
-
return this.
|
|
104
|
+
return this.indicatorManager.getMainIndicatorParams(indicatorId)
|
|
500
105
|
}
|
|
501
106
|
|
|
502
|
-
/**
|
|
503
|
-
* 清除所有主图指标
|
|
504
|
-
*/
|
|
505
107
|
clearMainIndicators(): void {
|
|
506
|
-
|
|
507
|
-
for (const id of map.keys()) {
|
|
508
|
-
this.disableMainIndicatorRenderer(id)
|
|
509
|
-
}
|
|
510
|
-
this._mainIndicatorsSignal.set(new Map())
|
|
511
|
-
this.scheduleDraw()
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* 启用主图指标渲染器(内部方法)
|
|
516
|
-
*/
|
|
517
|
-
private enableMainIndicatorRenderer(indicatorId: string): void {
|
|
518
|
-
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
519
|
-
const mainPane = definition?.mainPane
|
|
520
|
-
if (!definition || !mainPane) return
|
|
521
|
-
|
|
522
|
-
if (!this.getRenderer(mainPane.rendererName)) {
|
|
523
|
-
this.useRenderer(definition.rendererFactory({ paneId: 'main', indicatorId }))
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
this.setRendererEnabled(mainPane.rendererName, true)
|
|
527
|
-
|
|
528
|
-
if (!this.getRenderer('mainIndicatorLegend')) {
|
|
529
|
-
this.useRenderer(createMainIndicatorLegendRendererPlugin({ yPaddingPx: this.opt.yPaddingPx }))
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* 禁用主图指标渲染器(内部方法)
|
|
535
|
-
*/
|
|
536
|
-
private disableMainIndicatorRenderer(indicatorId: string): void {
|
|
537
|
-
const rendererName = this.indicatorScheduler.getIndicatorMetadata(indicatorId)?.mainPane?.rendererName
|
|
538
|
-
if (rendererName) {
|
|
539
|
-
this.setRendererEnabled(rendererName, false)
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* 更新调度器配置(内部方法)
|
|
545
|
-
*/
|
|
546
|
-
private updateIndicatorSchedulerConfig(indicatorId: string): void {
|
|
547
|
-
const entry = this._mainIndicatorsSignal.peek().get(indicatorId)
|
|
548
|
-
const isActive = entry !== undefined
|
|
549
|
-
const params = entry?.params ?? {}
|
|
550
|
-
|
|
551
|
-
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
552
|
-
const toActiveConfig = definition?.mainPane?.toActiveConfig
|
|
553
|
-
if (!definition?.updateConfig || !toActiveConfig) return
|
|
554
|
-
|
|
555
|
-
const config = toActiveConfig(params, isActive)
|
|
556
|
-
if (config !== null) {
|
|
557
|
-
definition.updateConfig(this.indicatorScheduler, config, 'main')
|
|
558
|
-
}
|
|
108
|
+
this.indicatorManager.clearMainIndicators()
|
|
559
109
|
}
|
|
560
110
|
|
|
561
111
|
/**
|
|
562
112
|
* @deprecated 使用 enableMainIndicator/disableMainIndicator 替代
|
|
563
113
|
*/
|
|
564
114
|
setActiveMainIndicators(indicators: string[]): void {
|
|
565
|
-
|
|
566
|
-
const newSet = new Set(indicators.map(i => i.toUpperCase()))
|
|
567
|
-
const currentSet = new Set(this._mainIndicatorsSignal.peek().keys())
|
|
568
|
-
|
|
569
|
-
// 禁用不再激活的
|
|
570
|
-
for (const id of currentSet) {
|
|
571
|
-
if (!newSet.has(id)) {
|
|
572
|
-
this.disableMainIndicator(id)
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// 启用新激活的
|
|
577
|
-
for (const id of newSet) {
|
|
578
|
-
if (!currentSet.has(id)) {
|
|
579
|
-
this.enableMainIndicator(id)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
115
|
+
this.indicatorManager.setActiveMainIndicators(indicators)
|
|
582
116
|
}
|
|
583
117
|
|
|
584
118
|
/**
|
|
@@ -595,7 +129,6 @@ export class Chart {
|
|
|
595
129
|
this.interaction.setOnInteractionChange((snapshot) => {
|
|
596
130
|
this._interactionSignal.set(snapshot)
|
|
597
131
|
})
|
|
598
|
-
this.markerManager = new MarkerManager()
|
|
599
132
|
this.pluginHost = createPluginHost()
|
|
600
133
|
this.rendererPluginManager = new RendererPluginManager()
|
|
601
134
|
this.sharedWebGLSurface = new SharedWebGLSurface()
|
|
@@ -604,189 +137,140 @@ export class Chart {
|
|
|
604
137
|
this.rendererPluginManager.setPluginHost(this.pluginHost)
|
|
605
138
|
this.rendererPluginManager.setInvalidateCallback(() => this.scheduleDraw())
|
|
606
139
|
|
|
607
|
-
this.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
for (const definition of getBuiltinIndicatorDefinitions()) {
|
|
619
|
-
this.indicatorScheduler.registerIndicator(definition)
|
|
620
|
-
}
|
|
621
|
-
this.indicatorScheduler.setInvalidateCallback(() => {
|
|
622
|
-
this.pendingIndicatorDataUpdate = false
|
|
623
|
-
this.scheduleDraw()
|
|
140
|
+
this.viewportManager = new ChartViewportManager({
|
|
141
|
+
getDom: () => this.dom,
|
|
142
|
+
getBottomAxisHeight: () => this.opt.bottomAxisHeight,
|
|
143
|
+
getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
|
|
144
|
+
getZoomLevel: () => this.zoomController.currentZoomLevel,
|
|
145
|
+
getLastVisibleRange: () => this.dataManager.lastVisibleRange,
|
|
146
|
+
getKWidth: () => this.opt.kWidth,
|
|
147
|
+
getKGap: () => this.opt.kGap,
|
|
148
|
+
scheduleDraw: (level) => this.scheduleDraw(level),
|
|
149
|
+
onResizeCompleted: () => { this.resize() },
|
|
150
|
+
resizeSharedWebGLSurface: (plotWidth, plotHeight, dpr) => this.sharedWebGLSurface.resize(plotWidth, plotHeight, dpr),
|
|
624
151
|
})
|
|
625
152
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
() =>
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if ((import.meta as any).env?.MODE !== 'production') {
|
|
635
|
-
this._indicatorsComputed.subscribe(() => {
|
|
636
|
-
const instances = this._indicatorsComputed.peek()
|
|
637
|
-
console.log('[Chart] indicators signal changed:', instances)
|
|
638
|
-
})
|
|
639
|
-
this._subPanesComputed.subscribe(() => {
|
|
640
|
-
const subPanes = this._subPanesComputed.peek()
|
|
641
|
-
console.log('[Chart] subPanes signal changed:', subPanes)
|
|
642
|
-
})
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// 注册绘图主插件(负责绘制 shape,layer: 'main')
|
|
646
|
-
this.useRenderer(createDrawingRendererPlugin({ store: this.drawingStore }))
|
|
647
|
-
// 注册绘图标签插件(负责推送选中绘图的轴标签,layer: 'overlay')
|
|
648
|
-
// 注意:此插件依赖 overlay 更新级别,若将来添加 Main 级别需调整
|
|
649
|
-
this.useRenderer(createDrawingLabelOverlayPlugin({ store: this.drawingStore }))
|
|
650
|
-
this.initCoreRenderers()
|
|
651
|
-
this.initResizeObserver()
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
private initCoreRenderers(): void {
|
|
656
|
-
const axisWidth = this.opt.rightAxisWidth + (this.opt.priceLabelWidth ?? 0)
|
|
657
|
-
|
|
658
|
-
this.useRenderer(createGridLinesRendererPlugin())
|
|
659
|
-
this.useRenderer(createCandleRenderer())
|
|
660
|
-
this.useRenderer(createComparisonLineRenderer())
|
|
661
|
-
this.useRenderer(createLastPriceLineRendererPlugin())
|
|
662
|
-
this.useRenderer(createLastPriceLabelRegistrarPlugin())
|
|
663
|
-
this.useRenderer(createCustomMarkersRenderer())
|
|
664
|
-
this.useRenderer(createExtremaMarkersRendererPlugin())
|
|
665
|
-
this.useRenderer(createMainIndicatorLegendRendererPlugin({
|
|
666
|
-
yPaddingPx: this.opt.yPaddingPx,
|
|
667
|
-
}))
|
|
668
|
-
this.useRenderer(createYAxisRendererPlugin({
|
|
669
|
-
axisWidth,
|
|
670
|
-
yPaddingPx: this.opt.yPaddingPx,
|
|
671
|
-
getCrosshair: () => {
|
|
672
|
-
const pos = this.interaction.crosshairPos
|
|
673
|
-
const price = this.interaction.crosshairPrice
|
|
674
|
-
const activePaneId = this.interaction.activePaneId
|
|
675
|
-
if (pos && price !== null) {
|
|
676
|
-
return { y: pos.y, price, activePaneId }
|
|
677
|
-
}
|
|
678
|
-
return null
|
|
679
|
-
},
|
|
680
|
-
}))
|
|
681
|
-
this.useRenderer(createCrosshairRendererPlugin({
|
|
682
|
-
getCrosshairState: () => ({
|
|
683
|
-
pos: this.interaction.crosshairPos,
|
|
684
|
-
activePaneId: this.interaction.activePaneId,
|
|
685
|
-
isDragging: this.interaction.isDraggingState(),
|
|
686
|
-
price: this.interaction.crosshairPrice,
|
|
153
|
+
this.layoutManager = new ChartPaneLayout(this.opt.panes, {
|
|
154
|
+
getDom: () => this.dom,
|
|
155
|
+
getOption: () => ({
|
|
156
|
+
rightAxisWidth: this.opt.rightAxisWidth,
|
|
157
|
+
yPaddingPx: this.opt.yPaddingPx,
|
|
158
|
+
priceLabelWidth: this.opt.priceLabelWidth,
|
|
159
|
+
paneGap: this.opt.paneGap,
|
|
160
|
+
defaultPaneMinHeightPx: this.opt.defaultPaneMinHeightPx,
|
|
687
161
|
}),
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
return null
|
|
162
|
+
getViewport: () => this.viewportManager.getViewport(),
|
|
163
|
+
getSharedWebGLSurface: () => this.sharedWebGLSurface,
|
|
164
|
+
setKnownPaneIds: (ids) => this.rendererPluginManager.setKnownPaneIds(ids),
|
|
165
|
+
notifyPaneResize: (paneId, pane) => this.rendererPluginManager.notifyResize(paneId, wrapPaneInfo(pane)),
|
|
166
|
+
scheduleDraw: (level) => this.scheduleDraw(level),
|
|
167
|
+
onLayoutChange: (ratios, specs) => {
|
|
168
|
+
this._paneRatiosSignal.set(ratios)
|
|
169
|
+
this._paneLayoutSignal.set(specs)
|
|
170
|
+
this.opt = { ...this.opt, panes: specs }
|
|
698
171
|
},
|
|
699
|
-
}))
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
private initResizeObserver() {
|
|
704
|
-
if (typeof ResizeObserver === 'undefined') return
|
|
705
|
-
|
|
706
|
-
const target = this.dom.container
|
|
707
|
-
if (!target) return
|
|
708
|
-
|
|
709
|
-
// 初始化 scrollLeft 缓存
|
|
710
|
-
this.cachedScrollLeft = target.scrollLeft
|
|
711
|
-
this.onScroll = () => { this.cachedScrollLeft = target.scrollLeft }
|
|
712
|
-
target.addEventListener('scroll', this.onScroll, { passive: true })
|
|
713
|
-
|
|
714
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
715
|
-
const entry = entries[0]
|
|
716
|
-
if (!entry) return
|
|
717
|
-
|
|
718
|
-
const prevWidth = this.observedSize.width
|
|
719
|
-
const prevHeight = this.observedSize.height
|
|
720
|
-
const prevDpr = this.preciseDpr
|
|
721
|
-
|
|
722
|
-
this.updateObservedMetrics(entry)
|
|
723
|
-
|
|
724
|
-
const widthChanged = this.observedSize.width !== prevWidth
|
|
725
|
-
const heightChanged = this.observedSize.height !== prevHeight
|
|
726
|
-
const dprChanged = this.preciseDpr !== prevDpr
|
|
727
|
-
if ((import.meta as any).env?.MODE !== 'production') {
|
|
728
|
-
console.log(
|
|
729
|
-
`[Chart] resize observer: ` +
|
|
730
|
-
`size ${prevWidth}x${prevHeight} -> ${this.observedSize.width}x${this.observedSize.height} ` +
|
|
731
|
-
`dpr ${prevDpr} -> ${this.preciseDpr} ` +
|
|
732
|
-
`changed: ${widthChanged || heightChanged ? 'size' : ''}${widthChanged || heightChanged && dprChanged ? '+' : ''}${dprChanged ? 'dpr' : ''}`
|
|
733
|
-
)
|
|
734
|
-
}
|
|
735
|
-
if (widthChanged || heightChanged || dprChanged) {
|
|
736
|
-
this.resize()
|
|
737
|
-
}
|
|
738
172
|
})
|
|
739
173
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
this.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
174
|
+
this.dataManager = new ChartDataManager({
|
|
175
|
+
getOption: () => this.opt,
|
|
176
|
+
getEffectiveDpr: () => this.viewportManager.getEffectiveDpr(),
|
|
177
|
+
getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
|
|
178
|
+
getCachedScrollLeft: () => this.viewportManager.getCachedScrollLeft(),
|
|
179
|
+
setCachedScrollLeft: (v) => { this.viewportManager.setCachedScrollLeft(v) },
|
|
180
|
+
setPendingScrollLeft: (v) => { this.viewportManager.setPendingScrollLeft(v) },
|
|
181
|
+
getDom: () => this.dom,
|
|
182
|
+
getObservedSize: () => this.viewportManager.getObservedSize(),
|
|
183
|
+
getViewport: () => this.viewportManager.getViewport(),
|
|
184
|
+
scheduleDraw: (level) => this.scheduleDraw(level),
|
|
185
|
+
resetInteraction: () => this.interaction.reset(),
|
|
186
|
+
getIndicatorScheduler: () => this.indicatorManager.indicatorSchedulerAccessor,
|
|
187
|
+
setPendingIndicatorDataUpdate: (v) => { this.dataManager.pendingIndicatorDataUpdate = v },
|
|
188
|
+
isPointerDown: () => this.interaction.isPointerDown(),
|
|
189
|
+
})
|
|
748
190
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
191
|
+
this.zoomController = new ChartZoomController({
|
|
192
|
+
getLogicalScrollLeft: () => this.viewportManager.getLogicalScrollLeft(),
|
|
193
|
+
getCurrentDpr: () => this.viewportManager.getEffectiveDpr(),
|
|
194
|
+
getLeftLoadBufferWidth: () => this.dataManager.getLeftLoadBufferWidth(),
|
|
195
|
+
setScrollLeft: (v) => { this.viewportManager.setScrollLeft(v) },
|
|
196
|
+
onZoomCommitted: (result) => {
|
|
197
|
+
this.opt = { ...this.opt, kWidth: result.kWidth, kGap: result.kGap }
|
|
198
|
+
this.updateViewportSignal()
|
|
199
|
+
this.scheduleDraw()
|
|
200
|
+
},
|
|
201
|
+
getKWidth: () => this.opt.kWidth,
|
|
202
|
+
getKGap: () => this.opt.kGap,
|
|
203
|
+
getMinKWidth: () => this.opt.minKWidth,
|
|
204
|
+
getMaxKWidth: () => this.opt.maxKWidth,
|
|
205
|
+
zoomLevelCount: Math.max(2, Math.round(this.opt.zoomLevels ?? 20)),
|
|
206
|
+
initialZoomLevel: this.opt.initialZoomLevel ?? 1,
|
|
207
|
+
})
|
|
208
|
+
// 注意:初始 kWidth/kGap 应由外部通过 applyRenderState() 传入
|
|
754
209
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
this.
|
|
759
|
-
|
|
760
|
-
|
|
210
|
+
// 初始化指标管理器
|
|
211
|
+
this.indicatorManager = new ChartIndicatorManager({
|
|
212
|
+
getOption: () => this.opt,
|
|
213
|
+
getPluginHost: () => this.pluginHost,
|
|
214
|
+
getRenderer: (name) => this.getRenderer(name),
|
|
215
|
+
useRenderer: (plugin, config) => this.useRenderer(plugin, config),
|
|
216
|
+
removeRenderer: (name) => this.removeRenderer(name),
|
|
217
|
+
updateRendererConfig: (name, config) => this.updateRendererConfig(name, config),
|
|
218
|
+
setRendererEnabled: (name, enabled) => this.setRendererEnabled(name, enabled),
|
|
219
|
+
hasPane: (paneId) => this.layoutManager.hasPane(paneId),
|
|
220
|
+
upsertPane: (def) => this.layoutManager.upsertPane(def),
|
|
221
|
+
removePaneDefinition: (paneId) => this.layoutManager.removePaneDefinition(paneId),
|
|
222
|
+
getPaneSpecs: () => this.layoutManager.getPaneSpecs(),
|
|
223
|
+
getPaneRatiosSignal: () => this._paneRatiosSignal,
|
|
224
|
+
getInternalPaneRatios: () => this.layoutManager.getInternalPaneRatios(),
|
|
225
|
+
setInternalPaneRatio: (paneId, ratio) => this.layoutManager.setInternalPaneRatio(paneId, ratio),
|
|
226
|
+
deleteInternalPaneRatio: (paneId) => this.layoutManager.deleteInternalPaneRatio(paneId),
|
|
227
|
+
applyPaneLayoutSpecs: (specs) => this.layoutManager.applyPaneLayoutSpecs(specs),
|
|
228
|
+
getLastVisibleRange: () => this.dataManager.lastVisibleRange,
|
|
229
|
+
getCrosshairPos: () => this.interaction.crosshairPos,
|
|
230
|
+
getCrosshairPrice: () => this.interaction.crosshairPrice,
|
|
231
|
+
getActivePaneId: () => this.interaction.activePaneId,
|
|
232
|
+
scheduleDraw: (level) => this.scheduleDraw(level),
|
|
233
|
+
setPendingIndicatorDataUpdate: (v) => { this.dataManager.pendingIndicatorDataUpdate = v },
|
|
234
|
+
})
|
|
761
235
|
|
|
762
|
-
|
|
763
|
-
this.
|
|
236
|
+
// 初始化渲染器
|
|
237
|
+
this.renderer = new ChartRenderer({
|
|
238
|
+
getDom: () => this.dom,
|
|
239
|
+
getOption: () => this.opt,
|
|
240
|
+
getPaneRenderers: () => this.paneRenderers,
|
|
241
|
+
getInteraction: () => this.interaction,
|
|
242
|
+
getSharedWebGLSurface: () => this.sharedWebGLSurface,
|
|
243
|
+
getPluginHost: () => this.pluginHost,
|
|
244
|
+
getRendererPluginManager: () => this.rendererPluginManager,
|
|
245
|
+
getTheme: () => this._themeSignal.peek(),
|
|
246
|
+
getCurrentZoomLevel: () => this.zoomController.currentZoomLevel,
|
|
247
|
+
getZoomLevelCount: () => this.zoomController.zoomLevelCount,
|
|
248
|
+
getViewportManager: () => this.viewportManager,
|
|
249
|
+
getDataManager: () => this.dataManager,
|
|
250
|
+
getIndicatorManager: () => this.indicatorManager,
|
|
251
|
+
})
|
|
252
|
+
this.renderer.registerDrawingPlugins()
|
|
253
|
+
this.renderer.initCoreRenderers()
|
|
254
|
+
this.viewportManager.init()
|
|
764
255
|
}
|
|
765
256
|
|
|
766
|
-
private getEffectiveDpr(): number {
|
|
767
|
-
let dpr = this.preciseDpr > 0
|
|
768
|
-
? this.preciseDpr
|
|
769
|
-
: Math.round((window.devicePixelRatio || 1) * 64) / 64
|
|
770
|
-
if (dpr < 1) dpr = 1
|
|
771
|
-
return dpr
|
|
772
|
-
}
|
|
773
257
|
|
|
774
258
|
getViewport(): Viewport | null {
|
|
775
|
-
return this.
|
|
259
|
+
return this.viewportManager.getViewport()
|
|
776
260
|
}
|
|
777
261
|
|
|
778
262
|
getCurrentDpr(): number {
|
|
779
|
-
return this.getEffectiveDpr()
|
|
263
|
+
return this.viewportManager.getEffectiveDpr()
|
|
780
264
|
}
|
|
781
265
|
|
|
782
266
|
/** 获取缓存的 scrollLeft(避免读取 DOM 触发强制回流) */
|
|
783
267
|
getCachedScrollLeft(): number {
|
|
784
|
-
return this.
|
|
268
|
+
return this.viewportManager.getCachedScrollLeft()
|
|
785
269
|
}
|
|
786
270
|
|
|
787
271
|
/** 获取逻辑 scrollLeft(减去左侧加载缓冲宽度,可为负值) */
|
|
788
272
|
getLogicalScrollLeft(): number {
|
|
789
|
-
return this.
|
|
273
|
+
return this.viewportManager.getLogicalScrollLeft()
|
|
790
274
|
}
|
|
791
275
|
|
|
792
276
|
/** 获取插件宿主 */
|
|
@@ -831,15 +315,20 @@ export class Chart {
|
|
|
831
315
|
|
|
832
316
|
/** 更新用户设置(触发重绘) */
|
|
833
317
|
updateSettings(settings: ChartSettings): void {
|
|
834
|
-
this.settings
|
|
318
|
+
this.renderer.updateSettings(settings)
|
|
835
319
|
this.interaction.updateSettings(settings)
|
|
836
320
|
|
|
837
321
|
// 同步刻度类型设置到所有 pane(百分比仅用于主图)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
322
|
+
if ('axisType' in settings) {
|
|
323
|
+
const axisType = (settings.axisType as ScaleType) ?? 'linear'
|
|
324
|
+
const currentType = this.paneRenderers[0]?.getPane().yAxis.getScaleType()
|
|
325
|
+
if (axisType !== currentType) {
|
|
326
|
+
for (const renderer of this.paneRenderers) {
|
|
327
|
+
const pane = renderer.getPane()
|
|
328
|
+
const scaleType = axisType === 'percent' && pane.role !== 'price' ? 'linear' : axisType
|
|
329
|
+
pane.yAxis.setScaleType(scaleType)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
843
332
|
}
|
|
844
333
|
|
|
845
334
|
this.scheduleDraw()
|
|
@@ -850,321 +339,10 @@ export class Chart {
|
|
|
850
339
|
* @param level 更新级别,决定渲染哪些层
|
|
851
340
|
*/
|
|
852
341
|
draw(level: UpdateLevel = UpdateLevel.All) {
|
|
853
|
-
|
|
854
|
-
this.markerManager.clear()
|
|
855
|
-
|
|
856
|
-
// 2. 准备帧数据(视口 / 可见范围 / K 线坐标,优先走缓存)
|
|
857
|
-
const frame = this.prepareFrameData(level)
|
|
858
|
-
if (!frame) {
|
|
859
|
-
if (this._internalData.length === 0) this.clearAllCanvases()
|
|
860
|
-
return
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame } = frame
|
|
864
|
-
|
|
865
|
-
// 3. 更新交互控制器坐标映射
|
|
866
|
-
this.interaction.setKLinePositions(kLinePositions, range, kWidthPx)
|
|
867
|
-
|
|
868
|
-
// 4. 通知调度器当前活跃主图指标 + 获取价格范围
|
|
869
|
-
this.indicatorScheduler.setActiveMainIndicators(
|
|
870
|
-
[...this._mainIndicatorsSignal.peek().entries()].map(([id, entry]) => ({ id, params: entry.params })),
|
|
871
|
-
)
|
|
872
|
-
const mainIndicatorRange = useCachedFrame ? null : this.indicatorScheduler.getMainIndicatorPriceRange()
|
|
873
|
-
const hasCrosshair = this.interaction.getCrosshairIndex() !== null
|
|
874
|
-
|
|
875
|
-
// 5. 遍历所有 Pane 渲染主层 / overlay / Y 轴
|
|
876
|
-
const { sharedXAxisLabels, sharedXAxisRanges } = this.renderPanes(
|
|
877
|
-
vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx,
|
|
878
|
-
mainIndicatorRange, hasCrosshair, useCachedFrame, level,
|
|
879
|
-
)
|
|
880
|
-
|
|
881
|
-
// 6. 持久化十字线状态供下帧判断清除
|
|
882
|
-
this.overlayHadCrosshair = hasCrosshair
|
|
883
|
-
|
|
884
|
-
// 7. 渲染 X 轴时间轴
|
|
885
|
-
this.renderXAxis(vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, sharedXAxisLabels, sharedXAxisRanges)
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
private prepareFrameData(level: UpdateLevel): FrameData | null {
|
|
889
|
-
const useCachedFrame = level === UpdateLevel.Overlay && this.cachedDrawFrame !== null
|
|
890
|
-
|
|
891
|
-
const vp = useCachedFrame ? this.cachedDrawFrame!.viewport : this.computeViewport()
|
|
892
|
-
if (!vp) return null
|
|
893
|
-
|
|
894
|
-
if (this._internalData.length === 0) return null
|
|
895
|
-
|
|
896
|
-
const rawRange = useCachedFrame
|
|
897
|
-
? this.cachedDrawFrame!.range
|
|
898
|
-
: (() => {
|
|
899
|
-
const { start, end } = getVisibleRange(
|
|
900
|
-
vp.scrollLeft,
|
|
901
|
-
vp.plotWidth,
|
|
902
|
-
this.opt.kWidth,
|
|
903
|
-
this.opt.kGap,
|
|
904
|
-
this._internalData.length,
|
|
905
|
-
vp.dpr
|
|
906
|
-
)
|
|
907
|
-
return { start, end }
|
|
908
|
-
})()
|
|
909
|
-
const range = { start: Math.max(0, rawRange.start), end: rawRange.end }
|
|
910
|
-
|
|
911
|
-
if (!useCachedFrame && (
|
|
912
|
-
range.start !== this.lastVisibleRange.start
|
|
913
|
-
|| range.end !== this.lastVisibleRange.end
|
|
914
|
-
|| rawRange.start !== this.lastRawVisibleRange.start
|
|
915
|
-
|| rawRange.end !== this.lastRawVisibleRange.end
|
|
916
|
-
)) {
|
|
917
|
-
this.indicatorScheduler.updateVisibleRange(range)
|
|
918
|
-
this.lastVisibleRange = range
|
|
919
|
-
this.lastRawVisibleRange = rawRange
|
|
920
|
-
this.checkVisibleRangeGapWhenIdle()
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
const kLinePositions = useCachedFrame
|
|
924
|
-
? this.cachedDrawFrame!.kLinePositions
|
|
925
|
-
: this.calcKLinePositions(range)
|
|
926
|
-
|
|
927
|
-
let kLineCenters: number[]
|
|
928
|
-
let kBarRects: Array<{ x: number; width: number }>
|
|
929
|
-
let kWidthPx: number
|
|
930
|
-
|
|
931
|
-
if (useCachedFrame) {
|
|
932
|
-
kLineCenters = this.cachedDrawFrame!.kLineCenters
|
|
933
|
-
kBarRects = this.cachedDrawFrame!.kBarRects
|
|
934
|
-
kWidthPx = this.cachedDrawFrame!.kWidthPx
|
|
935
|
-
} else {
|
|
936
|
-
const physConfig = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, vp.dpr)
|
|
937
|
-
let barWidthPx = Math.max(1, physConfig.unitPx - 1)
|
|
938
|
-
if (barWidthPx % 2 === 0) barWidthPx -= 1
|
|
939
|
-
|
|
940
|
-
kLineCenters = new Array(kLinePositions.length)
|
|
941
|
-
kBarRects = new Array(kLinePositions.length)
|
|
942
|
-
|
|
943
|
-
for (let i = 0; i < kLinePositions.length; i++) {
|
|
944
|
-
const x = kLinePositions[i]!
|
|
945
|
-
const leftPx = Math.round(x * vp.dpr)
|
|
946
|
-
const wickXPx = leftPx + (physConfig.kWidthPx - 1) / 2
|
|
947
|
-
kLineCenters[i] = wickXPx / vp.dpr
|
|
948
|
-
|
|
949
|
-
const barLeftPx = wickXPx - (barWidthPx - 1) / 2
|
|
950
|
-
kBarRects[i] = { x: barLeftPx / vp.dpr, width: barWidthPx / vp.dpr }
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
kWidthPx = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, vp.dpr).kWidthPx
|
|
954
|
-
this.cachedDrawFrame = {
|
|
955
|
-
viewport: { ...vp },
|
|
956
|
-
range: { ...range },
|
|
957
|
-
kLinePositions,
|
|
958
|
-
kLineCenters,
|
|
959
|
-
kBarRects,
|
|
960
|
-
kWidthPx,
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
return { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame }
|
|
342
|
+
this.renderer.draw(level)
|
|
965
343
|
}
|
|
966
344
|
|
|
967
|
-
private clearAllCanvases() {
|
|
968
|
-
const vp = this.computeViewport()
|
|
969
|
-
if (!vp) return
|
|
970
|
-
for (const r of this.paneRenderers) {
|
|
971
|
-
const { mainCtx, overlayCtx, yAxisCtx } = r.getContexts()
|
|
972
|
-
const pane = r.getPane()
|
|
973
|
-
mainCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
|
|
974
|
-
overlayCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
|
|
975
|
-
yAxisCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
|
|
976
|
-
}
|
|
977
|
-
const xCtx = this.xAxisCtx
|
|
978
|
-
if (xCtx) {
|
|
979
|
-
const xW = xCtx.canvas.width
|
|
980
|
-
const xH = xCtx.canvas.height
|
|
981
|
-
xCtx.clearRect(0, 0, xW, xH)
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
345
|
|
|
985
|
-
private renderPanes(
|
|
986
|
-
vp: Viewport,
|
|
987
|
-
range: VisibleRange,
|
|
988
|
-
kLinePositions: KLinePositions,
|
|
989
|
-
kLineCenters: number[],
|
|
990
|
-
kBarRects: Array<{ x: number; width: number }>,
|
|
991
|
-
kWidthPx: number,
|
|
992
|
-
mainIndicatorRange: { min: number; max: number } | null,
|
|
993
|
-
hasCrosshair: boolean,
|
|
994
|
-
useCachedFrame: boolean,
|
|
995
|
-
level: UpdateLevel,
|
|
996
|
-
): { sharedXAxisLabels: XAxisLabel[]; sharedXAxisRanges: XAxisRange[] } {
|
|
997
|
-
const sharedYAxisLabels: YAxisLabel[] = []
|
|
998
|
-
const sharedXAxisLabels: XAxisLabel[] = []
|
|
999
|
-
const sharedYAxisRanges: YAxisRange[] = []
|
|
1000
|
-
const sharedXAxisRanges: XAxisRange[] = []
|
|
1001
|
-
|
|
1002
|
-
for (const renderer of this.paneRenderers) {
|
|
1003
|
-
const pane = renderer.getPane()
|
|
1004
|
-
const { mainCtx, overlayCtx, yAxisCtx } = renderer.getContexts()
|
|
1005
|
-
const { candleSurface, lineSurface } = renderer.getWebGL()
|
|
1006
|
-
|
|
1007
|
-
if (!useCachedFrame) {
|
|
1008
|
-
const indicatorRange = pane.role === 'price' ? mainIndicatorRange : null
|
|
1009
|
-
const comparisonRange = pane.id === 'main' ? this.getComparisonEquivalentPriceRange(range) : null
|
|
1010
|
-
const mergedRange = this.mergeNumericRanges(indicatorRange, comparisonRange)
|
|
1011
|
-
pane.updateRange(this._internalData, range, mergedRange)
|
|
1012
|
-
if (pane.id === 'main' && this.settings.disableMainPaneVerticalScroll) {
|
|
1013
|
-
pane.yAxis.resetTransform()
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
const shouldUpdateMain = level === UpdateLevel.Main || level === UpdateLevel.All
|
|
1018
|
-
const shouldUpdateOverlay = level === UpdateLevel.All || (level === UpdateLevel.Overlay && (hasCrosshair || this.overlayHadCrosshair))
|
|
1019
|
-
|
|
1020
|
-
if (shouldUpdateMain && mainCtx) {
|
|
1021
|
-
mainCtx.setTransform(1, 0, 0, 1, 0, 0)
|
|
1022
|
-
mainCtx.scale(vp.dpr, vp.dpr)
|
|
1023
|
-
mainCtx.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
|
|
1024
|
-
candleSurface?.clear()
|
|
1025
|
-
lineSurface?.clear()
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (shouldUpdateOverlay && overlayCtx) {
|
|
1029
|
-
const overlayWidth = overlayCtx.canvas.width / vp.dpr
|
|
1030
|
-
overlayCtx.setTransform(1, 0, 0, 1, 0, 0)
|
|
1031
|
-
overlayCtx.scale(vp.dpr, vp.dpr)
|
|
1032
|
-
overlayCtx.clearRect(0, 0, overlayWidth + 1, pane.height + 2 / vp.dpr)
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (yAxisCtx && !useCachedFrame) {
|
|
1036
|
-
const yAxisWidth = yAxisCtx.canvas.width / vp.dpr
|
|
1037
|
-
yAxisCtx.setTransform(1, 0, 0, 1, 0, 0)
|
|
1038
|
-
yAxisCtx.scale(vp.dpr, vp.dpr)
|
|
1039
|
-
yAxisCtx.clearRect(0, 0, yAxisWidth, pane.height + 2 / vp.dpr)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const context: RenderContext = {
|
|
1043
|
-
ctx: mainCtx!,
|
|
1044
|
-
overlayCtx: overlayCtx ?? undefined,
|
|
1045
|
-
pane: wrapPaneInfo(pane),
|
|
1046
|
-
data: this._internalData,
|
|
1047
|
-
comparisonData: this._comparisonData,
|
|
1048
|
-
comparisonSymbols: this._comparisonSpecs,
|
|
1049
|
-
range,
|
|
1050
|
-
scrollLeft: vp.scrollLeft,
|
|
1051
|
-
kWidth: this.opt.kWidth,
|
|
1052
|
-
kGap: this.opt.kGap,
|
|
1053
|
-
dpr: vp.dpr,
|
|
1054
|
-
paneWidth: vp.plotWidth,
|
|
1055
|
-
kLinePositions,
|
|
1056
|
-
kLineCenters,
|
|
1057
|
-
kBarRects,
|
|
1058
|
-
markerManager: this.markerManager,
|
|
1059
|
-
crosshairIndex: this.interaction.getCrosshairIndex(),
|
|
1060
|
-
yAxisCtx: yAxisCtx ?? undefined,
|
|
1061
|
-
candleWebGLSurface: candleSurface ?? undefined,
|
|
1062
|
-
lineWebGLSurface: lineSurface ?? undefined,
|
|
1063
|
-
zoomLevel: this.currentZoomLevel,
|
|
1064
|
-
zoomLevelCount: this.zoomLevelCount,
|
|
1065
|
-
viewport: {
|
|
1066
|
-
scrollLeft: vp.scrollLeft,
|
|
1067
|
-
plotWidth: vp.plotWidth,
|
|
1068
|
-
plotHeight: vp.plotHeight,
|
|
1069
|
-
},
|
|
1070
|
-
settings: this.settings,
|
|
1071
|
-
yAxisLabels: sharedYAxisLabels,
|
|
1072
|
-
xAxisLabels: sharedXAxisLabels,
|
|
1073
|
-
yAxisRanges: sharedYAxisRanges,
|
|
1074
|
-
xAxisRanges: sharedXAxisRanges,
|
|
1075
|
-
theme: this._themeSignal.peek(),
|
|
1076
|
-
isAsiaMarket: this.settings.isAsiaMarket as boolean,
|
|
1077
|
-
colorPresetSettings: this.settings.colorPresetSettings,
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (shouldUpdateMain || shouldUpdateOverlay) {
|
|
1081
|
-
const errors = this.rendererPluginManager.render(pane.id, context, level)
|
|
1082
|
-
if (errors.length > 0) {
|
|
1083
|
-
this.pluginHost.events.emit('renderer:error', { paneId: pane.id, errors })
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
const yAxisErrors = this.rendererPluginManager.renderPlugin('yAxis', context)
|
|
1087
|
-
if (yAxisErrors.length > 0) {
|
|
1088
|
-
this.pluginHost.events.emit('renderer:error', { paneId: pane.id, errors: yAxisErrors })
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
return { sharedXAxisLabels, sharedXAxisRanges }
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
private renderXAxis(
|
|
1097
|
-
vp: Viewport,
|
|
1098
|
-
range: VisibleRange,
|
|
1099
|
-
kLinePositions: KLinePositions,
|
|
1100
|
-
kLineCenters: number[],
|
|
1101
|
-
kBarRects: Array<{ x: number; width: number }>,
|
|
1102
|
-
kWidthPx: number,
|
|
1103
|
-
sharedXAxisLabels: XAxisLabel[],
|
|
1104
|
-
sharedXAxisRanges: XAxisRange[],
|
|
1105
|
-
): void {
|
|
1106
|
-
const xAxisCtx = this.xAxisCtx ?? this.dom.xAxisCanvas.getContext('2d')
|
|
1107
|
-
if (!this.xAxisCtx) {
|
|
1108
|
-
this.xAxisCtx = xAxisCtx
|
|
1109
|
-
}
|
|
1110
|
-
if (xAxisCtx) {
|
|
1111
|
-
const timeAxisContext: RenderContext = {
|
|
1112
|
-
ctx: xAxisCtx,
|
|
1113
|
-
pane: {
|
|
1114
|
-
id: 'xAxis',
|
|
1115
|
-
role: 'auxiliary',
|
|
1116
|
-
capabilities: {
|
|
1117
|
-
showPriceAxisTicks: false,
|
|
1118
|
-
showCrosshairPriceLabel: false,
|
|
1119
|
-
candleHitTest: false,
|
|
1120
|
-
supportsPriceTranslate: false,
|
|
1121
|
-
},
|
|
1122
|
-
top: 0,
|
|
1123
|
-
height: this.opt.bottomAxisHeight,
|
|
1124
|
-
yAxis: {
|
|
1125
|
-
priceToY: () => 0,
|
|
1126
|
-
yToPrice: () => 0,
|
|
1127
|
-
getPaddingTop: () => 0,
|
|
1128
|
-
getPaddingBottom: () => 0,
|
|
1129
|
-
getPriceOffset: () => 0,
|
|
1130
|
-
getDisplayRange: (baseRange) => baseRange ?? { maxPrice: 0, minPrice: 0 },
|
|
1131
|
-
getScaleType: () => 'linear' as const,
|
|
1132
|
-
getBasePrice: () => null,
|
|
1133
|
-
toPercent: () => 0,
|
|
1134
|
-
fromPercent: () => 0,
|
|
1135
|
-
getDisplayPercentRange: () => ({ minPct: 0, maxPct: 0 }),
|
|
1136
|
-
},
|
|
1137
|
-
priceRange: { maxPrice: 0, minPrice: 0 },
|
|
1138
|
-
},
|
|
1139
|
-
data: this._internalData,
|
|
1140
|
-
range,
|
|
1141
|
-
scrollLeft: vp.scrollLeft,
|
|
1142
|
-
kWidth: this.opt.kWidth,
|
|
1143
|
-
kGap: this.opt.kGap,
|
|
1144
|
-
dpr: vp.dpr,
|
|
1145
|
-
paneWidth: vp.plotWidth,
|
|
1146
|
-
kLinePositions,
|
|
1147
|
-
kLineCenters,
|
|
1148
|
-
kBarRects,
|
|
1149
|
-
xAxisCtx,
|
|
1150
|
-
viewport: {
|
|
1151
|
-
scrollLeft: vp.scrollLeft,
|
|
1152
|
-
plotWidth: vp.plotWidth,
|
|
1153
|
-
plotHeight: vp.plotHeight,
|
|
1154
|
-
},
|
|
1155
|
-
yAxisLabels: [],
|
|
1156
|
-
xAxisLabels: sharedXAxisLabels,
|
|
1157
|
-
xAxisRanges: sharedXAxisRanges,
|
|
1158
|
-
theme: this._themeSignal.peek(),
|
|
1159
|
-
isAsiaMarket: this.settings.isAsiaMarket as boolean,
|
|
1160
|
-
colorPresetSettings: this.settings.colorPresetSettings,
|
|
1161
|
-
}
|
|
1162
|
-
const errors = this.rendererPluginManager.renderPlugin('timeAxis', timeAxisContext)
|
|
1163
|
-
if (errors.length > 0) {
|
|
1164
|
-
this.pluginHost.events.emit('renderer:error', { paneId: 'timeAxis', errors })
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
346
|
|
|
1169
347
|
// ========== Render State API (Vue SSOT) ==========
|
|
1170
348
|
|
|
@@ -1175,11 +353,11 @@ export class Chart {
|
|
|
1175
353
|
*/
|
|
1176
354
|
applyRenderState(kWidth: number, kGap: number, zoomLevel?: number): void {
|
|
1177
355
|
const nextZoomLevel = zoomLevel !== undefined
|
|
1178
|
-
? Math.max(1, Math.min(this.zoomLevelCount, zoomLevel))
|
|
1179
|
-
: this.currentZoomLevel
|
|
356
|
+
? Math.max(1, Math.min(this.zoomController.zoomLevelCount, zoomLevel))
|
|
357
|
+
: this.zoomController.currentZoomLevel
|
|
1180
358
|
const renderStateChanged = this.opt.kWidth !== kWidth
|
|
1181
359
|
|| this.opt.kGap !== kGap
|
|
1182
|
-
|| this.currentZoomLevel !== nextZoomLevel
|
|
360
|
+
|| this.zoomController.currentZoomLevel !== nextZoomLevel
|
|
1183
361
|
|
|
1184
362
|
if (!renderStateChanged) {
|
|
1185
363
|
return
|
|
@@ -1187,7 +365,7 @@ export class Chart {
|
|
|
1187
365
|
|
|
1188
366
|
this.opt = { ...this.opt, kWidth, kGap }
|
|
1189
367
|
if (zoomLevel !== undefined) {
|
|
1190
|
-
this.
|
|
368
|
+
this.zoomController.setZoomLevel(nextZoomLevel)
|
|
1191
369
|
}
|
|
1192
370
|
this.updateViewportSignal()
|
|
1193
371
|
this.scheduleDraw()
|
|
@@ -1195,7 +373,7 @@ export class Chart {
|
|
|
1195
373
|
|
|
1196
374
|
/** 获取总缩放级别数 */
|
|
1197
375
|
getZoomLevelCount(): number {
|
|
1198
|
-
return this.zoomLevelCount
|
|
376
|
+
return this.zoomController.zoomLevelCount
|
|
1199
377
|
}
|
|
1200
378
|
|
|
1201
379
|
/** 获取所有 PaneRenderer */
|
|
@@ -1205,18 +383,18 @@ export class Chart {
|
|
|
1205
383
|
|
|
1206
384
|
/** 获取 MarkerManager(供 InteractionController 使用) */
|
|
1207
385
|
getMarkerManager(): MarkerManager {
|
|
1208
|
-
return this.
|
|
386
|
+
return this.renderer.getMarkerManager()
|
|
1209
387
|
}
|
|
1210
388
|
|
|
1211
389
|
/** 更新自定义标记 */
|
|
1212
390
|
updateCustomMarkers(markers: CustomMarkerEntity[]): void {
|
|
1213
|
-
this.
|
|
391
|
+
this.renderer.getMarkerManager().setCustomMarkers(markers)
|
|
1214
392
|
this.scheduleDraw()
|
|
1215
393
|
}
|
|
1216
394
|
|
|
1217
395
|
/** 清除自定义标记 */
|
|
1218
396
|
clearCustomMarkers(): void {
|
|
1219
|
-
this.
|
|
397
|
+
this.renderer.getMarkerManager().clearCustomMarkers()
|
|
1220
398
|
this.scheduleDraw()
|
|
1221
399
|
}
|
|
1222
400
|
|
|
@@ -1230,36 +408,6 @@ export class Chart {
|
|
|
1230
408
|
return this.opt
|
|
1231
409
|
}
|
|
1232
410
|
|
|
1233
|
-
/**
|
|
1234
|
-
* 计算 K 线起始 x 坐标数组,与 candle.ts 的像素对齐方式保持一致
|
|
1235
|
-
* @param range 可见 K 线索引范围
|
|
1236
|
-
* @returns x 坐标数组(逻辑像素,经过物理像素对齐)
|
|
1237
|
-
*/
|
|
1238
|
-
calcKLinePositions(range: VisibleRange): KLinePositions {
|
|
1239
|
-
const { start, end } = range
|
|
1240
|
-
const count = end - start
|
|
1241
|
-
|
|
1242
|
-
// 边界检查:防止负数或零长度数组
|
|
1243
|
-
if (count <= 0) {
|
|
1244
|
-
return []
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
const dpr = this.getEffectiveDpr()
|
|
1248
|
-
|
|
1249
|
-
// 统一使用 getPhysicalKLineConfig,确保与渲染完全一致
|
|
1250
|
-
const { unitPx, startXPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
|
|
1251
|
-
|
|
1252
|
-
const positions: number[] = new Array(count)
|
|
1253
|
-
|
|
1254
|
-
for (let i = 0; i < count; i++) {
|
|
1255
|
-
const dataIndex = start + i
|
|
1256
|
-
const leftPx = startXPx + dataIndex * unitPx
|
|
1257
|
-
positions[i] = leftPx / dpr
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
return positions
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
411
|
/**
|
|
1264
412
|
* 更新配置并触发布局/重绘
|
|
1265
413
|
* @param partial 部分配置项
|
|
@@ -1277,7 +425,7 @@ export class Chart {
|
|
|
1277
425
|
if (partial.panes) {
|
|
1278
426
|
const nextPanes = partial.panes.map((pane) => ({ ...pane }))
|
|
1279
427
|
this.opt = { ...this.opt, ...partial, panes: nextPanes }
|
|
1280
|
-
this.applyPaneLayoutSpecs(nextPanes)
|
|
428
|
+
this.layoutManager.applyPaneLayoutSpecs(nextPanes)
|
|
1281
429
|
return
|
|
1282
430
|
}
|
|
1283
431
|
|
|
@@ -1285,347 +433,93 @@ export class Chart {
|
|
|
1285
433
|
this.resize()
|
|
1286
434
|
}
|
|
1287
435
|
|
|
1288
|
-
/** 更新 pane 布局配置
|
|
1289
|
-
* @param panes 新的 pane 配置数组
|
|
1290
|
-
*
|
|
1291
|
-
* 显式整盘替换:清空之前 user-resize 留下的 paneRatios 缓存,让 spec 中的 ratio
|
|
1292
|
-
* 真正生效。`addPane`/`upsertPane`/`removePaneDefinition` 走 `applyPaneLayoutSpecs`
|
|
1293
|
-
* 时仍保留 prev 值以记住用户拖拽过的高度——只有显式的 layout replacement 才重置。
|
|
1294
|
-
*/
|
|
1295
436
|
updatePaneLayout(panes: PaneSpec[]): void {
|
|
1296
|
-
this.
|
|
1297
|
-
this.applyPaneLayoutSpecs(panes)
|
|
437
|
+
this.layoutManager.updatePaneLayout(panes)
|
|
1298
438
|
}
|
|
1299
439
|
|
|
1300
440
|
setPaneDefinitions(defs: PaneSpec[]): void {
|
|
1301
|
-
this.
|
|
441
|
+
this.layoutManager.setPaneDefinitions(defs)
|
|
1302
442
|
}
|
|
1303
443
|
|
|
1304
444
|
upsertPane(def: PaneSpec): void {
|
|
1305
|
-
|
|
1306
|
-
if (idx === -1) {
|
|
1307
|
-
this.applyPaneLayoutSpecs([...this.opt.panes, { ...def }])
|
|
1308
|
-
return
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const next = [...this.opt.panes]
|
|
1312
|
-
next[idx] = { ...next[idx], ...def }
|
|
1313
|
-
this.applyPaneLayoutSpecs(next)
|
|
445
|
+
this.layoutManager.upsertPane(def)
|
|
1314
446
|
}
|
|
1315
447
|
|
|
1316
448
|
removePaneDefinition(paneId: string): void {
|
|
1317
|
-
|
|
1318
|
-
this._internalPaneRatios.delete(paneId)
|
|
1319
|
-
this.applyPaneLayoutSpecs(this.opt.panes.filter((pane) => pane.id !== paneId))
|
|
449
|
+
this.layoutManager.removePaneDefinition(paneId)
|
|
1320
450
|
}
|
|
1321
451
|
|
|
1322
452
|
bindIndicatorToPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
|
|
1323
|
-
|
|
1324
|
-
if (!paneExists) {
|
|
1325
|
-
this.upsertPane({ id: paneId, ratio: 1, visible: true, role: 'indicator' })
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
1329
|
-
if (!definition) {
|
|
1330
|
-
throw new Error(`[Chart] Unknown indicator: ${indicatorId}`)
|
|
1331
|
-
}
|
|
1332
|
-
const renderer = createSubIndicatorRenderer({ indicatorId, paneId, definition, params })
|
|
1333
|
-
const rendererName = renderer.name
|
|
1334
|
-
const existing = this.getRenderer(rendererName)
|
|
1335
|
-
if (existing) {
|
|
1336
|
-
if (params) this.updateRendererConfig(rendererName, params)
|
|
1337
|
-
return
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
this.useRenderer(renderer, params)
|
|
453
|
+
this.indicatorManager.bindIndicatorToPane(paneId, indicatorId, params)
|
|
1341
454
|
}
|
|
1342
455
|
|
|
1343
456
|
/** 更新绘图对象 */
|
|
1344
457
|
setDrawings(drawings: import('../plugin').DrawingObject[]): void {
|
|
1345
|
-
this.
|
|
458
|
+
this.renderer.getDrawingStore().setAll(drawings)
|
|
1346
459
|
this._drawingsSignal.set(drawings)
|
|
1347
460
|
this.scheduleDraw()
|
|
1348
461
|
}
|
|
1349
462
|
|
|
1350
463
|
/** 更新选中的绘图 ID */
|
|
1351
464
|
setSelectedDrawingId(id: string | null): void {
|
|
1352
|
-
|
|
1353
|
-
|
|
465
|
+
const store = this.renderer.getDrawingStore()
|
|
466
|
+
if (store.getSelectedId() === id) return
|
|
467
|
+
store.setSelectedId(id)
|
|
1354
468
|
this.scheduleDraw()
|
|
1355
469
|
}
|
|
1356
470
|
|
|
1357
|
-
/** 获取当前 pane 布局快照(含 ratio) */
|
|
1358
471
|
getPaneLayoutSpecs(): PaneSpec[] {
|
|
1359
|
-
|
|
1360
|
-
const sum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0)
|
|
1361
|
-
const safeSum = sum > 0 ? sum : 1
|
|
1362
|
-
return this.opt.panes.map((spec) => {
|
|
1363
|
-
const base = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
|
|
1364
|
-
const ratio = spec.visible === false ? base : base / safeSum
|
|
1365
|
-
const pane = this.paneRenderers.find((r) => r.getPane().id === spec.id)?.getPane()
|
|
1366
|
-
return {
|
|
1367
|
-
...spec,
|
|
1368
|
-
ratio,
|
|
1369
|
-
role: pane?.role ?? spec.role,
|
|
1370
|
-
capabilities: pane ? { ...pane.capabilities } : spec.capabilities,
|
|
1371
|
-
}
|
|
1372
|
-
})
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
private emitPaneLayoutChange(): void {
|
|
1376
|
-
// 同步 pane ratios 到 signal
|
|
1377
|
-
const ratios: Record<string, number> = {}
|
|
1378
|
-
this._internalPaneRatios.forEach((ratio, id) => {
|
|
1379
|
-
ratios[id] = ratio
|
|
1380
|
-
})
|
|
1381
|
-
this._paneRatiosSignal.set(ratios)
|
|
1382
|
-
|
|
1383
|
-
this._paneLayoutSignal.set(this.getPaneLayoutSpecs())
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
private applyPaneLayoutSpecs(panes: PaneSpec[]): void {
|
|
1387
|
-
this.opt.panes = panes.map((spec) => ({ ...spec }))
|
|
1388
|
-
this.syncPaneRatiosFromSpecs(this.opt.panes)
|
|
1389
|
-
this.initPanes()
|
|
1390
|
-
this.layoutPanes()
|
|
1391
|
-
this.emitPaneLayoutChange()
|
|
1392
|
-
this.scheduleDraw()
|
|
472
|
+
return this.layoutManager.getPaneLayoutSpecs()
|
|
1393
473
|
}
|
|
1394
474
|
|
|
1395
|
-
/**
|
|
1396
|
-
* 调整相邻 pane 边界(支持连锁挤压)
|
|
1397
|
-
* @param upperPaneId 上方 pane ID(边界位于此 pane 与其下方邻居之间)
|
|
1398
|
-
* @param deltaY Y 方向位移(逻辑像素,正数表示边界向下,upper 增大;负数表示向上,upper 减小)
|
|
1399
|
-
*/
|
|
1400
475
|
resizePaneBoundary(upperPaneId: string, deltaY: number): boolean {
|
|
1401
|
-
|
|
1402
|
-
if (!Number.isFinite(deltaY) || deltaY === 0) return false
|
|
1403
|
-
const vp = this._internalViewport
|
|
1404
|
-
if (!vp) return false
|
|
1405
|
-
|
|
1406
|
-
// === 2. 定位相邻 pane 对(边界两侧) ===
|
|
1407
|
-
const visibleSpecs = this.opt.panes.filter(p => p.visible !== false)
|
|
1408
|
-
const boundaryIndex = visibleSpecs.findIndex(p => p.id === upperPaneId)
|
|
1409
|
-
if (boundaryIndex < 0 || boundaryIndex >= visibleSpecs.length - 1) return false
|
|
1410
|
-
|
|
1411
|
-
const upperSpec = visibleSpecs[boundaryIndex]
|
|
1412
|
-
const lowerSpec = visibleSpecs[boundaryIndex + 1]
|
|
1413
|
-
if (!upperSpec || !lowerSpec) return false
|
|
1414
|
-
|
|
1415
|
-
// === 3. 收集所有 pane 当前高度 ===
|
|
1416
|
-
const heights = new Map<string, number>()
|
|
1417
|
-
for (const spec of visibleSpecs) {
|
|
1418
|
-
const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id)
|
|
1419
|
-
if (renderer) {
|
|
1420
|
-
heights.set(spec.id, renderer.getPane().height)
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// === 4. 连锁挤压/扩展 ===
|
|
1425
|
-
// deltaY > 0: 边界下移,upper expand,lower shrink
|
|
1426
|
-
// deltaY < 0: 边界上移,upper shrink,lower expand
|
|
1427
|
-
const expandIdx = deltaY > 0 ? boundaryIndex : boundaryIndex + 1
|
|
1428
|
-
const shrinkIdx = deltaY > 0 ? boundaryIndex + 1 : boundaryIndex
|
|
1429
|
-
const expandDir = deltaY > 0 ? -1 : 1 // expand 方向(向边界方向找)
|
|
1430
|
-
const shrinkDir = deltaY > 0 ? 1 : -1 // shrink 方向(远离边界方向找)
|
|
1431
|
-
|
|
1432
|
-
let remaining = Math.abs(deltaY)
|
|
1433
|
-
|
|
1434
|
-
// 先尝试 shrink(从 shrinkIdx 开始,沿 shrinkDir 方向连锁)
|
|
1435
|
-
let shrinkCursor = shrinkIdx
|
|
1436
|
-
while (remaining > 0 && shrinkCursor >= 0 && shrinkCursor < visibleSpecs.length) {
|
|
1437
|
-
const spec = visibleSpecs[shrinkCursor]
|
|
1438
|
-
if (!spec) break
|
|
1439
|
-
|
|
1440
|
-
const currentH = heights.get(spec.id) ?? 0
|
|
1441
|
-
const minH = this.getPaneMinHeight(spec, vp.plotHeight)
|
|
1442
|
-
const canShrink = Math.max(0, currentH - minH)
|
|
1443
|
-
|
|
1444
|
-
if (canShrink > 0) {
|
|
1445
|
-
const shrink = Math.min(canShrink, remaining)
|
|
1446
|
-
heights.set(spec.id, currentH - shrink)
|
|
1447
|
-
remaining -= shrink
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// 继续向 shrinkDir 方向找下一个可 shrink 的 pane
|
|
1451
|
-
if (remaining > 0) {
|
|
1452
|
-
shrinkCursor += shrinkDir
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// 如果还有剩余(无法完全 shrink),说明拖拽无效
|
|
1457
|
-
if (remaining > 0) return false
|
|
1458
|
-
|
|
1459
|
-
// 将节省的高度全部加到 expand 方
|
|
1460
|
-
const expandSpec = visibleSpecs[expandIdx]
|
|
1461
|
-
if (!expandSpec) return false
|
|
1462
|
-
const expandCurrentH = heights.get(expandSpec.id) ?? 0
|
|
1463
|
-
heights.set(expandSpec.id, expandCurrentH + Math.abs(deltaY))
|
|
1464
|
-
|
|
1465
|
-
// === 5. 将像素高度转换为 ratio ===
|
|
1466
|
-
const gap = Math.max(0, this.opt.paneGap ?? 0)
|
|
1467
|
-
const totalGaps = gap * Math.max(0, visibleSpecs.length - 1)
|
|
1468
|
-
const availableH = Math.max(1, vp.plotHeight - totalGaps)
|
|
1469
|
-
|
|
1470
|
-
for (const spec of visibleSpecs) {
|
|
1471
|
-
const h = heights.get(spec.id) ?? 0
|
|
1472
|
-
this._internalPaneRatios.set(spec.id, h / availableH)
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// === 6. 归一化并同步 ===
|
|
1476
|
-
this.normalizeVisiblePaneRatios(visibleSpecs)
|
|
1477
|
-
this.syncPaneRatiosToSpecs()
|
|
1478
|
-
|
|
1479
|
-
// === 7. 应用布局 ===
|
|
1480
|
-
this.layoutPanes()
|
|
1481
|
-
this.emitPaneLayoutChange()
|
|
1482
|
-
this.scheduleDraw()
|
|
1483
|
-
return true
|
|
476
|
+
return this.layoutManager.resizePaneBoundary(upperPaneId, deltaY)
|
|
1484
477
|
}
|
|
1485
478
|
|
|
1486
|
-
private resolvePaneRole(spec: PaneSpec, index: number): PaneRole {
|
|
1487
|
-
if (spec.role) return spec.role
|
|
1488
|
-
return index === 0 ? 'price' : 'indicator'
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
479
|
addPane(paneId: string): void {
|
|
1493
|
-
|
|
1494
|
-
console.warn(`Pane "${paneId}" already exists`)
|
|
1495
|
-
return
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
const hasPricePane = this.opt.panes.some((spec, index) => this.resolvePaneRole(spec, index) === 'price')
|
|
1499
|
-
const role: PaneRole = hasPricePane ? 'indicator' : 'price'
|
|
1500
|
-
this.applyPaneLayoutSpecs([
|
|
1501
|
-
...this.opt.panes,
|
|
1502
|
-
{ id: paneId, ratio: 1, visible: true, role },
|
|
1503
|
-
])
|
|
480
|
+
this.layoutManager.addPane(paneId)
|
|
1504
481
|
}
|
|
1505
482
|
|
|
1506
|
-
/**
|
|
1507
|
-
* 动态移除 pane
|
|
1508
|
-
* @param paneId pane 标识符
|
|
1509
|
-
*/
|
|
1510
483
|
removePane(paneId: string): void {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
const next = this.opt.panes.filter((spec) => spec.id !== paneId)
|
|
1514
|
-
this._internalPaneRatios.delete(paneId)
|
|
1515
|
-
this.applyPaneLayoutSpecs(next)
|
|
484
|
+
this.layoutManager.removePane(paneId)
|
|
1516
485
|
}
|
|
1517
486
|
|
|
1518
|
-
/**
|
|
1519
|
-
* 检查 pane 是否存在
|
|
1520
|
-
* @param paneId pane 标识符
|
|
1521
|
-
*/
|
|
1522
487
|
hasPane(paneId: string): boolean {
|
|
1523
|
-
return this.
|
|
488
|
+
return this.layoutManager.hasPane(paneId)
|
|
1524
489
|
}
|
|
1525
490
|
|
|
1526
491
|
// ========== 副图管理 API ==========
|
|
1527
492
|
|
|
1528
|
-
/**
|
|
1529
|
-
* 创建副图面板并注册指标渲染器
|
|
1530
|
-
* @param paneId 副图实例标识符(如 'RSI_0', 'MACD_0')
|
|
1531
|
-
* @param indicatorId 指标类型
|
|
1532
|
-
* @param params 指标参数
|
|
1533
|
-
* @returns 是否创建成功
|
|
1534
|
-
*/
|
|
1535
493
|
createSubPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): boolean {
|
|
1536
|
-
|
|
1537
|
-
const visibleSpecs = this.opt.panes.filter((pane) => pane.visible !== false)
|
|
1538
|
-
const pricePanes = visibleSpecs.filter((pane, index) => this.resolvePaneRole(pane, index) === 'price')
|
|
1539
|
-
const indicatorPanes = visibleSpecs.filter((pane, index) => this.resolvePaneRole(pane, index) === 'indicator')
|
|
1540
|
-
|
|
1541
|
-
if (pricePanes.length === 1) {
|
|
1542
|
-
const pricePane = pricePanes[0]
|
|
1543
|
-
if (pricePane) {
|
|
1544
|
-
this._internalPaneRatios.set(pricePane.id, 3)
|
|
1545
|
-
}
|
|
1546
|
-
for (const pane of indicatorPanes) {
|
|
1547
|
-
this._internalPaneRatios.set(pane.id, 1)
|
|
1548
|
-
}
|
|
1549
|
-
this._internalPaneRatios.set(paneId, 1)
|
|
1550
|
-
} else {
|
|
1551
|
-
this._internalPaneRatios.set(paneId, 1)
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
this.upsertPane({ id: paneId, ratio: this._internalPaneRatios.get(paneId) ?? 1, visible: true, role: 'indicator' })
|
|
1555
|
-
|
|
1556
|
-
const success = this.subPaneManager.create(this, paneId, indicatorId, params ?? this.getDefaultSubPaneParams(indicatorId))
|
|
1557
|
-
return success
|
|
494
|
+
return this.indicatorManager.createSubPane(paneId, indicatorId, params)
|
|
1558
495
|
}
|
|
1559
496
|
|
|
1560
|
-
/**
|
|
1561
|
-
* 移除副图面板及其渲染器
|
|
1562
|
-
* @param paneId 副图实例标识符
|
|
1563
|
-
*/
|
|
1564
497
|
removeSubPane(paneId: string): void {
|
|
1565
|
-
this.
|
|
498
|
+
this.indicatorManager.removeSubPane(paneId)
|
|
1566
499
|
}
|
|
1567
500
|
|
|
1568
|
-
/**
|
|
1569
|
-
* 替换副图的指标类型
|
|
1570
|
-
* @param paneId 副图实例标识符
|
|
1571
|
-
* @param newIndicatorId 新的指标类型
|
|
1572
|
-
* @param params 新指标参数
|
|
1573
|
-
*/
|
|
1574
501
|
replaceSubPaneIndicator(paneId: string, newIndicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
|
|
1575
|
-
this.
|
|
502
|
+
this.indicatorManager.replaceSubPaneIndicator(paneId, newIndicatorId, params)
|
|
1576
503
|
}
|
|
1577
504
|
|
|
1578
|
-
/**
|
|
1579
|
-
* 更新副图指标参数
|
|
1580
|
-
* @param paneId 副图实例标识符
|
|
1581
|
-
* @param params 新参数
|
|
1582
|
-
*/
|
|
1583
505
|
updateSubPaneParams(paneId: string, params: Record<string, unknown>): void {
|
|
1584
|
-
this.
|
|
506
|
+
this.indicatorManager.updateSubPaneParams(paneId, params)
|
|
1585
507
|
}
|
|
1586
508
|
|
|
1587
|
-
/**
|
|
1588
|
-
* 清除所有副图面板
|
|
1589
|
-
*/
|
|
1590
509
|
clearSubPanes(): void {
|
|
1591
|
-
|
|
1592
|
-
const subPaneIds = this.subPaneManager.getPaneIds()
|
|
1593
|
-
|
|
1594
|
-
if (subPaneIds.length === 0) return
|
|
1595
|
-
|
|
1596
|
-
// 移除所有副图
|
|
1597
|
-
this.subPaneManager.clear(this)
|
|
1598
|
-
|
|
1599
|
-
// 清理 pane ratios
|
|
1600
|
-
for (const paneId of subPaneIds) {
|
|
1601
|
-
this._internalPaneRatios.delete(paneId)
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// 更新布局,移除所有副图 pane
|
|
1605
|
-
this.applyPaneLayoutSpecs(this.opt.panes.filter((spec) => !subPaneIds.includes(spec.id)))
|
|
510
|
+
this.indicatorManager.clearSubPanes()
|
|
1606
511
|
}
|
|
1607
512
|
|
|
1608
|
-
/**
|
|
1609
|
-
* 获取当前所有副图指标类型
|
|
1610
|
-
* @deprecated 使用 getSubPaneEntries 获取完整信息
|
|
1611
|
-
*/
|
|
1612
513
|
getSubPaneIndicators(): SubIndicatorType[] {
|
|
1613
|
-
return this.
|
|
514
|
+
return this.indicatorManager.getSubPaneIndicators()
|
|
1614
515
|
}
|
|
1615
516
|
|
|
1616
|
-
/**
|
|
1617
|
-
* 获取所有副图条目
|
|
1618
|
-
*/
|
|
1619
517
|
getSubPaneEntries(): SubPaneEntry[] {
|
|
1620
|
-
return this.
|
|
518
|
+
return this.indicatorManager.getSubPaneEntries()
|
|
1621
519
|
}
|
|
1622
520
|
|
|
1623
|
-
/**
|
|
1624
|
-
* 根据 paneId 获取副图条目
|
|
1625
|
-
* @param paneId 副图实例标识符
|
|
1626
|
-
*/
|
|
1627
521
|
getSubPaneEntry(paneId: string): SubPaneEntry | undefined {
|
|
1628
|
-
return this.
|
|
522
|
+
return this.indicatorManager.getSubPaneEntry(paneId)
|
|
1629
523
|
}
|
|
1630
524
|
|
|
1631
525
|
private getDefaultSubPaneParams(indicatorId: SubIndicatorType): Record<string, unknown> {
|
|
@@ -1671,8 +565,7 @@ export class Chart {
|
|
|
1671
565
|
return { ...(defaults[indicatorId] ?? {}) }
|
|
1672
566
|
}
|
|
1673
567
|
|
|
1674
|
-
|
|
1675
|
-
private static readonly SUB_PANE_PREFIX = 'sub_'
|
|
568
|
+
|
|
1676
569
|
|
|
1677
570
|
/**
|
|
1678
571
|
* 平移价格轴(用于主图区域上下拖动)
|
|
@@ -1730,166 +623,58 @@ export class Chart {
|
|
|
1730
623
|
* @param data K 线数据数组
|
|
1731
624
|
*/
|
|
1732
625
|
updateData(data: KLineData[]) {
|
|
1733
|
-
this.
|
|
1734
|
-
this._dataSignal.set([...this._internalData])
|
|
1735
|
-
|
|
1736
|
-
// 重算 DOM scrollLeft 状态, 防止左右滚动超出数据长度范围
|
|
1737
|
-
const container = this.dom.container
|
|
1738
|
-
if (container) {
|
|
1739
|
-
const minScrollLeft = this.getLeftLoadBufferWidth()
|
|
1740
|
-
if (this.cachedScrollLeft < minScrollLeft) {
|
|
1741
|
-
this.cachedScrollLeft = minScrollLeft
|
|
1742
|
-
this._pendingScrollLeft = minScrollLeft
|
|
1743
|
-
}
|
|
1744
|
-
const contentWidth = this.getContentWidth()
|
|
1745
|
-
const maxScrollLeft = Math.max(0, contentWidth - container.clientWidth)
|
|
1746
|
-
if (this.cachedScrollLeft > maxScrollLeft) {
|
|
1747
|
-
this.cachedScrollLeft = maxScrollLeft
|
|
1748
|
-
this._pendingScrollLeft = maxScrollLeft
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
// 重置交互状态
|
|
1753
|
-
this.interaction.reset()
|
|
1754
|
-
|
|
1755
|
-
// 如果 visibleRange 还是 {0,0},先从 viewport 算一个初步范围
|
|
1756
|
-
// 避免 scheduler 第一次计算时用 {0,0} 产出 Infinity 极值
|
|
1757
|
-
if (this.lastVisibleRange.start === 0 && this.lastVisibleRange.end === 0 && this._internalData.length > 0) {
|
|
1758
|
-
const plotWidth = this.observedSize.width > 0
|
|
1759
|
-
? this.observedSize.width
|
|
1760
|
-
: Math.max(1, Math.round(this.dom.container?.clientWidth ?? 800))
|
|
1761
|
-
const dpr = this.getEffectiveDpr()
|
|
1762
|
-
const { start, end } = getVisibleRange(
|
|
1763
|
-
this.getLogicalScrollLeft(),
|
|
1764
|
-
plotWidth,
|
|
1765
|
-
this.opt.kWidth,
|
|
1766
|
-
this.opt.kGap,
|
|
1767
|
-
this._internalData.length,
|
|
1768
|
-
dpr,
|
|
1769
|
-
)
|
|
1770
|
-
this.lastRawVisibleRange = { start, end }
|
|
1771
|
-
this.lastVisibleRange = { start: Math.max(0, start), end }
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
// 触发指标计算(在 scheduleDraw 之前,确保渲染器读到最新状态)
|
|
1775
|
-
const indicatorsReady = this.indicatorScheduler.update(this._internalData, this.lastVisibleRange)
|
|
1776
|
-
if (indicatorsReady) {
|
|
1777
|
-
this.pendingIndicatorDataUpdate = false
|
|
1778
|
-
this.scheduleDraw()
|
|
1779
|
-
} else {
|
|
1780
|
-
this.pendingIndicatorDataUpdate = true
|
|
1781
|
-
}
|
|
626
|
+
this.dataManager.updateData(data)
|
|
1782
627
|
}
|
|
1783
628
|
|
|
1784
629
|
/** 获取当前数据源(供 renderers 和 interaction 使用) */
|
|
1785
630
|
getData(): KLineData[] {
|
|
1786
|
-
return this.
|
|
631
|
+
return this.dataManager.getData()
|
|
1787
632
|
}
|
|
1788
633
|
|
|
1789
634
|
/** 获取指标调度器(供外部控制器更新指标配置) */
|
|
1790
635
|
getIndicatorScheduler(): IndicatorScheduler {
|
|
1791
|
-
return this.
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
private getTrailingSlotCount(): number {
|
|
1795
|
-
return 24
|
|
636
|
+
return this.indicatorManager.indicatorSchedulerAccessor
|
|
1796
637
|
}
|
|
1797
638
|
|
|
1798
639
|
getLogicalSlotCount(): number {
|
|
1799
|
-
return this.
|
|
640
|
+
return this.dataManager.getLogicalSlotCount()
|
|
1800
641
|
}
|
|
1801
642
|
|
|
1802
643
|
getTimestampAtLogicalIndex(index: number): number | null {
|
|
1803
|
-
|
|
1804
|
-
return this._internalData[index]?.timestamp ?? null
|
|
644
|
+
return this.dataManager.getTimestampAtLogicalIndex(index)
|
|
1805
645
|
}
|
|
1806
646
|
|
|
1807
647
|
/** 根据视口内 X 坐标反查逻辑索引(允许超出最后一根 K 线) */
|
|
1808
648
|
getLogicalIndexAtX(mouseX: number): number | null {
|
|
1809
|
-
|
|
1810
|
-
if (!vp || this._internalData.length === 0) return null
|
|
1811
|
-
const dpr = this.getEffectiveDpr()
|
|
1812
|
-
const { startXPx, unitPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
|
|
1813
|
-
const worldX = Math.round((vp.scrollLeft + mouseX) * dpr)
|
|
1814
|
-
const index = Math.floor((worldX - startXPx) / unitPx)
|
|
1815
|
-
if (index < 0) return null
|
|
1816
|
-
return index
|
|
649
|
+
return this.dataManager.getLogicalIndexAtX(mouseX)
|
|
1817
650
|
}
|
|
1818
651
|
|
|
1819
652
|
/** 根据视口内 X 坐标反查数据索引(用于绘图落点) */
|
|
1820
653
|
getDataIndexAtX(mouseX: number): number | null {
|
|
1821
|
-
|
|
1822
|
-
if (index === null || index >= this._internalData.length) return null
|
|
1823
|
-
return index
|
|
654
|
+
return this.dataManager.getDataIndexAtX(mouseX)
|
|
1824
655
|
}
|
|
1825
656
|
|
|
1826
657
|
|
|
1827
|
-
private static readonly LEADING_SLOTS = 60
|
|
1828
|
-
private static readonly TRAILING_DRAWING_SLOTS = 24
|
|
1829
|
-
|
|
1830
658
|
/** 获取内容总宽度(用于外部 scroll-content 撑开 scrollWidth) */
|
|
1831
659
|
getContentWidth(): number {
|
|
1832
|
-
|
|
1833
|
-
if (dataLength === 0) return 0
|
|
1834
|
-
const kWidth = this.opt.kWidth
|
|
1835
|
-
const kGap = this.opt.kGap
|
|
1836
|
-
const viewWidth = this._internalViewport?.plotWidth ?? 0
|
|
1837
|
-
const dpr = this.getEffectiveDpr()
|
|
1838
|
-
const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
|
|
1839
|
-
const dataPlotWidth = (startXPx + (Chart.LEADING_SLOTS + dataLength + Chart.TRAILING_DRAWING_SLOTS) * unitPx) / dpr
|
|
1840
|
-
return this.getLeftLoadBufferWidth() + Math.max(dataPlotWidth, viewWidth)
|
|
660
|
+
return this.dataManager.getContentWidth()
|
|
1841
661
|
}
|
|
1842
662
|
|
|
1843
663
|
/** 滚动到最右侧(最新数据位置) */
|
|
1844
664
|
scrollToRight(): void {
|
|
1845
|
-
|
|
1846
|
-
if (!container || this._internalData.length === 0) return
|
|
1847
|
-
const dpr = this.getEffectiveDpr()
|
|
1848
|
-
const { unitPx, startXPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
|
|
1849
|
-
const lastKLineEndPx = (startXPx + this._internalData.length * unitPx) / dpr
|
|
1850
|
-
const target = this.getLeftLoadBufferWidth() + Math.max(0, lastKLineEndPx - container.clientWidth)
|
|
1851
|
-
const maxScroll = Math.max(0, container.scrollWidth - container.clientWidth)
|
|
1852
|
-
container.scrollLeft = Math.round(Math.min(target, maxScroll) * dpr) / dpr
|
|
1853
|
-
this.cachedScrollLeft = container.scrollLeft
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
private getLeftLoadBufferWidth(): number {
|
|
1857
|
-
if (this._internalData.length === 0) {
|
|
1858
|
-
this.leftLoadBufferWidth = 0
|
|
1859
|
-
return 0
|
|
1860
|
-
}
|
|
1861
|
-
const plotWidth = this._internalViewport?.plotWidth
|
|
1862
|
-
?? (this.observedSize.width > 0 ? this.observedSize.width : undefined)
|
|
1863
|
-
?? Math.round(this.dom.container?.clientWidth ?? 0)
|
|
1864
|
-
this.leftLoadBufferWidth = Math.max(0, plotWidth)
|
|
1865
|
-
return this.leftLoadBufferWidth
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
private computeRawVisibleRange(): VisibleRange | null {
|
|
1869
|
-
if (this._internalData.length === 0) return null
|
|
1870
|
-
const vp = this._internalViewport ?? this.computeViewport()
|
|
1871
|
-
if (!vp) return null
|
|
1872
|
-
return getVisibleRange(
|
|
1873
|
-
vp.scrollLeft,
|
|
1874
|
-
vp.plotWidth,
|
|
1875
|
-
this.opt.kWidth,
|
|
1876
|
-
this.opt.kGap,
|
|
1877
|
-
this._internalData.length,
|
|
1878
|
-
vp.dpr,
|
|
1879
|
-
)
|
|
665
|
+
this.dataManager.scrollToRight()
|
|
1880
666
|
}
|
|
1881
667
|
|
|
1882
668
|
/** 容器尺寸变化时调用 */
|
|
1883
669
|
resize() {
|
|
1884
|
-
const vp = this.computeViewport()
|
|
670
|
+
const vp = this.viewportManager.computeViewport()
|
|
1885
671
|
// 防御性检查:容器尺寸无效时跳过布局
|
|
1886
672
|
if (!vp || vp.viewWidth < 10 || vp.viewHeight < 10) {
|
|
1887
673
|
return
|
|
1888
674
|
}
|
|
1889
|
-
this.
|
|
1890
|
-
this.layoutPanes()
|
|
1891
|
-
this.
|
|
1892
|
-
this.updateViewportSignal()
|
|
675
|
+
this.renderer.clearCachedFrame()
|
|
676
|
+
this.layoutManager.layoutPanes()
|
|
677
|
+
this.viewportManager.updateViewportSignal()
|
|
1893
678
|
this.scheduleDraw()
|
|
1894
679
|
}
|
|
1895
680
|
|
|
@@ -1898,432 +683,27 @@ export class Chart {
|
|
|
1898
683
|
* @param level 更新级别,默认为 All
|
|
1899
684
|
*/
|
|
1900
685
|
scheduleDraw(level: UpdateLevel = UpdateLevel.All): void {
|
|
1901
|
-
|
|
1902
|
-
if (this.raf !== null) {
|
|
1903
|
-
// 已有 All 级别调度,任何新请求都忽略
|
|
1904
|
-
if (this.pendingUpdateLevel === UpdateLevel.All) return
|
|
1905
|
-
// 新请求是 All,覆盖之前的 Main/Overlay
|
|
1906
|
-
if (level === UpdateLevel.All) {
|
|
1907
|
-
this.pendingUpdateLevel = UpdateLevel.All
|
|
1908
|
-
return
|
|
1909
|
-
}
|
|
1910
|
-
// Main + Overlay = All
|
|
1911
|
-
if (
|
|
1912
|
-
(this.pendingUpdateLevel === UpdateLevel.Main && level === UpdateLevel.Overlay) ||
|
|
1913
|
-
(this.pendingUpdateLevel === UpdateLevel.Overlay && level === UpdateLevel.Main)
|
|
1914
|
-
) {
|
|
1915
|
-
this.pendingUpdateLevel = UpdateLevel.All
|
|
1916
|
-
return
|
|
1917
|
-
}
|
|
1918
|
-
// 同级别或更低级别,忽略
|
|
1919
|
-
return
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
this.pendingUpdateLevel = level
|
|
1923
|
-
this.raf = requestAnimationFrame(() => {
|
|
1924
|
-
this.raf = null
|
|
1925
|
-
const levelToDraw = this.pendingUpdateLevel
|
|
1926
|
-
this.pendingUpdateLevel = UpdateLevel.All // 重置为默认值
|
|
1927
|
-
this.draw(levelToDraw)
|
|
1928
|
-
if (this._pendingScrollLeft !== null) {
|
|
1929
|
-
const c = this.dom.container
|
|
1930
|
-
if (c) {
|
|
1931
|
-
c.scrollLeft = this._pendingScrollLeft
|
|
1932
|
-
this.cachedScrollLeft = c.scrollLeft
|
|
1933
|
-
this._pendingScrollLeft = null
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
})
|
|
686
|
+
this.renderer.scheduleDraw(level)
|
|
1937
687
|
}
|
|
1938
688
|
|
|
1939
689
|
/** 销毁图表实例 */
|
|
1940
690
|
async destroy() {
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
if (this._dataBufferUnsub) {
|
|
1947
|
-
this._dataBufferUnsub()
|
|
1948
|
-
this._dataBufferUnsub = null
|
|
1949
|
-
}
|
|
1950
|
-
this.clearIncrementalLoadHintTimer()
|
|
1951
|
-
this.incrementalLoadHintEl?.remove()
|
|
1952
|
-
this.incrementalLoadHintEl = null
|
|
1953
|
-
this.pendingPrependedCount = 0
|
|
1954
|
-
this._dataBuffer.dispose()
|
|
1955
|
-
this.clearComparisonBuffers()
|
|
1956
|
-
|
|
1957
|
-
// 清理尺寸观察器
|
|
1958
|
-
this.resizeObserver?.disconnect()
|
|
1959
|
-
this.resizeObserver = undefined
|
|
1960
|
-
this.preciseDpr = 0
|
|
1961
|
-
this.observedSize = { width: 0, height: 0 }
|
|
1962
|
-
|
|
1963
|
-
// 清理 scroll 监听
|
|
1964
|
-
if (this.onScroll) {
|
|
1965
|
-
this.dom.container?.removeEventListener('scroll', this.onScroll)
|
|
1966
|
-
this.onScroll = undefined
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
this._internalViewport = null
|
|
1970
|
-
this.cachedDrawFrame = null
|
|
1971
|
-
this.xAxisCtx = null
|
|
1972
|
-
this.paneRenderers.forEach((r) => r.destroy())
|
|
1973
|
-
this.paneRenderers = []
|
|
691
|
+
this.renderer.destroy()
|
|
692
|
+
this.dataManager.destroy()
|
|
693
|
+
this.viewportManager.destroy()
|
|
694
|
+
this.layoutManager.destroy()
|
|
1974
695
|
this.sharedWebGLSurface.destroy()
|
|
1975
|
-
|
|
1976
|
-
// 清理渲染器插件管理器(会调用所有 onUninstall)
|
|
1977
|
-
this.rendererPluginManager.clear()
|
|
1978
|
-
|
|
1979
|
-
this.indicatorScheduler.destroy()
|
|
696
|
+
this.indicatorManager.destroy()
|
|
1980
697
|
await this.pluginHost.destroy()
|
|
1981
698
|
}
|
|
1982
699
|
|
|
1983
|
-
/** 初始化所有 pane */
|
|
1984
|
-
private initPanes() {
|
|
1985
|
-
this.paneRenderers = this.opt.panes.map((spec, index) => {
|
|
1986
|
-
const pane = new Pane(spec.id, {
|
|
1987
|
-
role: this.resolvePaneRole(spec, index),
|
|
1988
|
-
capabilities: spec.capabilities,
|
|
1989
|
-
})
|
|
1990
|
-
|
|
1991
|
-
const mainCanvas = document.createElement('canvas')
|
|
1992
|
-
const overlayCanvas = document.createElement('canvas')
|
|
1993
|
-
const yAxisCanvas = document.createElement('canvas')
|
|
1994
|
-
|
|
1995
|
-
const isMain = pane.role === 'price'
|
|
1996
|
-
|
|
1997
|
-
// Main Canvas - K线、指标、网格
|
|
1998
|
-
mainCanvas.id = `${spec.id}-main`
|
|
1999
|
-
mainCanvas.className = isMain ? 'main-canvas main' : 'main-canvas sub'
|
|
2000
|
-
mainCanvas.style.position = 'absolute'
|
|
2001
|
-
mainCanvas.style.left = '0'
|
|
2002
|
-
mainCanvas.style.top = '0'
|
|
2003
|
-
|
|
2004
|
-
// Overlay Canvas - 十字线、Tooltip(透明,事件穿透)
|
|
2005
|
-
overlayCanvas.id = `${spec.id}-overlay`
|
|
2006
|
-
overlayCanvas.className = 'overlay-canvas'
|
|
2007
|
-
overlayCanvas.style.position = 'absolute'
|
|
2008
|
-
overlayCanvas.style.left = '0'
|
|
2009
|
-
overlayCanvas.style.top = '0'
|
|
2010
|
-
overlayCanvas.style.pointerEvents = 'none' // 事件穿透到 mainCanvas
|
|
2011
|
-
overlayCanvas.style.backgroundColor = 'transparent'
|
|
2012
|
-
|
|
2013
|
-
yAxisCanvas.id = `${spec.id}-yAxis`
|
|
2014
|
-
yAxisCanvas.className = 'right-axis'
|
|
2015
|
-
yAxisCanvas.style.position = 'absolute'
|
|
2016
|
-
yAxisCanvas.style.left = '0'
|
|
2017
|
-
|
|
2018
|
-
const renderer = new PaneRenderer(
|
|
2019
|
-
{ mainCanvas, overlayCanvas, yAxisCanvas },
|
|
2020
|
-
pane,
|
|
2021
|
-
{
|
|
2022
|
-
rightAxisWidth: this.opt.rightAxisWidth,
|
|
2023
|
-
yPaddingPx: this.opt.yPaddingPx,
|
|
2024
|
-
priceLabelWidth: this.opt.priceLabelWidth,
|
|
2025
|
-
},
|
|
2026
|
-
this.sharedWebGLSurface,
|
|
2027
|
-
)
|
|
2028
|
-
|
|
2029
|
-
return renderer
|
|
2030
|
-
})
|
|
2031
|
-
|
|
2032
|
-
const canvasLayer = this.dom.canvasLayer
|
|
2033
|
-
const rightAxisLayer = this.dom.rightAxisLayer
|
|
2034
|
-
if (canvasLayer) {
|
|
2035
|
-
const existingCanvases = canvasLayer.querySelectorAll('canvas:not(.x-axis-canvas)')
|
|
2036
|
-
existingCanvases.forEach((canvas) => canvas.remove())
|
|
2037
|
-
}
|
|
2038
|
-
if (rightAxisLayer) {
|
|
2039
|
-
const existingAxisCanvases = rightAxisLayer.querySelectorAll('canvas.right-axis')
|
|
2040
|
-
existingAxisCanvases.forEach((canvas) => canvas.remove())
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
this.paneRenderers.forEach((renderer) => {
|
|
2044
|
-
const dom = renderer.getDom()
|
|
2045
|
-
// 先添加 mainCanvas,再添加 overlayCanvas(overlay 在上层)
|
|
2046
|
-
canvasLayer.appendChild(dom.mainCanvas)
|
|
2047
|
-
canvasLayer.appendChild(dom.overlayCanvas)
|
|
2048
|
-
rightAxisLayer.appendChild(dom.yAxisCanvas)
|
|
2049
|
-
})
|
|
2050
|
-
|
|
2051
|
-
this.rendererPluginManager.setKnownPaneIds(this.paneRenderers.map((renderer) => renderer.getPane().id))
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
private syncPaneRatiosFromSpecs(specs: PaneSpec[]): void {
|
|
2056
|
-
const next = new Map<string, number>()
|
|
2057
|
-
for (const spec of specs) {
|
|
2058
|
-
const prev = this._internalPaneRatios.get(spec.id)
|
|
2059
|
-
const incoming = Number.isFinite(spec.ratio) ? spec.ratio : 0
|
|
2060
|
-
const ratio = prev !== undefined ? prev : (incoming > 0 ? incoming : 1)
|
|
2061
|
-
next.set(spec.id, ratio)
|
|
2062
|
-
}
|
|
2063
|
-
this._internalPaneRatios = next
|
|
2064
|
-
this.normalizeVisiblePaneRatios(specs)
|
|
2065
|
-
this.syncPaneRatiosToSpecs()
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
private syncPaneRatiosToSpecs(): void {
|
|
2069
|
-
const visible = this.opt.panes.filter(p => p.visible !== false)
|
|
2070
|
-
const visibleSum = visible.reduce((s, p) => s + (this._internalPaneRatios.get(p.id) ?? p.ratio ?? 0), 0)
|
|
2071
|
-
const safeVisibleSum = visibleSum > 0 ? visibleSum : 1
|
|
2072
|
-
|
|
2073
|
-
this.opt.panes = this.opt.panes.map((spec) => {
|
|
2074
|
-
const ratio = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
|
|
2075
|
-
if (spec.visible === false) {
|
|
2076
|
-
return { ...spec, ratio }
|
|
2077
|
-
}
|
|
2078
|
-
return { ...spec, ratio: ratio / safeVisibleSum }
|
|
2079
|
-
})
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
private normalizeVisiblePaneRatios(specs: PaneSpec[]): void {
|
|
2083
|
-
const visible = specs.filter(p => p.visible !== false)
|
|
2084
|
-
if (visible.length === 0) return
|
|
2085
|
-
|
|
2086
|
-
let sum = 0
|
|
2087
|
-
for (const spec of visible) {
|
|
2088
|
-
const raw = this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0
|
|
2089
|
-
const safe = Number.isFinite(raw) && raw > 0 ? raw : 0
|
|
2090
|
-
this._internalPaneRatios.set(spec.id, safe)
|
|
2091
|
-
sum += safe
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
if (sum <= 0) {
|
|
2095
|
-
const equal = 1 / visible.length
|
|
2096
|
-
for (const spec of visible) {
|
|
2097
|
-
this._internalPaneRatios.set(spec.id, equal)
|
|
2098
|
-
}
|
|
2099
|
-
return
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
for (const spec of visible) {
|
|
2103
|
-
const v = this._internalPaneRatios.get(spec.id) ?? 0
|
|
2104
|
-
this._internalPaneRatios.set(spec.id, v / sum)
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
private getPaneMinHeight(spec: PaneSpec, plotHeight: number): number {
|
|
2109
|
-
const fallback = this.opt.defaultPaneMinHeightPx ?? 120 // 最小高度
|
|
2110
|
-
const raw = spec.minHeightPx ?? fallback
|
|
2111
|
-
return Math.max(1, Math.min(Math.round(raw), Math.max(1, plotHeight)))
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
private computePaneHeightsByRatio(visibleSpecs: PaneSpec[], availableH: number): number[] {
|
|
2115
|
-
if (visibleSpecs.length === 0) return []
|
|
2116
|
-
|
|
2117
|
-
const ratios = visibleSpecs.map(spec => this._internalPaneRatios.get(spec.id) ?? spec.ratio ?? 0)
|
|
2118
|
-
const ratioSum = ratios.reduce((s, r) => s + (r > 0 ? r : 0), 0)
|
|
2119
|
-
const safeRatios = ratioSum > 0
|
|
2120
|
-
? ratios.map(r => (r > 0 ? r : 0) / ratioSum)
|
|
2121
|
-
: visibleSpecs.map(() => 1 / visibleSpecs.length)
|
|
2122
|
-
|
|
2123
|
-
const heights = safeRatios.map(r => Math.max(1, Math.round(availableH * r)))
|
|
2124
|
-
const mins = visibleSpecs.map(spec => this.getPaneMinHeight(spec, availableH))
|
|
2125
|
-
|
|
2126
|
-
for (let i = 0; i < heights.length; i++) {
|
|
2127
|
-
heights[i] = Math.max(heights[i]!, Math.min(mins[i]!, availableH))
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
let total = heights.reduce((s, h) => s + h, 0)
|
|
2131
|
-
|
|
2132
|
-
if (total > availableH) {
|
|
2133
|
-
let overflow = total - availableH
|
|
2134
|
-
while (overflow > 0) {
|
|
2135
|
-
let shrunk = false
|
|
2136
|
-
for (let i = heights.length - 1; i >= 0 && overflow > 0; i--) {
|
|
2137
|
-
const minH = Math.max(1, Math.min(mins[i]!, availableH))
|
|
2138
|
-
const h = heights[i]!
|
|
2139
|
-
if (h > minH) {
|
|
2140
|
-
heights[i] = h - 1
|
|
2141
|
-
overflow--
|
|
2142
|
-
shrunk = true
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
if (!shrunk) break
|
|
2146
|
-
}
|
|
2147
|
-
} else if (total < availableH) {
|
|
2148
|
-
heights[heights.length - 1] = (heights[heights.length - 1] ?? 1) + (availableH - total)
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
total = heights.reduce((s, h) => s + h, 0)
|
|
2152
|
-
if (total !== availableH && heights.length > 0) {
|
|
2153
|
-
heights[heights.length - 1] = Math.max(1, (heights[heights.length - 1] ?? 1) + (availableH - total))
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
return heights
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
/** 计算每个 pane 的布局(top 和 height) */
|
|
2160
|
-
private layoutPanes() {
|
|
2161
|
-
const vp = this._internalViewport
|
|
2162
|
-
if (!vp) return
|
|
2163
|
-
|
|
2164
|
-
const visibleSpecs = this.opt.panes.filter(p => p.visible !== false)
|
|
2165
|
-
if (visibleSpecs.length === 0) return
|
|
2166
|
-
|
|
2167
|
-
const gap = Math.max(0, this.opt.paneGap ?? 0)
|
|
2168
|
-
let y = 0
|
|
2169
|
-
|
|
2170
|
-
const totalGaps = gap * Math.max(0, visibleSpecs.length - 1)
|
|
2171
|
-
const availableH = Math.max(1, vp.plotHeight - totalGaps)
|
|
2172
|
-
|
|
2173
|
-
this.normalizeVisiblePaneRatios(visibleSpecs)
|
|
2174
|
-
const paneHeights = this.computePaneHeightsByRatio(visibleSpecs, availableH)
|
|
2175
|
-
|
|
2176
|
-
for (let i = 0; i < visibleSpecs.length; i++) {
|
|
2177
|
-
const spec = visibleSpecs[i]
|
|
2178
|
-
if (!spec) continue
|
|
2179
|
-
|
|
2180
|
-
const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id)
|
|
2181
|
-
if (!renderer) continue
|
|
2182
|
-
|
|
2183
|
-
const pane = renderer.getPane()
|
|
2184
|
-
const h = paneHeights[i] ?? 1
|
|
2185
|
-
|
|
2186
|
-
pane.setLayout(y, h)
|
|
2187
|
-
pane.setPadding(this.opt.yPaddingPx, this.opt.yPaddingPx)
|
|
2188
700
|
|
|
2189
|
-
renderer.resize(vp.plotWidth, h, vp.dpr)
|
|
2190
|
-
renderer.setWebGLRegion({
|
|
2191
|
-
x: 0,
|
|
2192
|
-
y,
|
|
2193
|
-
width: vp.plotWidth,
|
|
2194
|
-
height: h,
|
|
2195
|
-
dpr: vp.dpr,
|
|
2196
|
-
})
|
|
2197
|
-
this.rendererPluginManager.notifyResize(pane.id, wrapPaneInfo(pane))
|
|
2198
|
-
const dom = renderer.getDom()
|
|
2199
|
-
dom.mainCanvas.style.top = `${y}px`
|
|
2200
|
-
dom.overlayCanvas.style.top = `${y}px`
|
|
2201
|
-
dom.yAxisCanvas.style.top = `${y}px`
|
|
2202
|
-
dom.yAxisCanvas.style.left = '0px'
|
|
2203
|
-
|
|
2204
|
-
y += h + gap
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
// 按实际像素高度回写 ratio,确保后续 resize 视觉比例稳定
|
|
2208
|
-
const finalAvailable = Math.max(1, availableH)
|
|
2209
|
-
for (const spec of visibleSpecs) {
|
|
2210
|
-
const renderer = this.paneRenderers.find(r => r.getPane().id === spec.id)
|
|
2211
|
-
if (!renderer) continue
|
|
2212
|
-
const h = renderer.getPane().height
|
|
2213
|
-
this._internalPaneRatios.set(spec.id, h / finalAvailable)
|
|
2214
|
-
}
|
|
2215
|
-
this.normalizeVisiblePaneRatios(visibleSpecs)
|
|
2216
|
-
this.syncPaneRatiosToSpecs()
|
|
2217
|
-
}
|
|
2218
701
|
private computeViewport(): Viewport | null {
|
|
2219
|
-
|
|
2220
|
-
if (!container) return null
|
|
2221
|
-
|
|
2222
|
-
const observedWidth = this.observedSize.width
|
|
2223
|
-
const observedHeight = this.observedSize.height
|
|
2224
|
-
const viewWidth = observedWidth > 0
|
|
2225
|
-
? observedWidth
|
|
2226
|
-
: Math.max(1, Math.round(container.clientWidth))
|
|
2227
|
-
const viewHeight = observedHeight > 0
|
|
2228
|
-
? observedHeight
|
|
2229
|
-
: Math.max(1, Math.round(container.clientHeight))
|
|
2230
|
-
|
|
2231
|
-
const plotWidth = Math.round(viewWidth)
|
|
2232
|
-
const plotHeight = Math.round(viewHeight - this.opt.bottomAxisHeight)
|
|
2233
|
-
|
|
2234
|
-
let dpr = this.getEffectiveDpr()
|
|
2235
|
-
|
|
2236
|
-
const MAX_CANVAS_PIXELS = 16 * 1024 * 1024
|
|
2237
|
-
const requestedPixels = viewWidth * dpr * (viewHeight * dpr)
|
|
2238
|
-
if (requestedPixels > MAX_CANVAS_PIXELS) {
|
|
2239
|
-
dpr = Math.sqrt(MAX_CANVAS_PIXELS / (viewWidth * viewHeight))
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
// 对齐 scrollLeft,消除 translate 亚像素偏移
|
|
2243
|
-
const scrollLeft = Math.round(this.getLogicalScrollLeft() * dpr) / dpr
|
|
2244
|
-
|
|
2245
|
-
const canvasLayerWidth = `${viewWidth}px`
|
|
2246
|
-
if (this.dom.canvasLayer.style.width !== canvasLayerWidth) {
|
|
2247
|
-
this.dom.canvasLayer.style.width = canvasLayerWidth
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
const canvasLayerHeight = `${viewHeight}px`
|
|
2251
|
-
if (this.dom.canvasLayer.style.height !== canvasLayerHeight) {
|
|
2252
|
-
this.dom.canvasLayer.style.height = canvasLayerHeight
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
const xAxisWidth = Math.round(plotWidth * dpr)
|
|
2256
|
-
if (this.dom.xAxisCanvas.width !== xAxisWidth) {
|
|
2257
|
-
this.dom.xAxisCanvas.width = xAxisWidth
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
const xAxisHeight = Math.round(this.opt.bottomAxisHeight * dpr)
|
|
2261
|
-
if (this.dom.xAxisCanvas.height !== xAxisHeight) {
|
|
2262
|
-
this.dom.xAxisCanvas.height = xAxisHeight
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
const xAxisCssWidth = `${xAxisWidth / dpr}px`
|
|
2266
|
-
if (this.dom.xAxisCanvas.style.width !== xAxisCssWidth) {
|
|
2267
|
-
this.dom.xAxisCanvas.style.width = xAxisCssWidth
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const xAxisCssHeight = `${xAxisHeight / dpr}px`
|
|
2271
|
-
if (this.dom.xAxisCanvas.style.height !== xAxisCssHeight) {
|
|
2272
|
-
this.dom.xAxisCanvas.style.height = xAxisCssHeight
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
this.sharedWebGLSurface.resize(plotWidth, plotHeight, dpr)
|
|
2276
|
-
|
|
2277
|
-
const vp: Viewport = {
|
|
2278
|
-
viewWidth,
|
|
2279
|
-
viewHeight,
|
|
2280
|
-
plotWidth,
|
|
2281
|
-
plotHeight,
|
|
2282
|
-
scrollLeft,
|
|
2283
|
-
dpr,
|
|
2284
|
-
}
|
|
2285
|
-
const prevViewport = this._internalViewport
|
|
2286
|
-
const viewportChanged = !prevViewport
|
|
2287
|
-
|| prevViewport.viewWidth !== vp.viewWidth
|
|
2288
|
-
|| prevViewport.viewHeight !== vp.viewHeight
|
|
2289
|
-
|| prevViewport.plotWidth !== vp.plotWidth
|
|
2290
|
-
|| prevViewport.plotHeight !== vp.plotHeight
|
|
2291
|
-
|| prevViewport.scrollLeft !== vp.scrollLeft
|
|
2292
|
-
|| prevViewport.dpr !== vp.dpr
|
|
2293
|
-
|
|
2294
|
-
this._internalViewport = vp
|
|
2295
|
-
if (viewportChanged) {
|
|
2296
|
-
const current = this._viewportSignal.peek()
|
|
2297
|
-
this._viewportSignal.set({
|
|
2298
|
-
zoomLevel: current.zoomLevel,
|
|
2299
|
-
plotWidth: vp.plotWidth,
|
|
2300
|
-
plotHeight: vp.plotHeight,
|
|
2301
|
-
dpr: vp.dpr > 0 ? vp.dpr : current.dpr,
|
|
2302
|
-
visibleFrom: current.visibleFrom,
|
|
2303
|
-
visibleTo: current.visibleTo,
|
|
2304
|
-
kWidth: current.kWidth,
|
|
2305
|
-
kGap: current.kGap,
|
|
2306
|
-
})
|
|
2307
|
-
}
|
|
2308
|
-
return vp
|
|
702
|
+
return this.viewportManager.computeViewport()
|
|
2309
703
|
}
|
|
2310
704
|
|
|
2311
705
|
// ==================== Facade API (High-level interface for adapters) ====================
|
|
2312
706
|
|
|
2313
|
-
// ---------- Signals ----------
|
|
2314
|
-
private _viewportSignal = createSignal<ViewportState>({
|
|
2315
|
-
zoomLevel: 1,
|
|
2316
|
-
plotWidth: 0,
|
|
2317
|
-
plotHeight: 0,
|
|
2318
|
-
dpr: 1,
|
|
2319
|
-
visibleFrom: 0,
|
|
2320
|
-
visibleTo: 0,
|
|
2321
|
-
kWidth: 0,
|
|
2322
|
-
kGap: 1,
|
|
2323
|
-
})
|
|
2324
|
-
|
|
2325
|
-
private _dataSignal = createSignal<ReadonlyArray<KLineData>>([])
|
|
2326
|
-
private _symbolsSignal = createSignal<ReadonlyArray<SymbolSpec>>([])
|
|
2327
707
|
private _themeSignal = createSignal<'light' | 'dark'>('light')
|
|
2328
708
|
private _drawingToolSignal = createSignal<DrawingToolType | null>(null)
|
|
2329
709
|
private _drawingsSignal = createSignal<ReadonlyArray<import('../plugin').DrawingObject>>([])
|
|
@@ -2346,51 +726,29 @@ export class Chart {
|
|
|
2346
726
|
isHoveringRightAxis: false,
|
|
2347
727
|
})
|
|
2348
728
|
|
|
2349
|
-
private _indicatorsComputed = computed<ReadonlyArray<IndicatorInstance>>(() => {
|
|
2350
|
-
const mainIndicators: IndicatorInstance[] = [...this._mainIndicatorsSignal().entries()].map(([id, entry]) => ({
|
|
2351
|
-
id,
|
|
2352
|
-
definitionId: id,
|
|
2353
|
-
label: id,
|
|
2354
|
-
name: id,
|
|
2355
|
-
role: 'main' as const,
|
|
2356
|
-
params: { ...entry.params },
|
|
2357
|
-
}))
|
|
2358
|
-
|
|
2359
|
-
const subIndicators: IndicatorInstance[] = this.subPaneManager.entriesSignal().map(entry => ({
|
|
2360
|
-
id: entry.paneId,
|
|
2361
|
-
definitionId: entry.indicatorId,
|
|
2362
|
-
label: entry.indicatorId,
|
|
2363
|
-
name: entry.indicatorId,
|
|
2364
|
-
role: 'sub' as const,
|
|
2365
|
-
paneId: entry.paneId,
|
|
2366
|
-
params: { ...entry.params },
|
|
2367
|
-
}))
|
|
2368
|
-
|
|
2369
|
-
return [...mainIndicators, ...subIndicators]
|
|
2370
|
-
})
|
|
2371
|
-
private _subPanesComputed = computed<ReadonlyArray<SubPaneInfo>>(() => {
|
|
2372
|
-
const ratios = this._paneRatiosSignal()
|
|
2373
|
-
return this.subPaneManager.entriesSignal().map(entry => ({
|
|
2374
|
-
paneId: entry.paneId,
|
|
2375
|
-
indicatorId: entry.indicatorId,
|
|
2376
|
-
params: { ...entry.params },
|
|
2377
|
-
ratio: ratios[entry.paneId] ?? 1,
|
|
2378
|
-
}))
|
|
2379
|
-
})
|
|
2380
|
-
|
|
2381
729
|
/** 视口状态信号 */
|
|
2382
730
|
get viewport(): Signal<ViewportState> {
|
|
2383
|
-
return this.
|
|
731
|
+
return this.viewportManager.viewportSignal
|
|
2384
732
|
}
|
|
2385
733
|
|
|
2386
734
|
/** 数据信号 */
|
|
2387
735
|
get data(): Signal<ReadonlyArray<KLineData>> {
|
|
2388
|
-
return this.
|
|
736
|
+
return this.dataManager.data
|
|
2389
737
|
}
|
|
2390
738
|
|
|
2391
739
|
/** 符号信号 */
|
|
2392
740
|
get symbols(): Signal<ReadonlyArray<SymbolSpec>> {
|
|
2393
|
-
return this.
|
|
741
|
+
return this.dataManager.symbols
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** 比较商品颜色信号 */
|
|
745
|
+
get comparisonColors(): Signal<ReadonlyMap<string, string>> {
|
|
746
|
+
return this.dataManager.comparisonColors
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** 比较商品加载信号 */
|
|
750
|
+
get comparisonLoading(): Signal<boolean> {
|
|
751
|
+
return this.dataManager.comparisonLoading
|
|
2394
752
|
}
|
|
2395
753
|
|
|
2396
754
|
/** 主题信号 */
|
|
@@ -2400,12 +758,12 @@ export class Chart {
|
|
|
2400
758
|
|
|
2401
759
|
/** 指标实例列表信号(派生信号,自动随主/副图状态更新) */
|
|
2402
760
|
get indicators(): Computed<ReadonlyArray<IndicatorInstance>> {
|
|
2403
|
-
return this.
|
|
761
|
+
return this.indicatorManager.indicatorsComputed
|
|
2404
762
|
}
|
|
2405
763
|
|
|
2406
764
|
/** 子图信息信号(派生信号,自动随副图条目/比例更新) */
|
|
2407
765
|
get subPanes(): Computed<ReadonlyArray<SubPaneInfo>> {
|
|
2408
|
-
return this.
|
|
766
|
+
return this.indicatorManager.subPanesComputed
|
|
2409
767
|
}
|
|
2410
768
|
|
|
2411
769
|
/** 当前绘图工具信号 */
|
|
@@ -2434,233 +792,36 @@ export class Chart {
|
|
|
2434
792
|
|
|
2435
793
|
// ---------- Data ----------
|
|
2436
794
|
|
|
2437
|
-
/**
|
|
2438
|
-
* 设置数据(高层 API)
|
|
2439
|
-
* 内部调用 updateData,并更新 data signal
|
|
2440
|
-
*/
|
|
2441
795
|
setData(data: KLineData[]): void {
|
|
2442
|
-
this.
|
|
796
|
+
this.dataManager.setData(data)
|
|
2443
797
|
}
|
|
2444
798
|
|
|
2445
|
-
/**
|
|
2446
|
-
* 追加数据(高层 API)
|
|
2447
|
-
* 合并现有数据并更新
|
|
2448
|
-
*/
|
|
2449
799
|
appendData(newData: KLineData[]): void {
|
|
2450
|
-
|
|
2451
|
-
this.setData(merged)
|
|
800
|
+
this.dataManager.appendData(newData)
|
|
2452
801
|
}
|
|
2453
802
|
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
*/
|
|
2457
|
-
setDataFetcher(fetcher: DataFetcher | null): void {
|
|
2458
|
-
this._dataFetcher = fetcher
|
|
2459
|
-
this._dataBuffer.setFetcher(fetcher)
|
|
2460
|
-
for (const buffer of this._comparisonBuffers.values()) {
|
|
2461
|
-
buffer.setFetcher(fetcher)
|
|
2462
|
-
}
|
|
803
|
+
setDataFetcher(fetcher: import('../controllers/types').DataFetcher | null): void {
|
|
804
|
+
this.dataManager.setDataFetcher(fetcher)
|
|
2463
805
|
}
|
|
2464
806
|
|
|
2465
|
-
get dataBuffer(): DataBuffer {
|
|
2466
|
-
return this.
|
|
807
|
+
get dataBuffer(): import('../data-fetchers/dataBuffer').DataBuffer {
|
|
808
|
+
return this.dataManager.dataBuffer
|
|
2467
809
|
}
|
|
2468
810
|
|
|
2469
811
|
checkVisibleRangeGap(): void {
|
|
2470
|
-
|
|
2471
|
-
const window = this._dataBuffer.loadedWindow
|
|
2472
|
-
if (!window) return
|
|
2473
|
-
const range = this.computeRawVisibleRange() ?? this.lastRawVisibleRange
|
|
2474
|
-
|
|
2475
|
-
if (range.start < 0 && this._dataFetcher) {
|
|
2476
|
-
const MS_PER_DAY = 86_400_000
|
|
2477
|
-
const earlierThanEarliest = window.earliestTs - 90 * MS_PER_DAY
|
|
2478
|
-
this._dataBuffer.ensureRange(earlierThanEarliest, window.earliestTs)
|
|
2479
|
-
return
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
|
-
if (range.start >= this._internalData.length) return
|
|
2483
|
-
const firstVisibleTs = this._internalData[Math.max(0, range.start)]?.timestamp
|
|
2484
|
-
if (firstVisibleTs === undefined) return
|
|
2485
|
-
if (firstVisibleTs < window.earliestTs) {
|
|
2486
|
-
this._dataBuffer.ensureRange(firstVisibleTs, window.earliestTs)
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
private checkVisibleRangeGapWhenIdle(): void {
|
|
2491
|
-
if (this.interaction.isPointerDown()) return
|
|
2492
|
-
this.checkVisibleRangeGap()
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
private getComparisonEquivalentPriceRange(range: VisibleRange): { min: number; max: number } | null {
|
|
2496
|
-
if (this._comparisonSpecs.length === 0 || this._comparisonData.size === 0) return null
|
|
2497
|
-
const baseIndex = Math.max(0, range.start)
|
|
2498
|
-
const mainBase = this._internalData[baseIndex]?.close
|
|
2499
|
-
const baseTimestamp = this._internalData[baseIndex]?.timestamp
|
|
2500
|
-
if (!Number.isFinite(mainBase) || mainBase <= 0 || baseTimestamp === undefined) return null
|
|
2501
|
-
|
|
2502
|
-
let min = Number.POSITIVE_INFINITY
|
|
2503
|
-
let max = Number.NEGATIVE_INFINITY
|
|
2504
|
-
|
|
2505
|
-
for (const spec of this._comparisonSpecs) {
|
|
2506
|
-
const data = this._comparisonData.get(spec.symbol)
|
|
2507
|
-
if (!data?.length) continue
|
|
2508
|
-
|
|
2509
|
-
const baseline = this.findComparisonBaseline(data, baseTimestamp)
|
|
2510
|
-
if (!baseline || !Number.isFinite(baseline.close) || baseline.close <= 0) continue
|
|
2511
|
-
|
|
2512
|
-
const byTimestamp = new Map<number, KLineData>()
|
|
2513
|
-
for (const item of data) byTimestamp.set(item.timestamp, item)
|
|
2514
|
-
|
|
2515
|
-
for (let i = Math.max(0, range.start); i < range.end && i < this._internalData.length; i++) {
|
|
2516
|
-
const mainItem = this._internalData[i]
|
|
2517
|
-
if (!mainItem) continue
|
|
2518
|
-
const item = byTimestamp.get(mainItem.timestamp)
|
|
2519
|
-
if (!item || !Number.isFinite(item.close)) continue
|
|
2520
|
-
|
|
2521
|
-
const pct = (item.close - baseline.close) / baseline.close
|
|
2522
|
-
const equivalentPrice = mainBase * (1 + pct)
|
|
2523
|
-
if (!Number.isFinite(equivalentPrice)) continue
|
|
2524
|
-
min = Math.min(min, equivalentPrice)
|
|
2525
|
-
max = Math.max(max, equivalentPrice)
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
if (!Number.isFinite(min) || !Number.isFinite(max)) return null
|
|
2530
|
-
return { min, max }
|
|
812
|
+
this.dataManager.checkVisibleRangeGap()
|
|
2531
813
|
}
|
|
2532
814
|
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
if (item.timestamp >= timestamp) return item
|
|
2536
|
-
}
|
|
2537
|
-
return null
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
private mergeNumericRanges(
|
|
2541
|
-
left: { min: number; max: number } | null | undefined,
|
|
2542
|
-
right: { min: number; max: number } | null | undefined,
|
|
2543
|
-
): { min: number; max: number } | null {
|
|
2544
|
-
if (!left) return right ?? null
|
|
2545
|
-
if (!right) return left
|
|
2546
|
-
return {
|
|
2547
|
-
min: Math.min(left.min, right.min),
|
|
2548
|
-
max: Math.max(left.max, right.max),
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
private syncComparisonBuffers(specs: ReadonlyArray<SymbolSpec>): void {
|
|
2553
|
-
this._comparisonSpecs = [...specs]
|
|
2554
|
-
const nextKeys = new Set(specs.map((spec) => spec.symbol))
|
|
2555
|
-
|
|
2556
|
-
for (const [key, buffer] of this._comparisonBuffers) {
|
|
2557
|
-
if (nextKeys.has(key)) continue
|
|
2558
|
-
this._comparisonBufferUnsubs.get(key)?.()
|
|
2559
|
-
this._comparisonBufferUnsubs.delete(key)
|
|
2560
|
-
buffer.dispose()
|
|
2561
|
-
this._comparisonBuffers.delete(key)
|
|
2562
|
-
this._comparisonData.delete(key)
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
if (!this._dataFetcher) return
|
|
2566
|
-
|
|
2567
|
-
for (const spec of specs) {
|
|
2568
|
-
const key = spec.symbol
|
|
2569
|
-
let buffer = this._comparisonBuffers.get(key)
|
|
2570
|
-
if (!buffer) {
|
|
2571
|
-
const newBuffer = new DataBuffer()
|
|
2572
|
-
newBuffer.setFetcher(this._dataFetcher)
|
|
2573
|
-
this._comparisonBuffers.set(key, newBuffer)
|
|
2574
|
-
const unsubscribe = newBuffer.data.subscribe(() => {
|
|
2575
|
-
this._comparisonData.set(key, [...newBuffer.data.peek()])
|
|
2576
|
-
this.scheduleDraw()
|
|
2577
|
-
})
|
|
2578
|
-
this._comparisonBufferUnsubs.set(key, unsubscribe)
|
|
2579
|
-
buffer = newBuffer
|
|
2580
|
-
} else {
|
|
2581
|
-
buffer.setFetcher(this._dataFetcher)
|
|
2582
|
-
}
|
|
2583
|
-
buffer.setSymbol(spec)
|
|
2584
|
-
}
|
|
815
|
+
setSymbols(specs: ReadonlyArray<SymbolSpec>): void {
|
|
816
|
+
this.dataManager.setSymbols(specs)
|
|
2585
817
|
}
|
|
2586
818
|
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
this._comparisonBufferUnsubs.clear()
|
|
2590
|
-
for (const buffer of this._comparisonBuffers.values()) buffer.dispose()
|
|
2591
|
-
this._comparisonBuffers.clear()
|
|
2592
|
-
this._comparisonData.clear()
|
|
2593
|
-
this._comparisonSpecs = []
|
|
819
|
+
addComparisonSymbol(spec: SymbolSpec): void {
|
|
820
|
+
this.dataManager.addComparisonSymbol(spec)
|
|
2594
821
|
}
|
|
2595
822
|
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
*/
|
|
2599
|
-
setSymbols(specs: ReadonlyArray<SymbolSpec>): void {
|
|
2600
|
-
this._symbolsSignal.set(specs)
|
|
2601
|
-
if (specs.length === 0) {
|
|
2602
|
-
this.clearComparisonBuffers()
|
|
2603
|
-
return
|
|
2604
|
-
}
|
|
2605
|
-
const spec = specs[0]!
|
|
2606
|
-
this.syncComparisonBuffers(specs.slice(1))
|
|
2607
|
-
if (!this._dataFetcher) return
|
|
2608
|
-
|
|
2609
|
-
this._dataBuffer.setFetcher(this._dataFetcher)
|
|
2610
|
-
|
|
2611
|
-
this._dataBuffer.onPrepend = (count: number) => {
|
|
2612
|
-
this.pendingPrependedCount = count
|
|
2613
|
-
const dpr = this.getEffectiveDpr()
|
|
2614
|
-
const { unitPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
|
|
2615
|
-
const compensation = (count * unitPx) / dpr
|
|
2616
|
-
const container = this.dom.container
|
|
2617
|
-
if (container) {
|
|
2618
|
-
container.scrollLeft += compensation
|
|
2619
|
-
this.cachedScrollLeft = container.scrollLeft
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
if (!this._dataBufferUnsub) {
|
|
2624
|
-
this._dataBufferUnsub = this._dataBuffer.data.subscribe(() => {
|
|
2625
|
-
const bufferData = this._dataBuffer.data.peek()
|
|
2626
|
-
this._internalData = [...bufferData]
|
|
2627
|
-
this._dataSignal.set([...this._internalData])
|
|
2628
|
-
const prependedCount = this.pendingPrependedCount
|
|
2629
|
-
this.pendingPrependedCount = 0
|
|
2630
|
-
if (this.cachedScrollLeft < this.getLeftLoadBufferWidth()) {
|
|
2631
|
-
const desiredScrollLeft = this.getLeftLoadBufferWidth()
|
|
2632
|
-
this.cachedScrollLeft = desiredScrollLeft
|
|
2633
|
-
this._pendingScrollLeft = desiredScrollLeft
|
|
2634
|
-
}
|
|
2635
|
-
this.interaction.reset()
|
|
2636
|
-
if (this.lastVisibleRange.start === 0 && this.lastVisibleRange.end === 0 && this._internalData.length > 0) {
|
|
2637
|
-
const plotWidth = this.observedSize.width > 0
|
|
2638
|
-
? this.observedSize.width
|
|
2639
|
-
: Math.max(1, Math.round(this.dom.container?.clientWidth ?? 800))
|
|
2640
|
-
const dpr = this.getEffectiveDpr()
|
|
2641
|
-
const { start, end } = getVisibleRange(
|
|
2642
|
-
this.getLogicalScrollLeft(),
|
|
2643
|
-
plotWidth,
|
|
2644
|
-
this.opt.kWidth,
|
|
2645
|
-
this.opt.kGap,
|
|
2646
|
-
this._internalData.length,
|
|
2647
|
-
dpr,
|
|
2648
|
-
)
|
|
2649
|
-
this.lastRawVisibleRange = { start, end }
|
|
2650
|
-
this.lastVisibleRange = { start: Math.max(0, start), end }
|
|
2651
|
-
}
|
|
2652
|
-
const indicatorsReady = this.indicatorScheduler.update(this._internalData, this.lastVisibleRange)
|
|
2653
|
-
if (indicatorsReady) {
|
|
2654
|
-
this.pendingIndicatorDataUpdate = false
|
|
2655
|
-
this.scheduleDraw()
|
|
2656
|
-
} else {
|
|
2657
|
-
this.pendingIndicatorDataUpdate = true
|
|
2658
|
-
}
|
|
2659
|
-
this.showIncrementalLoadHint(prependedCount)
|
|
2660
|
-
})
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
this._dataBuffer.setSymbol(spec)
|
|
823
|
+
removeComparisonSymbol(symbol: string): void {
|
|
824
|
+
this.dataManager.removeComparisonSymbol(symbol)
|
|
2664
825
|
}
|
|
2665
826
|
|
|
2666
827
|
// ---------- Theme ----------
|
|
@@ -2680,57 +841,21 @@ export class Chart {
|
|
|
2680
841
|
* 计算并应用新的 render state,更新 viewport signal
|
|
2681
842
|
*/
|
|
2682
843
|
zoomToLevel(level: number, anchorX?: number): void {
|
|
2683
|
-
|
|
2684
|
-
this.applyZoom(clamped, anchorX)
|
|
844
|
+
this.zoomController.zoomToLevel(level, anchorX)
|
|
2685
845
|
}
|
|
2686
846
|
|
|
2687
847
|
/**
|
|
2688
848
|
* 放大(高层 API)
|
|
2689
849
|
*/
|
|
2690
850
|
zoomIn(anchorX?: number): void {
|
|
2691
|
-
this.
|
|
851
|
+
this.zoomController.zoomIn(anchorX)
|
|
2692
852
|
}
|
|
2693
853
|
|
|
2694
854
|
/**
|
|
2695
855
|
* 缩小(高层 API)
|
|
2696
856
|
*/
|
|
2697
857
|
zoomOut(anchorX?: number): void {
|
|
2698
|
-
this.
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
/**
|
|
2702
|
-
* 内部缩放实现
|
|
2703
|
-
* 使用 computeZoom 纯函数计算精确的 scrollLeft
|
|
2704
|
-
*/
|
|
2705
|
-
private applyZoom(targetLevel: number, anchorViewportX?: number): void {
|
|
2706
|
-
if (targetLevel === this.currentZoomLevel) return
|
|
2707
|
-
|
|
2708
|
-
const delta = targetLevel - this.currentZoomLevel
|
|
2709
|
-
const logicalScrollLeft = this.getLogicalScrollLeft()
|
|
2710
|
-
const dpr = this.getCurrentDpr()
|
|
2711
|
-
|
|
2712
|
-
const result = computeZoom(
|
|
2713
|
-
delta,
|
|
2714
|
-
anchorViewportX ?? 0,
|
|
2715
|
-
logicalScrollLeft,
|
|
2716
|
-
this.currentZoomLevel,
|
|
2717
|
-
this.opt.kWidth,
|
|
2718
|
-
this.opt.kGap,
|
|
2719
|
-
{
|
|
2720
|
-
minKWidth: this.opt.minKWidth,
|
|
2721
|
-
maxKWidth: this.opt.maxKWidth,
|
|
2722
|
-
zoomLevelCount: this.zoomLevelCount,
|
|
2723
|
-
dpr,
|
|
2724
|
-
},
|
|
2725
|
-
)
|
|
2726
|
-
|
|
2727
|
-
if (!result) return
|
|
2728
|
-
|
|
2729
|
-
const domScrollLeft = result.newScrollLeft + this.getLeftLoadBufferWidth()
|
|
2730
|
-
this.currentZoomLevel = result.targetLevel
|
|
2731
|
-
this.cachedScrollLeft = domScrollLeft
|
|
2732
|
-
this._pendingScrollLeft = domScrollLeft
|
|
2733
|
-
this.applyRenderState(result.newKWidth, result.newKGap, result.targetLevel)
|
|
858
|
+
this.zoomController.zoomOut(anchorX)
|
|
2734
859
|
}
|
|
2735
860
|
|
|
2736
861
|
// ---------- Interaction (Zero-config unified entry) ----------
|
|
@@ -2806,16 +931,8 @@ export class Chart {
|
|
|
2806
931
|
* 使用 computeZoom 计算精确的 scrollLeft,更新 viewport signal
|
|
2807
932
|
*/
|
|
2808
933
|
handleWheelEvent(e: WheelEvent): void {
|
|
2809
|
-
const delta = e.deltaY > 0 ? -1 : 1
|
|
2810
|
-
const targetLevel = Math.max(1, Math.min(this.zoomLevelCount, this.currentZoomLevel + delta))
|
|
2811
|
-
|
|
2812
|
-
if (targetLevel === this.currentZoomLevel) return
|
|
2813
|
-
|
|
2814
|
-
// 获取鼠标在视口中的位置作为缩放锚点(视口局部坐标)
|
|
2815
934
|
const rect = this.dom.container.getBoundingClientRect()
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
this.applyZoom(targetLevel, mouseX)
|
|
935
|
+
this.zoomController.handleWheel(e.deltaY, e.clientX - rect.left)
|
|
2819
936
|
}
|
|
2820
937
|
|
|
2821
938
|
/**
|
|
@@ -2823,7 +940,7 @@ export class Chart {
|
|
|
2823
940
|
* 更新缓存的 scrollLeft 并触发交互 controller
|
|
2824
941
|
*/
|
|
2825
942
|
handleScrollEvent(): void {
|
|
2826
|
-
this.interaction.onScroll({ scheduleDraw: !this.pendingIndicatorDataUpdate })
|
|
943
|
+
this.interaction.onScroll({ scheduleDraw: !this.dataManager.pendingIndicatorDataUpdate })
|
|
2827
944
|
// 更新 viewport signal 中的 visible range
|
|
2828
945
|
this.updateViewportSignal()
|
|
2829
946
|
}
|
|
@@ -2834,30 +951,14 @@ export class Chart {
|
|
|
2834
951
|
* @param centerClientX 捏合中心在视口中的 X 坐标
|
|
2835
952
|
*/
|
|
2836
953
|
handlePinchZoom(delta: number, centerClientX: number): void {
|
|
2837
|
-
|
|
2838
|
-
if (targetLevel === this.currentZoomLevel) return
|
|
2839
|
-
|
|
2840
|
-
// centerClientX 已经是视口局部坐标,直接使用
|
|
2841
|
-
this.applyZoom(targetLevel, centerClientX)
|
|
954
|
+
this.zoomController.handlePinch(delta, centerClientX)
|
|
2842
955
|
}
|
|
2843
956
|
|
|
2844
957
|
/**
|
|
2845
958
|
* 更新 viewport signal(用于滚动事件)
|
|
2846
959
|
*/
|
|
2847
960
|
private updateViewportSignal(): void {
|
|
2848
|
-
|
|
2849
|
-
if (!vp) return
|
|
2850
|
-
|
|
2851
|
-
this._viewportSignal.set({
|
|
2852
|
-
zoomLevel: this.currentZoomLevel,
|
|
2853
|
-
plotWidth: vp.plotWidth,
|
|
2854
|
-
plotHeight: vp.plotHeight,
|
|
2855
|
-
dpr: vp.dpr,
|
|
2856
|
-
visibleFrom: this.lastVisibleRange.start,
|
|
2857
|
-
visibleTo: this.lastVisibleRange.end,
|
|
2858
|
-
kWidth: this.opt.kWidth,
|
|
2859
|
-
kGap: this.opt.kGap,
|
|
2860
|
-
})
|
|
961
|
+
this.viewportManager.updateViewportSignal()
|
|
2861
962
|
}
|
|
2862
963
|
|
|
2863
964
|
// ---------- Indicators (Explicit role) ----------
|
|
@@ -2874,81 +975,19 @@ export class Chart {
|
|
|
2874
975
|
role: 'main' | 'sub',
|
|
2875
976
|
params?: Record<string, unknown>,
|
|
2876
977
|
): string | null {
|
|
2877
|
-
|
|
2878
|
-
const success = this.enableMainIndicator(definitionId, params as Record<string, number | boolean | string>)
|
|
2879
|
-
if (!success) return null
|
|
2880
|
-
return definitionId.toUpperCase()
|
|
2881
|
-
} else {
|
|
2882
|
-
// 副图指标
|
|
2883
|
-
const paneId = `${definitionId.toUpperCase()}_${Date.now()}`
|
|
2884
|
-
const success = this.createSubPane(
|
|
2885
|
-
paneId,
|
|
2886
|
-
definitionId as SubIndicatorType,
|
|
2887
|
-
params as Record<string, number | boolean | string>,
|
|
2888
|
-
)
|
|
2889
|
-
if (!success) return null
|
|
2890
|
-
return paneId
|
|
2891
|
-
}
|
|
978
|
+
return this.indicatorManager.addIndicator(definitionId, role, params)
|
|
2892
979
|
}
|
|
2893
980
|
|
|
2894
|
-
/**
|
|
2895
|
-
* 移除指标(高层 API)
|
|
2896
|
-
* @param instanceId 指标实例 ID
|
|
2897
|
-
* @returns 是否成功移除
|
|
2898
|
-
*/
|
|
2899
981
|
removeIndicator(instanceId: string): boolean {
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
// 先尝试作为主图指标移除
|
|
2903
|
-
if (this._mainIndicatorsSignal.peek().has(id)) {
|
|
2904
|
-
return this.disableMainIndicator(instanceId)
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
// 再尝试作为副图指标移除
|
|
2908
|
-
const subPaneEntry = this.getSubPaneEntry(instanceId)
|
|
2909
|
-
if (subPaneEntry) {
|
|
2910
|
-
this.removeSubPane(instanceId)
|
|
2911
|
-
return true
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
return false
|
|
982
|
+
return this.indicatorManager.removeIndicator(instanceId)
|
|
2915
983
|
}
|
|
2916
984
|
|
|
2917
|
-
/**
|
|
2918
|
-
* 更新指标参数(高层 API)
|
|
2919
|
-
* @param instanceId 指标实例 ID
|
|
2920
|
-
* @param params 新参数
|
|
2921
|
-
* @returns 是否成功更新
|
|
2922
|
-
*/
|
|
2923
985
|
updateIndicatorParams(instanceId: string, params: Record<string, unknown>): boolean {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
// 先尝试作为主图指标更新
|
|
2927
|
-
if (this._mainIndicatorsSignal.peek().has(id)) {
|
|
2928
|
-
this.updateMainIndicatorParams(instanceId, params as Record<string, number | boolean | string>)
|
|
2929
|
-
return true
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
// 再尝试作为副图指标更新
|
|
2933
|
-
const subPaneEntry = this.getSubPaneEntry(instanceId)
|
|
2934
|
-
if (subPaneEntry) {
|
|
2935
|
-
this.updateSubPaneParams(instanceId, params)
|
|
2936
|
-
return true
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
return false
|
|
986
|
+
return this.indicatorManager.updateIndicatorParams(instanceId, params)
|
|
2940
987
|
}
|
|
2941
988
|
|
|
2942
|
-
/**
|
|
2943
|
-
* 重新排序指标(高层 API)
|
|
2944
|
-
* @param orderedInstanceIds 排序后的指标实例 ID 数组
|
|
2945
|
-
* @returns 是否成功
|
|
2946
|
-
*/
|
|
2947
989
|
reorderIndicators(orderedInstanceIds: string[]): boolean {
|
|
2948
|
-
|
|
2949
|
-
// 需要调用 updatePaneLayout 来调整 pane 顺序
|
|
2950
|
-
console.warn('[Chart] reorderIndicators not fully implemented yet')
|
|
2951
|
-
return false
|
|
990
|
+
return this.indicatorManager.reorderIndicators(orderedInstanceIds)
|
|
2952
991
|
}
|
|
2953
992
|
|
|
2954
993
|
|
|
@@ -3017,43 +1056,6 @@ export class Chart {
|
|
|
3017
1056
|
*/
|
|
3018
1057
|
}
|
|
3019
1058
|
|
|
3020
|
-
// ==================== Type definitions for Facade ====================
|
|
3021
|
-
|
|
3022
|
-
export type ViewportState = {
|
|
3023
|
-
zoomLevel: number
|
|
3024
|
-
plotWidth: number
|
|
3025
|
-
plotHeight: number
|
|
3026
|
-
dpr: number
|
|
3027
|
-
visibleFrom: number
|
|
3028
|
-
visibleTo: number
|
|
3029
|
-
kWidth: number
|
|
3030
|
-
kGap: number
|
|
3031
|
-
}
|
|
3032
1059
|
|
|
3033
|
-
export type IndicatorRole = 'main' | 'sub'
|
|
3034
|
-
|
|
3035
|
-
export interface IndicatorInstance {
|
|
3036
|
-
id: string
|
|
3037
|
-
definitionId: string
|
|
3038
|
-
label: string
|
|
3039
|
-
name: string
|
|
3040
|
-
role: IndicatorRole
|
|
3041
|
-
paneId?: string
|
|
3042
|
-
params: Record<string, unknown>
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
export interface SubPaneInfo {
|
|
3046
|
-
paneId: string
|
|
3047
|
-
indicatorId: string
|
|
3048
|
-
params: Record<string, unknown>
|
|
3049
|
-
ratio: number
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
export type DrawingToolType = 'trendline' | 'horizontal' | 'fib' | 'rectangle' | 'arrow'
|
|
3053
|
-
|
|
3054
|
-
export interface DrawingObject {
|
|
3055
|
-
id: string
|
|
3056
|
-
type: DrawingToolType
|
|
3057
|
-
}
|
|
3058
1060
|
|
|
3059
1061
|
|