@363045841yyt/klinechart-core 0.8.1-alpha.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +30 -4
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +9 -2
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +3 -3
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +6 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +88 -47
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +1 -0
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +1 -0
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +3 -0
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts +3 -0
- package/dist/data-fetchers/tradingview.d.ts.map +1 -0
- package/dist/data-fetchers/tradingview.js +45 -0
- package/dist/data-fetchers/tradingview.js.map +1 -0
- package/dist/engine/chart.d.ts +34 -351
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +246 -1716
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartContext.d.ts +24 -0
- package/dist/engine/chartContext.d.ts.map +1 -0
- package/dist/engine/chartContext.js +19 -0
- package/dist/engine/chartContext.js.map +1 -0
- package/dist/engine/chartTypes.d.ts +77 -0
- package/dist/engine/chartTypes.d.ts.map +1 -0
- package/dist/engine/chartTypes.js +2 -0
- package/dist/engine/chartTypes.js.map +1 -0
- package/dist/engine/controller/interaction.d.ts +1 -0
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +9 -2
- package/dist/engine/controller/interaction.js.map +1 -1
- package/dist/engine/data/chartDataManager.d.ts +102 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +590 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +438 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- package/dist/engine/viewport/viewport.js +1 -1
- package/dist/engine/viewport/viewport.js.map +1 -1
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +49 -13
- package/src/controllers/types.ts +9 -2
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +70 -22
- package/src/data-fetchers/index.ts +1 -0
- package/src/data-fetchers/router.ts +3 -0
- package/src/data-fetchers/tradingview.ts +48 -0
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +260 -2103
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
- package/src/engine/controller/interaction.ts +10 -2
- package/src/engine/data/chartDataManager.ts +691 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +579 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- package/src/engine/renderers/comparisonLine.ts +25 -11
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/engine/viewport/viewport.ts +1 -1
- package/src/plugin/types.ts +1 -0
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -619
|
@@ -90,27 +90,24 @@ export function createMainIndicatorLegendRendererPlugin(options: {
|
|
|
90
90
|
if (!meta.getTitleInfo) continue
|
|
91
91
|
if (!scheduler?.isMainIndicatorActive(meta.name)) continue
|
|
92
92
|
const params = scheduler?.getMainIndicatorParams(meta.name) ?? {}
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
const titleInfo = meta.getTitleInfo(
|
|
95
|
+
klineData,
|
|
96
|
+
targetIndex,
|
|
97
|
+
params as Record<string, number | boolean | string>,
|
|
98
|
+
pluginHost!,
|
|
99
|
+
'main',
|
|
100
|
+
)
|
|
101
|
+
if (!titleInfo) continue
|
|
94
102
|
|
|
95
103
|
rows.push({
|
|
96
104
|
draw: (rowIndex: number) => {
|
|
97
|
-
const titleInfo = getTitleInfo(
|
|
98
|
-
klineData,
|
|
99
|
-
targetIndex,
|
|
100
|
-
params as Record<string, number | boolean | string>,
|
|
101
|
-
pluginHost!,
|
|
102
|
-
'main',
|
|
103
|
-
)
|
|
104
|
-
if (!titleInfo) return
|
|
105
|
-
|
|
106
105
|
let x = legendX
|
|
107
106
|
let y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
|
|
108
|
-
// 指标名称
|
|
109
107
|
overlayCtx.fillStyle = colors.text.primary
|
|
110
108
|
overlayCtx.fillText(titleInfo.name, x, y)
|
|
111
109
|
x += measureTextWidth(overlayCtx, titleInfo.name)
|
|
112
110
|
|
|
113
|
-
// 指标参数
|
|
114
111
|
if (titleInfo.params && titleInfo.params.length > 0) {
|
|
115
112
|
const paramText = `(${titleInfo.params.join(',')})`
|
|
116
113
|
overlayCtx.fillStyle = colors.text.tertiary
|
|
@@ -120,7 +117,6 @@ export function createMainIndicatorLegendRendererPlugin(options: {
|
|
|
120
117
|
x += gap
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
// 指标数值
|
|
124
120
|
if (titleInfo.values) {
|
|
125
121
|
y += 1
|
|
126
122
|
for (const item of titleInfo.values) {
|
|
@@ -134,6 +130,8 @@ export function createMainIndicatorLegendRendererPlugin(options: {
|
|
|
134
130
|
})
|
|
135
131
|
}
|
|
136
132
|
|
|
133
|
+
pushComparisonLegendRows(context, klineData, targetIndex, range, rows, config.yPaddingPx, overlayCtx, legendX, legendYOffset, lineHeight, gap, colors)
|
|
134
|
+
|
|
137
135
|
rows.forEach((row, index) => row.draw(index))
|
|
138
136
|
overlayCtx.restore()
|
|
139
137
|
},
|
|
@@ -151,3 +149,91 @@ export function createMainIndicatorLegendRendererPlugin(options: {
|
|
|
151
149
|
},
|
|
152
150
|
}
|
|
153
151
|
}
|
|
152
|
+
|
|
153
|
+
function pushComparisonLegendRows(
|
|
154
|
+
context: RenderContext,
|
|
155
|
+
klineData: KLineData[],
|
|
156
|
+
targetIndex: number,
|
|
157
|
+
range: { start: number; end: number },
|
|
158
|
+
rows: Array<{ draw: (rowIndex: number) => void }>,
|
|
159
|
+
yPaddingPx: number,
|
|
160
|
+
overlayCtx: CanvasRenderingContext2D,
|
|
161
|
+
legendX: number,
|
|
162
|
+
legendYOffset: number,
|
|
163
|
+
lineHeight: number,
|
|
164
|
+
gap: number,
|
|
165
|
+
colors: ReturnType<typeof resolveThemeColors>,
|
|
166
|
+
): void {
|
|
167
|
+
const comparisonSymbols = context.comparisonSymbols
|
|
168
|
+
const comparisonData = context.comparisonData
|
|
169
|
+
const comparisonColors = context.comparisonColors
|
|
170
|
+
if (!comparisonSymbols?.length || !comparisonData?.size) return
|
|
171
|
+
|
|
172
|
+
const baseIndex = Math.max(0, range.start)
|
|
173
|
+
const baseItem = klineData[baseIndex]
|
|
174
|
+
if (!baseItem || !Number.isFinite(baseItem.close) || baseItem.close <= 0) return
|
|
175
|
+
|
|
176
|
+
const baseDate = baseItem.date ?? ''
|
|
177
|
+
|
|
178
|
+
for (const spec of comparisonSymbols) {
|
|
179
|
+
const data = comparisonData.get(spec.symbol)
|
|
180
|
+
if (!data?.length) continue
|
|
181
|
+
|
|
182
|
+
const baseline = baseDate
|
|
183
|
+
? findBaselineByDate(data, baseDate)
|
|
184
|
+
: findBaselineByTimestamp(data, baseItem.timestamp)
|
|
185
|
+
if (!baseline || baseline.close <= 0) continue
|
|
186
|
+
|
|
187
|
+
const byDate = new Map<string, KLineData>()
|
|
188
|
+
for (const item of data) {
|
|
189
|
+
byDate.set(item.date ?? String(item.timestamp), item)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const mainItem = klineData[targetIndex]
|
|
193
|
+
if (!mainItem) continue
|
|
194
|
+
const key = mainItem.date ?? String(mainItem.timestamp)
|
|
195
|
+
const currentItem = byDate.get(key)
|
|
196
|
+
if (!currentItem || !Number.isFinite(currentItem.close)) continue
|
|
197
|
+
|
|
198
|
+
const pct = ((currentItem.close - baseline.close) / baseline.close) * 100
|
|
199
|
+
const color = comparisonColors?.get(spec.symbol) ?? '#f59e0b'
|
|
200
|
+
|
|
201
|
+
rows.push({
|
|
202
|
+
draw: (rowIndex: number) => {
|
|
203
|
+
let x = legendX
|
|
204
|
+
const y = yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
|
|
205
|
+
|
|
206
|
+
const dotRadius = 4
|
|
207
|
+
overlayCtx.fillStyle = color
|
|
208
|
+
overlayCtx.beginPath()
|
|
209
|
+
const fontSize = lineHeight - 6
|
|
210
|
+
overlayCtx.arc(x + dotRadius, y + fontSize / 2 - 1, dotRadius, 0, Math.PI * 2)
|
|
211
|
+
overlayCtx.fill()
|
|
212
|
+
x += dotRadius * 2 + 4
|
|
213
|
+
|
|
214
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
215
|
+
overlayCtx.fillText(spec.symbol, x, y)
|
|
216
|
+
x += measureTextWidth(overlayCtx, spec.symbol) + gap
|
|
217
|
+
|
|
218
|
+
const sign = pct > 0 ? '+' : ''
|
|
219
|
+
const pctText = `${sign}${pct.toFixed(2)}%`
|
|
220
|
+
overlayCtx.fillStyle = pct > 0 ? colors.candleUpBody : pct < 0 ? colors.candleDownBody : colors.text.primary
|
|
221
|
+
overlayCtx.fillText(pctText, x, y + 1)
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findBaselineByDate(data: ReadonlyArray<KLineData>, date: string): KLineData | null {
|
|
228
|
+
for (const item of data) {
|
|
229
|
+
if (item.date && item.date >= date) return item
|
|
230
|
+
}
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findBaselineByTimestamp(data: ReadonlyArray<KLineData>, timestamp: number): KLineData | null {
|
|
235
|
+
for (const item of data) {
|
|
236
|
+
if (item.timestamp >= timestamp) return item
|
|
237
|
+
}
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { RendererPlugin, RenderContext } from '../../plugin'
|
|
|
2
2
|
import { RENDERER_PRIORITY } from '../../plugin'
|
|
3
3
|
import type { KLineData } from '../../types/price'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_COMPARISON_COLOR = '#f59e0b'
|
|
6
6
|
|
|
7
7
|
export function createComparisonLineRenderer(): RendererPlugin {
|
|
8
8
|
return {
|
|
@@ -21,9 +21,10 @@ export function createComparisonLineRenderer(): RendererPlugin {
|
|
|
21
21
|
if (context.pane.id !== 'main') return
|
|
22
22
|
|
|
23
23
|
const baseIndex = Math.max(0, context.range.start)
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const baseItem = mainData[baseIndex]
|
|
25
|
+
if (!baseItem || !Number.isFinite(baseItem.close) || baseItem.close <= 0) return
|
|
26
|
+
const mainBase = baseItem.close
|
|
27
|
+
const baseDate = baseItem.date ?? ''
|
|
27
28
|
|
|
28
29
|
const ctx = context.ctx
|
|
29
30
|
ctx.save()
|
|
@@ -31,18 +32,23 @@ export function createComparisonLineRenderer(): RendererPlugin {
|
|
|
31
32
|
ctx.lineWidth = Math.max(1, 1.5 / context.dpr)
|
|
32
33
|
|
|
33
34
|
for (let symbolIndex = 0; symbolIndex < comparisonSymbols.length; symbolIndex++) {
|
|
34
|
-
const spec = comparisonSymbols[symbolIndex]
|
|
35
|
+
const spec = comparisonSymbols[symbolIndex]!
|
|
35
36
|
const data = comparisonData.get(spec.symbol)
|
|
36
37
|
if (!data?.length) continue
|
|
37
38
|
|
|
38
|
-
const baseline =
|
|
39
|
+
const baseline = baseDate ? findBaselineByDate(data, baseDate) : findBaselineByTimestamp(data, baseItem.timestamp)
|
|
39
40
|
if (!baseline || baseline.close <= 0) continue
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
for (const item of data)
|
|
42
|
+
const byDate = new Map<string, KLineData>()
|
|
43
|
+
for (const item of data) {
|
|
44
|
+
if (item.date) byDate.set(item.date, item)
|
|
45
|
+
else byDate.set(String(item.timestamp), item)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const colors = context.comparisonColors
|
|
43
49
|
|
|
44
50
|
ctx.beginPath()
|
|
45
|
-
ctx.strokeStyle =
|
|
51
|
+
ctx.strokeStyle = colors?.get(spec.symbol) ?? DEFAULT_COMPARISON_COLOR
|
|
46
52
|
let hasPath = false
|
|
47
53
|
let previousHadPoint = false
|
|
48
54
|
|
|
@@ -52,7 +58,8 @@ export function createComparisonLineRenderer(): RendererPlugin {
|
|
|
52
58
|
previousHadPoint = false
|
|
53
59
|
continue
|
|
54
60
|
}
|
|
55
|
-
const
|
|
61
|
+
const key = mainItem.date ?? String(mainItem.timestamp)
|
|
62
|
+
const item = byDate.get(key)
|
|
56
63
|
const x = context.kLineCenters[i - context.range.start]
|
|
57
64
|
if (!item || x === undefined || !Number.isFinite(item.close)) {
|
|
58
65
|
previousHadPoint = false
|
|
@@ -81,7 +88,14 @@ export function createComparisonLineRenderer(): RendererPlugin {
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
function
|
|
91
|
+
function findBaselineByDate(data: ReadonlyArray<KLineData>, date: string): KLineData | null {
|
|
92
|
+
for (const item of data) {
|
|
93
|
+
if (item.date && item.date >= date) return item
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findBaselineByTimestamp(data: ReadonlyArray<KLineData>, timestamp: number): KLineData | null {
|
|
85
99
|
for (const item of data) {
|
|
86
100
|
if (item.timestamp >= timestamp) return item
|
|
87
101
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { Chart } from './chart'
|
|
2
1
|
import type { SubIndicatorType } from './renderers/Indicator'
|
|
3
2
|
import { createSignal, type Signal } from '../reactivity/signal'
|
|
4
3
|
import { createSubIndicatorRenderer } from './renderers/Indicator'
|
|
5
4
|
import { createPaneTitleRendererPlugin } from './renderers/paneTitle'
|
|
6
5
|
import { createIndicatorScaleRendererPlugin } from './renderers/Indicator/scale/indicator_scale'
|
|
7
6
|
import { findIndicator } from './renderers/Indicator/indicatorCatalog'
|
|
7
|
+
import type { IndicatorScheduler } from './indicators/scheduler'
|
|
8
|
+
import type { RendererPlugin, RendererPluginWithHost } from '../plugin'
|
|
9
|
+
import type { PaneSpec } from './chartTypes'
|
|
8
10
|
|
|
9
11
|
export interface SubPaneEntry {
|
|
10
12
|
paneId: string
|
|
@@ -15,6 +17,23 @@ export interface SubPaneEntry {
|
|
|
15
17
|
paneTitleRendererName: string
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
export interface SubPaneContext {
|
|
21
|
+
getIndicatorScheduler: () => IndicatorScheduler
|
|
22
|
+
hasPane: (paneId: string) => boolean
|
|
23
|
+
upsertPane: (def: PaneSpec) => void
|
|
24
|
+
getRenderer: <T extends RendererPlugin = RendererPlugin>(name: string) => T | undefined
|
|
25
|
+
useRenderer: (plugin: RendererPlugin | RendererPluginWithHost, config?: Record<string, unknown>) => void
|
|
26
|
+
removeRenderer: (name: string) => void
|
|
27
|
+
removePaneDefinition: (paneId: string) => void
|
|
28
|
+
updateRendererConfig: (name: string, config: Record<string, unknown>) => void
|
|
29
|
+
getRightAxisWidth: () => number
|
|
30
|
+
getPriceLabelWidth: () => number
|
|
31
|
+
getYPaddingPx: () => number
|
|
32
|
+
getCrosshairPos: () => { x: number; y: number } | null
|
|
33
|
+
getCrosshairPrice: () => number | null
|
|
34
|
+
getActivePaneId: () => string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
export class SubPaneManager {
|
|
19
38
|
private entries = new Map<string, SubPaneEntry>()
|
|
20
39
|
private _entriesSignal = createSignal<ReadonlyArray<SubPaneEntry>>([])
|
|
@@ -27,82 +46,78 @@ export class SubPaneManager {
|
|
|
27
46
|
this._entriesSignal.set(this.getAll())
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
create(
|
|
49
|
+
create(ctx: SubPaneContext, paneId: string, indicatorId: SubIndicatorType, params: Record<string, unknown>): boolean {
|
|
31
50
|
if (this.entries.has(paneId)) {
|
|
32
51
|
return true
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
const scaleRendererName = `${indicatorId.toLowerCase()}_scale_${paneId}`
|
|
36
55
|
const paneTitleRendererName = `paneTitle_${paneId}`
|
|
37
|
-
const renderer = this.createIndicatorRenderer(
|
|
56
|
+
const renderer = this.createIndicatorRenderer(ctx, paneId, indicatorId, params)
|
|
38
57
|
if (!renderer) return false
|
|
39
58
|
const rendererName = renderer.name
|
|
40
59
|
|
|
41
|
-
const paneExists =
|
|
60
|
+
const paneExists = ctx.hasPane(paneId)
|
|
42
61
|
if (!paneExists) {
|
|
43
|
-
|
|
62
|
+
ctx.upsertPane({ id: paneId, ratio: 1, visible: true, role: 'indicator' })
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
const existingRenderer =
|
|
65
|
+
const existingRenderer = ctx.getRenderer(rendererName)
|
|
47
66
|
if (!existingRenderer) {
|
|
48
|
-
|
|
67
|
+
ctx.useRenderer(renderer, params as Record<string, number | boolean | string>)
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
this.mountScaleRenderer(
|
|
52
|
-
this.mountPaneTitleRenderer(
|
|
70
|
+
this.mountScaleRenderer(ctx, paneId, indicatorId, scaleRendererName)
|
|
71
|
+
this.mountPaneTitleRenderer(ctx, paneId, indicatorId, params)
|
|
53
72
|
|
|
54
|
-
// 必须在 syncSchedulerConfig 之前注册 entry,
|
|
55
|
-
// 否则 scheduler 的 buildActiveConfig 读不到新 paneId,会将新指标的 show* 标志置为 false
|
|
56
73
|
this.entries.set(paneId, { paneId, indicatorId, params, rendererName, scaleRendererName, paneTitleRendererName })
|
|
57
74
|
|
|
58
|
-
this.syncSchedulerConfig(
|
|
75
|
+
this.syncSchedulerConfig(ctx, paneId, indicatorId, params)
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
ctx.getIndicatorScheduler().onSubPaneChanged()
|
|
61
78
|
|
|
62
79
|
this.syncEntriesSignal()
|
|
63
80
|
return true
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
remove(
|
|
83
|
+
remove(ctx: SubPaneContext, paneId: string): void {
|
|
67
84
|
const entry = this.entries.get(paneId)
|
|
68
85
|
if (!entry) return
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
ctx.removeRenderer(entry.rendererName)
|
|
88
|
+
ctx.removeRenderer(entry.scaleRendererName)
|
|
89
|
+
ctx.removeRenderer(entry.paneTitleRendererName)
|
|
73
90
|
|
|
74
91
|
this.entries.delete(paneId)
|
|
75
92
|
|
|
76
|
-
if (
|
|
77
|
-
|
|
93
|
+
if (ctx.hasPane(paneId)) {
|
|
94
|
+
ctx.removePaneDefinition(paneId)
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
ctx.getIndicatorScheduler().onSubPaneChanged()
|
|
81
98
|
this.syncEntriesSignal()
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
replaceIndicator(
|
|
101
|
+
replaceIndicator(ctx: SubPaneContext, paneId: string, newIndicatorId: SubIndicatorType, newParams: Record<string, unknown>): void {
|
|
85
102
|
const entry = this.entries.get(paneId)
|
|
86
103
|
if (!entry) return
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
chart.removeRenderer(entry.scaleRendererName)
|
|
92
|
-
chart.removeRenderer(entry.paneTitleRendererName)
|
|
105
|
+
ctx.removeRenderer(entry.rendererName)
|
|
106
|
+
ctx.removeRenderer(entry.scaleRendererName)
|
|
107
|
+
ctx.removeRenderer(entry.paneTitleRendererName)
|
|
93
108
|
|
|
94
109
|
const newScaleRendererName = `${newIndicatorId.toLowerCase()}_scale_${paneId}`
|
|
95
110
|
const newPaneTitleRendererName = `paneTitle_${paneId}`
|
|
96
|
-
const renderer = this.createIndicatorRenderer(
|
|
111
|
+
const renderer = this.createIndicatorRenderer(ctx, paneId, newIndicatorId, newParams)
|
|
97
112
|
if (!renderer) return
|
|
98
113
|
const newRendererName = renderer.name
|
|
99
114
|
|
|
100
|
-
|
|
115
|
+
ctx.useRenderer(renderer, newParams as Record<string, number | boolean | string>)
|
|
101
116
|
|
|
102
|
-
this.mountScaleRenderer(
|
|
103
|
-
this.mountPaneTitleRenderer(
|
|
117
|
+
this.mountScaleRenderer(ctx, paneId, newIndicatorId, newScaleRendererName)
|
|
118
|
+
this.mountPaneTitleRenderer(ctx, paneId, newIndicatorId, newParams)
|
|
104
119
|
|
|
105
|
-
this.syncSchedulerConfig(
|
|
120
|
+
this.syncSchedulerConfig(ctx, paneId, newIndicatorId, newParams)
|
|
106
121
|
|
|
107
122
|
this.entries.set(paneId, {
|
|
108
123
|
paneId,
|
|
@@ -113,19 +128,20 @@ export class SubPaneManager {
|
|
|
113
128
|
paneTitleRendererName: newPaneTitleRendererName,
|
|
114
129
|
})
|
|
115
130
|
|
|
116
|
-
|
|
131
|
+
ctx.getIndicatorScheduler().onSubPaneChanged()
|
|
117
132
|
this.syncEntriesSignal()
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
updateParams(
|
|
135
|
+
updateParams(ctx: SubPaneContext, paneId: string, params: Record<string, unknown>): void {
|
|
121
136
|
const entry = this.entries.get(paneId)
|
|
122
137
|
if (!entry) return
|
|
123
138
|
|
|
124
139
|
entry.params = { ...params }
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
ctx.updateRendererConfig(entry.rendererName, params)
|
|
142
|
+
ctx.updateRendererConfig(entry.paneTitleRendererName, { params: entry.params, indicatorId: entry.indicatorId })
|
|
127
143
|
|
|
128
|
-
this.syncSchedulerConfig(
|
|
144
|
+
this.syncSchedulerConfig(ctx, paneId, entry.indicatorId, entry.params)
|
|
129
145
|
this.syncEntriesSignal()
|
|
130
146
|
}
|
|
131
147
|
|
|
@@ -134,12 +150,12 @@ export class SubPaneManager {
|
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
private createIndicatorRenderer(
|
|
137
|
-
|
|
153
|
+
ctx: SubPaneContext,
|
|
138
154
|
paneId: string,
|
|
139
155
|
indicatorId: SubIndicatorType,
|
|
140
156
|
params: Record<string, unknown>,
|
|
141
|
-
):
|
|
142
|
-
const definition =
|
|
157
|
+
): RendererPlugin {
|
|
158
|
+
const definition = ctx.getIndicatorScheduler().getIndicatorMetadata(indicatorId)
|
|
143
159
|
if (!definition) {
|
|
144
160
|
throw new Error(`[SubPaneManager] Unknown indicator: ${indicatorId}`)
|
|
145
161
|
}
|
|
@@ -154,38 +170,38 @@ export class SubPaneManager {
|
|
|
154
170
|
return Array.from(this.entries.keys())
|
|
155
171
|
}
|
|
156
172
|
|
|
157
|
-
clear(
|
|
173
|
+
clear(ctx: SubPaneContext): void {
|
|
158
174
|
for (const entry of this.entries.values()) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
ctx.removeRenderer(entry.rendererName)
|
|
176
|
+
ctx.removeRenderer(entry.scaleRendererName)
|
|
177
|
+
ctx.removeRenderer(entry.paneTitleRendererName)
|
|
162
178
|
}
|
|
163
179
|
this.entries.clear()
|
|
164
|
-
|
|
180
|
+
ctx.getIndicatorScheduler().onSubPaneChanged()
|
|
165
181
|
this.syncEntriesSignal()
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
private syncSchedulerConfig(
|
|
169
|
-
|
|
185
|
+
ctx: SubPaneContext,
|
|
170
186
|
paneId: string,
|
|
171
187
|
indicatorId: SubIndicatorType,
|
|
172
188
|
params: Record<string, unknown>,
|
|
173
189
|
): void {
|
|
174
|
-
const scheduler =
|
|
190
|
+
const scheduler = ctx.getIndicatorScheduler()
|
|
175
191
|
const definition = scheduler.getIndicatorMetadata(indicatorId)
|
|
176
192
|
definition?.updateConfig?.(scheduler, params, paneId)
|
|
177
193
|
}
|
|
178
194
|
|
|
179
|
-
private mountScaleRenderer(
|
|
180
|
-
const existing =
|
|
195
|
+
private mountScaleRenderer(ctx: SubPaneContext, paneId: string, indicatorId: SubIndicatorType, scaleRendererName: string): void {
|
|
196
|
+
const existing = ctx.getRenderer(scaleRendererName)
|
|
181
197
|
if (existing) return
|
|
182
198
|
|
|
183
|
-
const axisWidth =
|
|
184
|
-
const yPaddingPx =
|
|
199
|
+
const axisWidth = ctx.getRightAxisWidth() + (ctx.getPriceLabelWidth() ?? 60)
|
|
200
|
+
const yPaddingPx = ctx.getYPaddingPx()
|
|
185
201
|
const getCrosshair = () => {
|
|
186
|
-
const pos =
|
|
187
|
-
const price =
|
|
188
|
-
const activePaneId =
|
|
202
|
+
const pos = ctx.getCrosshairPos()
|
|
203
|
+
const price = ctx.getCrosshairPrice()
|
|
204
|
+
const activePaneId = ctx.getActivePaneId()
|
|
189
205
|
if (pos && price !== null) {
|
|
190
206
|
return { y: pos.y, price, activePaneId }
|
|
191
207
|
}
|
|
@@ -194,14 +210,14 @@ export class SubPaneManager {
|
|
|
194
210
|
|
|
195
211
|
const opts = { axisWidth, paneId, yPaddingPx, getCrosshair }
|
|
196
212
|
|
|
197
|
-
const definition =
|
|
213
|
+
const definition = ctx.getIndicatorScheduler().getIndicatorMetadata(indicatorId)
|
|
198
214
|
if (definition?.scaleRendererFactory) {
|
|
199
|
-
|
|
215
|
+
ctx.useRenderer(definition.scaleRendererFactory({ ...opts, indicatorId }))
|
|
200
216
|
return
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
if (definition?.scale) {
|
|
204
|
-
|
|
220
|
+
ctx.useRenderer(createIndicatorScaleRendererPlugin({
|
|
205
221
|
...opts,
|
|
206
222
|
indicatorKey: definition.scale.indicatorKey ?? definition.name,
|
|
207
223
|
label: definition.scale.label ?? definition.displayName,
|
|
@@ -211,11 +227,11 @@ export class SubPaneManager {
|
|
|
211
227
|
}
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
private mountPaneTitleRenderer(
|
|
230
|
+
private mountPaneTitleRenderer(ctx: SubPaneContext, paneId: string, indicatorId: SubIndicatorType, params: Record<string, unknown>): void {
|
|
215
231
|
const rendererName = `paneTitle_${paneId}`
|
|
216
|
-
const existing =
|
|
232
|
+
const existing = ctx.getRenderer(rendererName)
|
|
217
233
|
if (existing) {
|
|
218
|
-
|
|
234
|
+
ctx.updateRendererConfig(rendererName, { params, indicatorId })
|
|
219
235
|
return
|
|
220
236
|
}
|
|
221
237
|
|
|
@@ -225,6 +241,6 @@ export class SubPaneManager {
|
|
|
225
241
|
indicatorId,
|
|
226
242
|
params,
|
|
227
243
|
})
|
|
228
|
-
|
|
244
|
+
ctx.useRenderer(renderer)
|
|
229
245
|
}
|
|
230
246
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { computeZoom } from './zoom'
|
|
2
|
+
|
|
3
|
+
export interface ZoomCommittedResult {
|
|
4
|
+
kWidth: number
|
|
5
|
+
kGap: number
|
|
6
|
+
zoomLevel: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ZoomDependencies {
|
|
10
|
+
getLogicalScrollLeft: () => number
|
|
11
|
+
getCurrentDpr: () => number
|
|
12
|
+
getLeftLoadBufferWidth: () => number
|
|
13
|
+
setScrollLeft: (v: number) => void
|
|
14
|
+
onZoomCommitted: (result: ZoomCommittedResult) => void
|
|
15
|
+
getKWidth: () => number
|
|
16
|
+
getKGap: () => number
|
|
17
|
+
getMinKWidth: () => number
|
|
18
|
+
getMaxKWidth: () => number
|
|
19
|
+
zoomLevelCount: number
|
|
20
|
+
initialZoomLevel: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ChartZoomController {
|
|
24
|
+
private _currentZoomLevel: number
|
|
25
|
+
private readonly deps: ZoomDependencies
|
|
26
|
+
|
|
27
|
+
constructor(deps: ZoomDependencies) {
|
|
28
|
+
this.deps = deps
|
|
29
|
+
const clamped = Math.max(1, Math.min(deps.zoomLevelCount, deps.initialZoomLevel ?? 1))
|
|
30
|
+
this._currentZoomLevel = clamped
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get currentZoomLevel(): number {
|
|
34
|
+
return this._currentZoomLevel
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get zoomLevelCount(): number {
|
|
38
|
+
return this.deps.zoomLevelCount
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setZoomLevel(level: number): void {
|
|
42
|
+
this._currentZoomLevel = Math.max(1, Math.min(this.deps.zoomLevelCount, level))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
zoomToLevel(level: number, anchorX?: number): void {
|
|
46
|
+
const clamped = Math.max(1, Math.min(this.deps.zoomLevelCount, Math.round(level)))
|
|
47
|
+
this.applyZoom(clamped, anchorX)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
zoomIn(anchorX?: number): void {
|
|
51
|
+
this.zoomToLevel(this._currentZoomLevel + 1, anchorX)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
zoomOut(anchorX?: number): void {
|
|
55
|
+
this.zoomToLevel(this._currentZoomLevel - 1, anchorX)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleWheel(deltaY: number, viewportX: number): void {
|
|
59
|
+
const delta = deltaY > 0 ? -1 : 1
|
|
60
|
+
const targetLevel = Math.max(1, Math.min(this.deps.zoomLevelCount, this._currentZoomLevel + delta))
|
|
61
|
+
if (targetLevel === this._currentZoomLevel) return
|
|
62
|
+
this.applyZoom(targetLevel, viewportX)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handlePinch(delta: number, centerClientX: number): void {
|
|
66
|
+
const targetLevel = Math.max(1, Math.min(this.deps.zoomLevelCount, this._currentZoomLevel + delta))
|
|
67
|
+
if (targetLevel === this._currentZoomLevel) return
|
|
68
|
+
this.applyZoom(targetLevel, centerClientX)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private applyZoom(targetLevel: number, anchorViewportX?: number): void {
|
|
72
|
+
if (targetLevel === this._currentZoomLevel) return
|
|
73
|
+
|
|
74
|
+
const delta = targetLevel - this._currentZoomLevel
|
|
75
|
+
const logicalScrollLeft = this.deps.getLogicalScrollLeft()
|
|
76
|
+
const dpr = this.deps.getCurrentDpr()
|
|
77
|
+
|
|
78
|
+
const result = computeZoom(
|
|
79
|
+
delta,
|
|
80
|
+
anchorViewportX ?? 0,
|
|
81
|
+
logicalScrollLeft,
|
|
82
|
+
this._currentZoomLevel,
|
|
83
|
+
this.deps.getKWidth(),
|
|
84
|
+
this.deps.getKGap(),
|
|
85
|
+
{
|
|
86
|
+
minKWidth: this.deps.getMinKWidth(),
|
|
87
|
+
maxKWidth: this.deps.getMaxKWidth(),
|
|
88
|
+
zoomLevelCount: this.deps.zoomLevelCount,
|
|
89
|
+
dpr,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (!result) return
|
|
94
|
+
|
|
95
|
+
const domScrollLeft = result.newScrollLeft + this.deps.getLeftLoadBufferWidth()
|
|
96
|
+
this._currentZoomLevel = result.targetLevel
|
|
97
|
+
this.deps.setScrollLeft(domScrollLeft)
|
|
98
|
+
this.deps.onZoomCommitted({
|
|
99
|
+
kWidth: result.newKWidth,
|
|
100
|
+
kGap: result.newKGap,
|
|
101
|
+
zoomLevel: result.targetLevel,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|