@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { ChartIndicatorManager, type IndicatorDependencies } from '../chartIndicatorManager'
|
|
3
|
+
import { createPluginHost } from '../../../plugin/PluginHost'
|
|
4
|
+
import { createSignal } from '../../../reactivity/signal'
|
|
5
|
+
import type { VisibleRange } from '../../layout/pane'
|
|
6
|
+
import { UpdateLevel } from '../../layout/pane'
|
|
7
|
+
|
|
8
|
+
function createMockDeps() {
|
|
9
|
+
const rendererMap = new Map<string, any>()
|
|
10
|
+
const paneRatiosSignal = createSignal<Readonly<Record<string, number>>>({})
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
rendererMap,
|
|
14
|
+
getOption: () => ({
|
|
15
|
+
rightAxisWidth: 60,
|
|
16
|
+
priceLabelWidth: 60,
|
|
17
|
+
yPaddingPx: 4,
|
|
18
|
+
paneGap: 1,
|
|
19
|
+
defaultPaneMinHeightPx: 40,
|
|
20
|
+
panes: [],
|
|
21
|
+
bottomAxisHeight: 20,
|
|
22
|
+
kWidth: 8,
|
|
23
|
+
kGap: 2,
|
|
24
|
+
minKWidth: 4,
|
|
25
|
+
maxKWidth: 16,
|
|
26
|
+
}),
|
|
27
|
+
getPluginHost: () => createPluginHost(),
|
|
28
|
+
getRenderer: vi.fn((name: string) => rendererMap.get(name)),
|
|
29
|
+
useRenderer: vi.fn((plugin: any, _config?: any) => {
|
|
30
|
+
if (plugin?.name) rendererMap.set(plugin.name, plugin)
|
|
31
|
+
}),
|
|
32
|
+
removeRenderer: vi.fn((name: string) => {
|
|
33
|
+
rendererMap.delete(name)
|
|
34
|
+
}),
|
|
35
|
+
updateRendererConfig: vi.fn(),
|
|
36
|
+
setRendererEnabled: vi.fn(),
|
|
37
|
+
hasPane: vi.fn(() => false),
|
|
38
|
+
upsertPane: vi.fn(),
|
|
39
|
+
removePaneDefinition: vi.fn(),
|
|
40
|
+
getPaneSpecs: vi.fn(() => []),
|
|
41
|
+
getPaneRatiosSignal: () => paneRatiosSignal,
|
|
42
|
+
getInternalPaneRatios: vi.fn(() => new Map()),
|
|
43
|
+
setInternalPaneRatio: vi.fn(),
|
|
44
|
+
deleteInternalPaneRatio: vi.fn(),
|
|
45
|
+
applyPaneLayoutSpecs: vi.fn(),
|
|
46
|
+
getLastVisibleRange: vi.fn(() => ({ start: 0, end: 0 }) as VisibleRange),
|
|
47
|
+
getCrosshairPos: vi.fn(() => null),
|
|
48
|
+
getCrosshairPrice: vi.fn(() => null),
|
|
49
|
+
getActivePaneId: vi.fn(() => null),
|
|
50
|
+
scheduleDraw: vi.fn(),
|
|
51
|
+
setPendingIndicatorDataUpdate: vi.fn(),
|
|
52
|
+
} as IndicatorDependencies & { rendererMap: Map<string, any> }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('ChartIndicatorManager', () => {
|
|
56
|
+
let manager: ChartIndicatorManager
|
|
57
|
+
let deps: ReturnType<typeof createMockDeps>
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
deps = createMockDeps()
|
|
61
|
+
manager = new ChartIndicatorManager(deps)
|
|
62
|
+
vi.clearAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('updateMainIndicatorParams', () => {
|
|
66
|
+
it('should call renderer setConfig with merged params', () => {
|
|
67
|
+
manager.enableMainIndicator('MA')
|
|
68
|
+
|
|
69
|
+
const maRenderer = deps.rendererMap.get('ma')
|
|
70
|
+
const setConfigSpy = vi.spyOn(maRenderer, 'setConfig')
|
|
71
|
+
|
|
72
|
+
manager.updateMainIndicatorParams('MA', { ma5: false })
|
|
73
|
+
|
|
74
|
+
expect(setConfigSpy).toHaveBeenCalledTimes(1)
|
|
75
|
+
expect(setConfigSpy).toHaveBeenCalledWith({ ma5: false, ma10: true, ma20: true, ma30: true, ma60: true })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should merge params instead of replacing', () => {
|
|
79
|
+
manager.enableMainIndicator('MA')
|
|
80
|
+
|
|
81
|
+
manager.updateMainIndicatorParams('MA', { ma5: false })
|
|
82
|
+
|
|
83
|
+
const params = manager.getMainIndicatorParams('MA')
|
|
84
|
+
expect(params).toEqual({ ma5: false, ma10: true, ma20: true, ma30: true, ma60: true })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should schedule a redraw after params update', () => {
|
|
88
|
+
manager.enableMainIndicator('MA')
|
|
89
|
+
vi.clearAllMocks()
|
|
90
|
+
|
|
91
|
+
manager.updateMainIndicatorParams('MA', { ma5: false })
|
|
92
|
+
|
|
93
|
+
expect(deps.scheduleDraw).toHaveBeenCalledTimes(1)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should be no-op when indicator is not active', () => {
|
|
97
|
+
manager.updateMainIndicatorParams('MA', { ma5: false })
|
|
98
|
+
|
|
99
|
+
expect(deps.scheduleDraw).not.toHaveBeenCalled()
|
|
100
|
+
expect(manager.getMainIndicatorParams('MA')).toBeNull()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import type { KLineData } from '../../types/price'
|
|
2
|
+
import { createSignal, computed, type Signal, type Computed } from '../../reactivity/signal'
|
|
3
|
+
import type { IndicatorInstance, SubPaneInfo, PaneSpec, ChartOptions } from '../chartTypes'
|
|
4
|
+
import type { VisibleRange } from '../layout/pane'
|
|
5
|
+
import { UpdateLevel } from '../layout/pane'
|
|
6
|
+
import { IndicatorScheduler } from './scheduler'
|
|
7
|
+
import { getBuiltinIndicatorDefinitions } from './registerBuiltins'
|
|
8
|
+
import { getRegisteredIndicatorDefinitions } from './indicatorDefinitionRegistry'
|
|
9
|
+
import { SubPaneManager, type SubPaneEntry, type SubPaneContext } from '../subPaneManager'
|
|
10
|
+
import { createSubIndicatorRenderer, type SubIndicatorType } from '../renderers/Indicator'
|
|
11
|
+
import { createMainIndicatorLegendRendererPlugin } from '../renderers/Indicator/mainIndicatorLegend'
|
|
12
|
+
import type {
|
|
13
|
+
PluginHostImpl,
|
|
14
|
+
RendererPlugin,
|
|
15
|
+
RendererPluginWithHost,
|
|
16
|
+
} from '../../plugin'
|
|
17
|
+
|
|
18
|
+
type ResolvedChartOptions = Omit<ChartOptions, 'kWidth' | 'kGap'> & {
|
|
19
|
+
kWidth: number
|
|
20
|
+
kGap: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 主图指标条目,存在 = 激活 */
|
|
24
|
+
interface MainIndicatorEntry {
|
|
25
|
+
params: Record<string, number | boolean | string>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IndicatorDependencies {
|
|
29
|
+
getOption: () => ResolvedChartOptions
|
|
30
|
+
getPluginHost: () => PluginHostImpl
|
|
31
|
+
getRenderer: <T extends RendererPlugin = RendererPlugin>(name: string) => T | undefined
|
|
32
|
+
useRenderer: (plugin: RendererPlugin | RendererPluginWithHost, config?: Record<string, unknown>) => void
|
|
33
|
+
removeRenderer: (name: string) => void
|
|
34
|
+
updateRendererConfig: (name: string, config: Record<string, unknown>) => void
|
|
35
|
+
setRendererEnabled: (name: string, enabled: boolean) => void
|
|
36
|
+
hasPane: (paneId: string) => boolean
|
|
37
|
+
upsertPane: (def: PaneSpec) => void
|
|
38
|
+
removePaneDefinition: (paneId: string) => void
|
|
39
|
+
getPaneSpecs: () => PaneSpec[]
|
|
40
|
+
getPaneRatiosSignal: () => Signal<Readonly<Record<string, number>>>
|
|
41
|
+
getInternalPaneRatios: () => Map<string, number>
|
|
42
|
+
setInternalPaneRatio: (paneId: string, ratio: number) => void
|
|
43
|
+
deleteInternalPaneRatio: (paneId: string) => void
|
|
44
|
+
applyPaneLayoutSpecs: (specs: PaneSpec[]) => void
|
|
45
|
+
getLastVisibleRange: () => VisibleRange
|
|
46
|
+
getCrosshairPos: () => { x: number; y: number } | null
|
|
47
|
+
getCrosshairPrice: () => number | null
|
|
48
|
+
getActivePaneId: () => string | null
|
|
49
|
+
scheduleDraw: (level?: UpdateLevel) => void
|
|
50
|
+
setPendingIndicatorDataUpdate: (v: boolean) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ChartIndicatorManager {
|
|
54
|
+
private deps: IndicatorDependencies
|
|
55
|
+
private indicatorScheduler: IndicatorScheduler
|
|
56
|
+
private subPaneManager: SubPaneManager
|
|
57
|
+
private _mainIndicatorsSignal: Signal<Map<string, MainIndicatorEntry>>
|
|
58
|
+
private _indicatorsComputed: Computed<ReadonlyArray<IndicatorInstance>>
|
|
59
|
+
private _subPanesComputed: Computed<ReadonlyArray<SubPaneInfo>>
|
|
60
|
+
private subPaneCtx: SubPaneContext
|
|
61
|
+
|
|
62
|
+
/** 主图指标默认参数(从注册表中懒加载) */
|
|
63
|
+
private static _defaultMainParamsCache: Record<string, Record<string, number | boolean | string>> | null = null
|
|
64
|
+
|
|
65
|
+
private static get DEFAULT_MAIN_PARAMS(): Record<string, Record<string, number | boolean | string>> {
|
|
66
|
+
if (ChartIndicatorManager._defaultMainParamsCache === null) {
|
|
67
|
+
ChartIndicatorManager._defaultMainParamsCache = {}
|
|
68
|
+
for (const def of getRegisteredIndicatorDefinitions()) {
|
|
69
|
+
if (def.category === 'main') {
|
|
70
|
+
ChartIndicatorManager._defaultMainParamsCache[def.displayName.toUpperCase()] = (def.runtime?.defaultConfig ?? {}) as Record<string, number | boolean | string>
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return ChartIndicatorManager._defaultMainParamsCache
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 可启用的主图指标白名单(从注册表中懒加载) */
|
|
78
|
+
private static _enableMainIndicatorsCache: string[] | null = null
|
|
79
|
+
|
|
80
|
+
private static get ENABLE_MAIN_INDICATORS(): string[] {
|
|
81
|
+
if (ChartIndicatorManager._enableMainIndicatorsCache === null) {
|
|
82
|
+
ChartIndicatorManager._enableMainIndicatorsCache = getRegisteredIndicatorDefinitions()
|
|
83
|
+
.filter(d => d.category === 'main')
|
|
84
|
+
.map(d => d.displayName.toUpperCase())
|
|
85
|
+
}
|
|
86
|
+
return ChartIndicatorManager._enableMainIndicatorsCache
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 副图渲染器名称前缀(保留向后兼容) */
|
|
90
|
+
static readonly SUB_PANE_PREFIX = 'sub_'
|
|
91
|
+
|
|
92
|
+
constructor(deps: IndicatorDependencies) {
|
|
93
|
+
this.deps = deps
|
|
94
|
+
|
|
95
|
+
// 初始化指标调度器
|
|
96
|
+
this.indicatorScheduler = new IndicatorScheduler()
|
|
97
|
+
this.indicatorScheduler.setPluginHost(deps.getPluginHost())
|
|
98
|
+
for (const definition of getBuiltinIndicatorDefinitions()) {
|
|
99
|
+
this.indicatorScheduler.registerIndicator(definition)
|
|
100
|
+
}
|
|
101
|
+
this.indicatorScheduler.setInvalidateCallback(() => {
|
|
102
|
+
deps.setPendingIndicatorDataUpdate(false)
|
|
103
|
+
deps.scheduleDraw()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// 初始化副图管理器
|
|
107
|
+
this.subPaneManager = new SubPaneManager()
|
|
108
|
+
this.subPaneCtx = this.createSubPaneContext()
|
|
109
|
+
|
|
110
|
+
// 注册副图活跃列表提供者
|
|
111
|
+
this.indicatorScheduler.setActiveSubPaneProvider(
|
|
112
|
+
() => this.subPaneManager.getPaneIds(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// 初始化主图指标信号
|
|
116
|
+
this._mainIndicatorsSignal = createSignal<Map<string, MainIndicatorEntry>>(new Map())
|
|
117
|
+
|
|
118
|
+
// 派生信号
|
|
119
|
+
const mainSignal = this._mainIndicatorsSignal
|
|
120
|
+
const subPaneManager = this.subPaneManager
|
|
121
|
+
this._indicatorsComputed = computed<ReadonlyArray<IndicatorInstance>>(() => {
|
|
122
|
+
const mainIndicators: IndicatorInstance[] = [...mainSignal().entries()].map(([id, entry]) => ({
|
|
123
|
+
id,
|
|
124
|
+
definitionId: id,
|
|
125
|
+
label: id,
|
|
126
|
+
name: id,
|
|
127
|
+
role: 'main' as const,
|
|
128
|
+
params: { ...entry.params },
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
const subIndicators: IndicatorInstance[] = subPaneManager.entriesSignal().map(entry => ({
|
|
132
|
+
id: entry.paneId,
|
|
133
|
+
definitionId: entry.indicatorId,
|
|
134
|
+
label: entry.indicatorId,
|
|
135
|
+
name: entry.indicatorId,
|
|
136
|
+
role: 'sub' as const,
|
|
137
|
+
paneId: entry.paneId,
|
|
138
|
+
params: { ...entry.params },
|
|
139
|
+
}))
|
|
140
|
+
|
|
141
|
+
return [...mainIndicators, ...subIndicators]
|
|
142
|
+
})
|
|
143
|
+
this._subPanesComputed = computed<ReadonlyArray<SubPaneInfo>>(() => {
|
|
144
|
+
const ratios = deps.getPaneRatiosSignal()()
|
|
145
|
+
return subPaneManager.entriesSignal().map(entry => ({
|
|
146
|
+
paneId: entry.paneId,
|
|
147
|
+
indicatorId: entry.indicatorId,
|
|
148
|
+
params: { ...entry.params },
|
|
149
|
+
ratio: ratios[entry.paneId] ?? 1,
|
|
150
|
+
}))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// dev: 主副图状态变更日志
|
|
154
|
+
if ((import.meta as any).env?.MODE !== 'production') {
|
|
155
|
+
this._indicatorsComputed.subscribe(() => {
|
|
156
|
+
const instances = this._indicatorsComputed.peek()
|
|
157
|
+
console.log('[Chart] indicators signal changed:', instances)
|
|
158
|
+
})
|
|
159
|
+
this._subPanesComputed.subscribe(() => {
|
|
160
|
+
const subPanes = this._subPanesComputed.peek()
|
|
161
|
+
console.log('[Chart] subPanes signal changed:', subPanes)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get indicatorSchedulerAccessor(): IndicatorScheduler {
|
|
167
|
+
return this.indicatorScheduler
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get subPaneManagerAccessor(): SubPaneManager {
|
|
171
|
+
return this.subPaneManager
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get mainIndicatorsSignalPeek(): Map<string, MainIndicatorEntry> {
|
|
175
|
+
return this._mainIndicatorsSignal.peek()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get indicatorsComputed(): Computed<ReadonlyArray<IndicatorInstance>> {
|
|
179
|
+
return this._indicatorsComputed
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get subPanesComputed(): Computed<ReadonlyArray<SubPaneInfo>> {
|
|
183
|
+
return this._subPanesComputed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ========== SubPaneContext factory ==========
|
|
187
|
+
|
|
188
|
+
private createSubPaneContext(): SubPaneContext {
|
|
189
|
+
const deps = this.deps
|
|
190
|
+
const self = this
|
|
191
|
+
return {
|
|
192
|
+
getIndicatorScheduler: () => self.indicatorScheduler,
|
|
193
|
+
hasPane: (paneId) => deps.hasPane(paneId),
|
|
194
|
+
upsertPane: (def) => deps.upsertPane(def),
|
|
195
|
+
getRenderer: <T extends RendererPlugin = RendererPlugin>(name: string) => deps.getRenderer<T>(name),
|
|
196
|
+
useRenderer: (plugin, config) => deps.useRenderer(plugin, config),
|
|
197
|
+
removeRenderer: (name) => deps.removeRenderer(name),
|
|
198
|
+
removePaneDefinition: (paneId) => deps.removePaneDefinition(paneId),
|
|
199
|
+
updateRendererConfig: (name, config) => deps.updateRendererConfig(name, config),
|
|
200
|
+
getRightAxisWidth: () => deps.getOption().rightAxisWidth,
|
|
201
|
+
getPriceLabelWidth: () => deps.getOption().priceLabelWidth ?? 60,
|
|
202
|
+
getYPaddingPx: () => deps.getOption().yPaddingPx,
|
|
203
|
+
getCrosshairPos: () => deps.getCrosshairPos(),
|
|
204
|
+
getCrosshairPrice: () => deps.getCrosshairPrice(),
|
|
205
|
+
getActivePaneId: () => deps.getActivePaneId(),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ========== 主图指标 API ==========
|
|
210
|
+
|
|
211
|
+
enableMainIndicator(indicatorId: string, params?: Record<string, number | boolean | string>): boolean {
|
|
212
|
+
const id = indicatorId.toUpperCase()
|
|
213
|
+
if (!ChartIndicatorManager.ENABLE_MAIN_INDICATORS.includes(id)) {
|
|
214
|
+
console.warn(`[Chart] 未知的主图指标: ${indicatorId}`)
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const map = this._mainIndicatorsSignal.peek()
|
|
219
|
+
const existing = map.get(id)
|
|
220
|
+
|
|
221
|
+
if (existing) {
|
|
222
|
+
if (params) {
|
|
223
|
+
const next = new Map(map)
|
|
224
|
+
next.set(id, { params: { ...existing.params, ...params } })
|
|
225
|
+
this._mainIndicatorsSignal.set(next)
|
|
226
|
+
this.updateIndicatorSchedulerConfig(id)
|
|
227
|
+
}
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const defaults = ChartIndicatorManager.DEFAULT_MAIN_PARAMS[id] ?? {}
|
|
232
|
+
const merged = params ? { ...defaults, ...params } : defaults
|
|
233
|
+
const next = new Map(map)
|
|
234
|
+
next.set(id, { params: merged })
|
|
235
|
+
this._mainIndicatorsSignal.set(next)
|
|
236
|
+
|
|
237
|
+
this.enableMainIndicatorRenderer(id)
|
|
238
|
+
|
|
239
|
+
this.updateIndicatorSchedulerConfig(id)
|
|
240
|
+
|
|
241
|
+
this.indicatorScheduler.updateVisibleRange(this.deps.getLastVisibleRange())
|
|
242
|
+
|
|
243
|
+
this.deps.scheduleDraw()
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
disableMainIndicator(indicatorId: string): boolean {
|
|
248
|
+
const id = indicatorId.toUpperCase()
|
|
249
|
+
const map = this._mainIndicatorsSignal.peek()
|
|
250
|
+
if (!map.has(id)) return false
|
|
251
|
+
|
|
252
|
+
const next = new Map(map)
|
|
253
|
+
next.delete(id)
|
|
254
|
+
this._mainIndicatorsSignal.set(next)
|
|
255
|
+
|
|
256
|
+
this.disableMainIndicatorRenderer(id)
|
|
257
|
+
|
|
258
|
+
this.updateIndicatorSchedulerConfig(id)
|
|
259
|
+
|
|
260
|
+
this.deps.scheduleDraw()
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
toggleMainIndicator(indicatorId: string, enabled: boolean): void {
|
|
265
|
+
if (enabled) {
|
|
266
|
+
this.enableMainIndicator(indicatorId)
|
|
267
|
+
} else {
|
|
268
|
+
this.disableMainIndicator(indicatorId)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getActiveMainIndicators(): string[] {
|
|
273
|
+
return [...this._mainIndicatorsSignal.peek().keys()]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
isMainIndicatorActive(indicatorId: string): boolean {
|
|
277
|
+
return this._mainIndicatorsSignal.peek().has(indicatorId.toUpperCase())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
updateMainIndicatorParams(indicatorId: string, params: Record<string, number | boolean | string>): void {
|
|
281
|
+
const id = indicatorId.toUpperCase()
|
|
282
|
+
const map = this._mainIndicatorsSignal.peek()
|
|
283
|
+
const entry = map.get(id)
|
|
284
|
+
if (!entry) return
|
|
285
|
+
|
|
286
|
+
const merged = { ...entry.params, ...params }
|
|
287
|
+
const next = new Map(map)
|
|
288
|
+
next.set(id, { params: merged })
|
|
289
|
+
this._mainIndicatorsSignal.set(next)
|
|
290
|
+
|
|
291
|
+
const rendererName = id.toLowerCase()
|
|
292
|
+
const renderer = this.deps.getRenderer(rendererName)
|
|
293
|
+
if (renderer && (renderer as any).setConfig) {
|
|
294
|
+
;(renderer as any).setConfig(merged)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.updateIndicatorSchedulerConfig(id)
|
|
298
|
+
this.deps.scheduleDraw()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getMainIndicatorParams(indicatorId: string): Record<string, number | boolean | string> | null {
|
|
302
|
+
return this._mainIndicatorsSignal.peek().get(indicatorId.toUpperCase())?.params ?? null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
clearMainIndicators(): void {
|
|
306
|
+
const map = this._mainIndicatorsSignal.peek()
|
|
307
|
+
for (const id of map.keys()) {
|
|
308
|
+
this.disableMainIndicatorRenderer(id)
|
|
309
|
+
}
|
|
310
|
+
this._mainIndicatorsSignal.set(new Map())
|
|
311
|
+
this.deps.scheduleDraw()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private enableMainIndicatorRenderer(indicatorId: string): void {
|
|
315
|
+
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
316
|
+
const mainPane = definition?.mainPane
|
|
317
|
+
if (!definition || !mainPane) return
|
|
318
|
+
|
|
319
|
+
if (!this.deps.getRenderer(mainPane.rendererName)) {
|
|
320
|
+
this.deps.useRenderer(definition.rendererFactory({ paneId: 'main', indicatorId }))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.deps.setRendererEnabled(mainPane.rendererName, true)
|
|
324
|
+
|
|
325
|
+
if (!this.deps.getRenderer('mainIndicatorLegend')) {
|
|
326
|
+
this.deps.useRenderer(createMainIndicatorLegendRendererPlugin({ yPaddingPx: this.deps.getOption().yPaddingPx }))
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private disableMainIndicatorRenderer(indicatorId: string): void {
|
|
331
|
+
const rendererName = this.indicatorScheduler.getIndicatorMetadata(indicatorId)?.mainPane?.rendererName
|
|
332
|
+
if (rendererName) {
|
|
333
|
+
this.deps.setRendererEnabled(rendererName, false)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private updateIndicatorSchedulerConfig(indicatorId: string): void {
|
|
338
|
+
const entry = this._mainIndicatorsSignal.peek().get(indicatorId)
|
|
339
|
+
const isActive = entry !== undefined
|
|
340
|
+
const params = entry?.params ?? {}
|
|
341
|
+
|
|
342
|
+
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
343
|
+
const toActiveConfig = definition?.mainPane?.toActiveConfig
|
|
344
|
+
if (!definition?.updateConfig || !toActiveConfig) return
|
|
345
|
+
|
|
346
|
+
const config = toActiveConfig(params, isActive)
|
|
347
|
+
if (config !== null) {
|
|
348
|
+
definition.updateConfig(this.indicatorScheduler, config, 'main')
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @deprecated 使用 enableMainIndicator/disableMainIndicator 替代
|
|
354
|
+
*/
|
|
355
|
+
setActiveMainIndicators(indicators: string[]): void {
|
|
356
|
+
const newSet = new Set(indicators.map(i => i.toUpperCase()))
|
|
357
|
+
const currentSet = new Set(this._mainIndicatorsSignal.peek().keys())
|
|
358
|
+
|
|
359
|
+
for (const id of currentSet) {
|
|
360
|
+
if (!newSet.has(id)) {
|
|
361
|
+
this.disableMainIndicator(id)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const id of newSet) {
|
|
366
|
+
if (!currentSet.has(id)) {
|
|
367
|
+
this.enableMainIndicator(id)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ========== 副图管理 API ==========
|
|
373
|
+
|
|
374
|
+
bindIndicatorToPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
|
|
375
|
+
if (!this.deps.hasPane(paneId)) {
|
|
376
|
+
this.deps.upsertPane({ id: paneId, ratio: 1, visible: true, role: 'indicator' })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
|
|
380
|
+
if (!definition) {
|
|
381
|
+
throw new Error(`[Chart] Unknown indicator: ${indicatorId}`)
|
|
382
|
+
}
|
|
383
|
+
const renderer = createSubIndicatorRenderer({ indicatorId, paneId, definition, params })
|
|
384
|
+
const rendererName = renderer.name
|
|
385
|
+
const existing = this.deps.getRenderer(rendererName)
|
|
386
|
+
if (existing) {
|
|
387
|
+
if (params) this.deps.updateRendererConfig(rendererName, params)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.deps.useRenderer(renderer, params)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
createSubPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): boolean {
|
|
395
|
+
const paneSpecs = this.deps.getPaneSpecs()
|
|
396
|
+
const visibleSpecs = paneSpecs.filter((pane) => pane.visible !== false)
|
|
397
|
+
const pricePanes = visibleSpecs.filter((pane) => pane.role === 'price')
|
|
398
|
+
const indicatorPanes = visibleSpecs.filter((pane) => pane.role === 'indicator')
|
|
399
|
+
|
|
400
|
+
if (pricePanes.length === 1) {
|
|
401
|
+
const pricePane = pricePanes[0]
|
|
402
|
+
if (pricePane) {
|
|
403
|
+
this.deps.setInternalPaneRatio(pricePane.id, 3)
|
|
404
|
+
}
|
|
405
|
+
for (const pane of indicatorPanes) {
|
|
406
|
+
this.deps.setInternalPaneRatio(pane.id, 1)
|
|
407
|
+
}
|
|
408
|
+
this.deps.setInternalPaneRatio(paneId, 1)
|
|
409
|
+
} else {
|
|
410
|
+
this.deps.setInternalPaneRatio(paneId, 1)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.deps.upsertPane({ id: paneId, ratio: this.deps.getInternalPaneRatios().get(paneId) ?? 1, visible: true, role: 'indicator' })
|
|
414
|
+
|
|
415
|
+
const success = this.subPaneManager.create(this.subPaneCtx, paneId, indicatorId, params ?? this.getDefaultSubPaneParams(indicatorId))
|
|
416
|
+
return success
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
removeSubPane(paneId: string): void {
|
|
420
|
+
this.subPaneManager.remove(this.subPaneCtx, paneId)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
replaceSubPaneIndicator(paneId: string, newIndicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
|
|
424
|
+
this.subPaneManager.replaceIndicator(this.subPaneCtx, paneId, newIndicatorId, params ?? this.getDefaultSubPaneParams(newIndicatorId))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
updateSubPaneParams(paneId: string, params: Record<string, unknown>): void {
|
|
428
|
+
this.subPaneManager.updateParams(this.subPaneCtx, paneId, params)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
clearSubPanes(): void {
|
|
432
|
+
const subPaneIds = this.subPaneManager.getPaneIds()
|
|
433
|
+
|
|
434
|
+
if (subPaneIds.length === 0) return
|
|
435
|
+
|
|
436
|
+
this.subPaneManager.clear(this.subPaneCtx)
|
|
437
|
+
|
|
438
|
+
for (const paneId of subPaneIds) {
|
|
439
|
+
this.deps.deleteInternalPaneRatio(paneId)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.deps.applyPaneLayoutSpecs(this.deps.getPaneSpecs().filter((spec) => !subPaneIds.includes(spec.id)))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @deprecated 使用 getSubPaneEntries 获取完整信息
|
|
447
|
+
*/
|
|
448
|
+
getSubPaneIndicators(): SubIndicatorType[] {
|
|
449
|
+
return this.subPaneManager.getAll().map((entry) => entry.indicatorId)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
getSubPaneEntries(): SubPaneEntry[] {
|
|
453
|
+
return this.subPaneManager.getAll()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
getSubPaneEntry(paneId: string): SubPaneEntry | undefined {
|
|
457
|
+
return this.subPaneManager.getByPaneId(paneId)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private getDefaultSubPaneParams(indicatorId: SubIndicatorType): Record<string, unknown> {
|
|
461
|
+
const defaults: Record<string, Record<string, unknown>> = {
|
|
462
|
+
VOLUME: {},
|
|
463
|
+
MACD: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
|
|
464
|
+
RSI: { period1: 6, period2: 12, period3: 24 },
|
|
465
|
+
CCI: { period: 14, showCCI: true },
|
|
466
|
+
STOCH: { n: 9, m: 3, showK: true, showD: true },
|
|
467
|
+
MOM: { period: 10, showMOM: true },
|
|
468
|
+
WMSR: { period: 14, showWMSR: true },
|
|
469
|
+
KST: { roc1: 10, roc2: 15, roc3: 20, roc4: 30, signalPeriod: 9, showKST: true, showSignal: true },
|
|
470
|
+
FASTK: { period: 9, showFASTK: true },
|
|
471
|
+
ATR: { period: 14, showATR: true },
|
|
472
|
+
WMA: { period: 10, showWMA: true },
|
|
473
|
+
DEMA: { period: 14, showDEMA: true },
|
|
474
|
+
TEMA: { period: 14, showTEMA: true },
|
|
475
|
+
HMA: { period: 14, showHMA: true },
|
|
476
|
+
KAMA: { period: 10, fastPeriod: 2, slowPeriod: 30, showKAMA: true },
|
|
477
|
+
SAR: { step: 0.02, maxStep: 0.2, showSAR: true },
|
|
478
|
+
SUPERTREND: { atrPeriod: 10, multiplier: 3, showSuperTrend: true },
|
|
479
|
+
KELTNER: { emaPeriod: 20, atrPeriod: 10, multiplier: 2, showUpper: true, showMiddle: true, showLower: true },
|
|
480
|
+
DONCHIAN: { period: 20, showUpper: true, showMiddle: true, showLower: true },
|
|
481
|
+
ICHIMOKU: { tenkanPeriod: 9, kijunPeriod: 26, spanBPeriod: 52, displacement: 26, showTenkan: true, showKijun: true, showSpanA: true, showSpanB: true, showChikou: true, showCloud: true },
|
|
482
|
+
ROC: { period: 12, showROC: true },
|
|
483
|
+
TRIX: { period: 15, signalPeriod: 9, showTRIX: true, showSignal: true },
|
|
484
|
+
HV: { period: 20, annualizationFactor: 252, showHV: true },
|
|
485
|
+
PARKINSON: { period: 20, annualizationFactor: 252, showParkinson: true },
|
|
486
|
+
CHAIKIN_VOL: { emaPeriod: 10, rocPeriod: 10, showChaikinVol: true },
|
|
487
|
+
VMA: { period: 5, showVMA: true },
|
|
488
|
+
OBV: { showOBV: true },
|
|
489
|
+
PVT: { showPVT: true },
|
|
490
|
+
VWAP: { sessionResetGapMs: 0, showVWAP: true },
|
|
491
|
+
CMF: { period: 20, showCMF: true },
|
|
492
|
+
MFI: { period: 14, showMFI: true },
|
|
493
|
+
PIVOT: { showPP: true, showR1: true, showR2: true, showR3: false, showS1: true, showS2: true, showS3: false },
|
|
494
|
+
FIB: { period: 50, showLevels: true },
|
|
495
|
+
STRUCTURE: { leftWindow: 2, rightWindow: 2, breakoutSource: 'close', showSwingLabels: true, showBOS: true, showCHOCH: true, showProvisional: false },
|
|
496
|
+
ZONES: { showFVG: true, showOB: true, showFilledZones: true, obLookback: 5 },
|
|
497
|
+
VOLUME_PROFILE: { bins: 24, lookback: 0, valueAreaPercent: 0.7, showVolumeProfile: true },
|
|
498
|
+
}
|
|
499
|
+
return { ...(defaults[indicatorId] ?? {}) }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ========== 高层指标 API ==========
|
|
503
|
+
|
|
504
|
+
addIndicator(
|
|
505
|
+
definitionId: string,
|
|
506
|
+
role: 'main' | 'sub',
|
|
507
|
+
params?: Record<string, unknown>,
|
|
508
|
+
): string | null {
|
|
509
|
+
if (role === 'main') {
|
|
510
|
+
const success = this.enableMainIndicator(definitionId, params as Record<string, number | boolean | string>)
|
|
511
|
+
if (!success) return null
|
|
512
|
+
return definitionId.toUpperCase()
|
|
513
|
+
} else {
|
|
514
|
+
const paneId = `${definitionId.toUpperCase()}_${Date.now()}`
|
|
515
|
+
const success = this.createSubPane(
|
|
516
|
+
paneId,
|
|
517
|
+
definitionId as SubIndicatorType,
|
|
518
|
+
params as Record<string, number | boolean | string>,
|
|
519
|
+
)
|
|
520
|
+
if (!success) return null
|
|
521
|
+
return paneId
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
removeIndicator(instanceId: string): boolean {
|
|
526
|
+
const id = instanceId.toUpperCase()
|
|
527
|
+
|
|
528
|
+
if (this._mainIndicatorsSignal.peek().has(id)) {
|
|
529
|
+
return this.disableMainIndicator(instanceId)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const subPaneEntry = this.getSubPaneEntry(instanceId)
|
|
533
|
+
if (subPaneEntry) {
|
|
534
|
+
this.removeSubPane(instanceId)
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
updateIndicatorParams(instanceId: string, params: Record<string, unknown>): boolean {
|
|
542
|
+
const id = instanceId.toUpperCase()
|
|
543
|
+
|
|
544
|
+
if (this._mainIndicatorsSignal.peek().has(id)) {
|
|
545
|
+
this.updateMainIndicatorParams(instanceId, params as Record<string, number | boolean | string>)
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const subPaneEntry = this.getSubPaneEntry(instanceId)
|
|
550
|
+
if (subPaneEntry) {
|
|
551
|
+
this.updateSubPaneParams(instanceId, params)
|
|
552
|
+
return true
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return false
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
reorderIndicators(orderedInstanceIds: string[]): boolean {
|
|
559
|
+
console.warn('[Chart] reorderIndicators not fully implemented yet')
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
destroy(): void {
|
|
564
|
+
this.indicatorScheduler.destroy()
|
|
565
|
+
}
|
|
566
|
+
}
|