@363045841yyt/klinechart-core 0.8.10-alpha.2 → 0.8.10
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/index.d.ts +1 -1
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +0 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.effects.d.ts +21 -0
- package/dist/data-fetchers/dataBuffer.effects.d.ts.map +1 -0
- package/dist/data-fetchers/dataBuffer.effects.js +55 -0
- package/dist/data-fetchers/dataBuffer.effects.js.map +1 -0
- package/dist/data-fetchers/dataBuffer.js +58 -93
- 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/timeShareBuffer.d.ts +2 -1
- package/dist/data-fetchers/timeShareBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/timeShareBuffer.js +36 -14
- package/dist/data-fetchers/timeShareBuffer.js.map +1 -1
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +2 -1
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/drawing/AnchorCollector.d.ts +26 -0
- package/dist/engine/drawing/AnchorCollector.d.ts.map +1 -0
- package/dist/engine/drawing/AnchorCollector.js +47 -0
- package/dist/engine/drawing/AnchorCollector.js.map +1 -0
- package/dist/engine/drawing/DragHandler.d.ts +38 -0
- package/dist/engine/drawing/DragHandler.d.ts.map +1 -0
- package/dist/engine/drawing/DragHandler.js +92 -0
- package/dist/engine/drawing/DragHandler.js.map +1 -0
- package/dist/engine/drawing/DrawingState.d.ts +51 -0
- package/dist/engine/drawing/DrawingState.d.ts.map +1 -0
- package/dist/engine/drawing/DrawingState.js +115 -0
- package/dist/engine/drawing/DrawingState.js.map +1 -0
- package/dist/engine/drawing/HitTester.d.ts +59 -0
- package/dist/engine/drawing/HitTester.d.ts.map +1 -0
- package/dist/engine/drawing/HitTester.js +219 -0
- package/dist/engine/drawing/HitTester.js.map +1 -0
- package/dist/engine/drawing/PreviewRenderer.d.ts +26 -0
- package/dist/engine/drawing/PreviewRenderer.d.ts.map +1 -0
- package/dist/engine/drawing/PreviewRenderer.js +131 -0
- package/dist/engine/drawing/PreviewRenderer.js.map +1 -0
- package/dist/engine/drawing/coordinateUtils.d.ts +57 -0
- package/dist/engine/drawing/coordinateUtils.d.ts.map +1 -0
- package/dist/engine/drawing/coordinateUtils.js +103 -0
- package/dist/engine/drawing/coordinateUtils.js.map +1 -0
- package/dist/engine/drawing/index.d.ts.map +1 -1
- package/dist/engine/drawing/index.js +11 -3
- package/dist/engine/drawing/index.js.map +1 -1
- package/dist/engine/drawing/interaction.d.ts +44 -40
- package/dist/engine/drawing/interaction.d.ts.map +1 -1
- package/dist/engine/drawing/interaction.js +132 -571
- package/dist/engine/drawing/interaction.js.map +1 -1
- package/dist/engine/drawing/toolConfig.d.ts +24 -0
- package/dist/engine/drawing/toolConfig.d.ts.map +1 -0
- package/dist/engine/drawing/toolConfig.js +76 -0
- package/dist/engine/drawing/toolConfig.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/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 +4 -1
- package/src/controllers/index.ts +1 -0
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +1 -3
- package/src/data-fetchers/dataBuffer.effects.ts +118 -0
- package/src/data-fetchers/dataBuffer.ts +45 -86
- package/src/data-fetchers/index.ts +7 -0
- package/src/data-fetchers/timeShareBuffer.ts +58 -19
- package/src/engine/__tests__/paneRenderer.resize.test.ts +3 -0
- package/src/engine/__tests__/subPaneManager.test.ts +13 -3
- package/src/engine/data/chartDataManager.ts +2 -1
- package/src/engine/drawing/AnchorCollector.ts +57 -0
- package/src/engine/drawing/DragHandler.ts +121 -0
- package/src/engine/drawing/DrawingState.ts +132 -0
- package/src/engine/drawing/HitTester.ts +288 -0
- package/src/engine/drawing/PreviewRenderer.ts +157 -0
- package/src/engine/drawing/coordinateUtils.ts +139 -0
- package/src/engine/drawing/index.ts +10 -3
- package/src/engine/drawing/interaction.ts +177 -687
- package/src/engine/drawing/toolConfig.ts +103 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +1 -0
- package/src/engine/indicators/__tests__/stateComposer.test.ts +5 -4
- package/src/engine/renderers/Indicator/__tests__/createSubIndicatorRenderer.test.ts +1 -0
- package/src/plugin/types.ts +1 -0
- package/src/tokens/__tests__/tokens.test.ts +2 -1
- package/src/version.ts +1 -1
|
@@ -1,41 +1,15 @@
|
|
|
1
1
|
import { createSignal, type Signal } from '../reactivity/signal'
|
|
2
2
|
import type { DataFetcher, KLineData, SymbolSpec } from '../controllers/types'
|
|
3
3
|
import type { DataBufferLike } from './dataBufferTypes'
|
|
4
|
+
import { Effect, pipe } from 'effect'
|
|
5
|
+
import type { Effect as EffectType } from 'effect/Effect'
|
|
6
|
+
import { fetchKLine, KLineFetchService, getPeriodDays, formatDate, MS_PER_DAY } from './dataBuffer.effects'
|
|
4
7
|
|
|
5
8
|
export interface DataWindow {
|
|
6
9
|
earliestTs: number
|
|
7
10
|
latestTs: number
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
const MS_PER_DAY = 86_400_000
|
|
11
|
-
const FETCH_MAX_RETRIES = 2
|
|
12
|
-
|
|
13
|
-
const PERIOD_INITIAL_DAYS: Record<string, number> = {
|
|
14
|
-
'1min': 3,
|
|
15
|
-
'5min': 30,
|
|
16
|
-
'15min': 60,
|
|
17
|
-
'30min': 90,
|
|
18
|
-
'60min': 180,
|
|
19
|
-
daily: 365,
|
|
20
|
-
weekly: 365,
|
|
21
|
-
monthly: 365,
|
|
22
|
-
quarterly: 365,
|
|
23
|
-
yearly: 365,
|
|
24
|
-
timeshare: 1,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getPeriodDays(period?: string): number {
|
|
28
|
-
return PERIOD_INITIAL_DAYS[period ?? 'daily'] ?? 365
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function formatDate(ts: number): string {
|
|
32
|
-
const d = new Date(ts)
|
|
33
|
-
const y = d.getFullYear()
|
|
34
|
-
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
35
|
-
const day = String(d.getDate()).padStart(2, '0')
|
|
36
|
-
return `${y}-${m}-${day}`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
13
|
function mergeSortedData(
|
|
40
14
|
existing: KLineData[],
|
|
41
15
|
incoming: KLineData[],
|
|
@@ -62,6 +36,7 @@ export class DataBuffer implements DataBufferLike {
|
|
|
62
36
|
private _loadedWindow: DataWindow | null = null
|
|
63
37
|
private _pendingFetch: Promise<void> | null = null
|
|
64
38
|
private _disposed = false
|
|
39
|
+
// 已尝试请求过的边界时间戳集合,防止同一个时间段重复请求
|
|
65
40
|
private _attemptedBoundaries: Set<number> = new Set()
|
|
66
41
|
private _monthKeys: Int32Array | null = null
|
|
67
42
|
private _dayKeys: Int32Array | null = null
|
|
@@ -157,57 +132,61 @@ export class DataBuffer implements DataBufferLike {
|
|
|
157
132
|
this.fetchRange(requestStartTs, incrementalEnd)
|
|
158
133
|
}
|
|
159
134
|
|
|
160
|
-
|
|
161
|
-
|
|
135
|
+
private loadInitial(): void {
|
|
136
|
+
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
162
137
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
138
|
+
const now = Date.now()
|
|
139
|
+
const days = getPeriodDays(this._currentSpec.period)
|
|
140
|
+
const startDate = now - days * MS_PER_DAY
|
|
141
|
+
const endDate = now
|
|
167
142
|
|
|
168
|
-
|
|
169
|
-
|
|
143
|
+
this.fetchRange(startDate, endDate)
|
|
144
|
+
}
|
|
170
145
|
|
|
171
146
|
private loadInitialRange(startTs: number, endTs: number): void {
|
|
172
147
|
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
173
148
|
this.fetchRange(startTs, endTs)
|
|
174
149
|
}
|
|
175
150
|
|
|
176
|
-
private fetchRange(startTs: number, endTs: number
|
|
151
|
+
private fetchRange(startTs: number, endTs: number): void {
|
|
177
152
|
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
178
153
|
|
|
179
154
|
if (this._pendingFetch) {
|
|
180
155
|
this._pendingFetch = this._pendingFetch.then(() => {
|
|
181
156
|
if (this._disposed) return
|
|
182
|
-
return this.fetchRange(startTs, endTs
|
|
157
|
+
return this.fetchRange(startTs, endTs)
|
|
183
158
|
})
|
|
184
159
|
return
|
|
185
160
|
}
|
|
186
161
|
|
|
187
162
|
const spec = this._currentSpec
|
|
188
|
-
const fetcher = this._fetcher
|
|
189
|
-
|
|
190
163
|
this._loadingSignal.set(true)
|
|
191
164
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
165
|
+
const service: { readonly fetch: (spec: SymbolSpec, startTs: number, endTs: number) => EffectType<ReadonlyArray<KLineData>, unknown> } = {
|
|
166
|
+
fetch: (s, start, end) =>
|
|
167
|
+
Effect.tryPromise(() => {
|
|
168
|
+
if (this._requestFetch) {
|
|
169
|
+
return this._requestFetch(s, start, end)
|
|
170
|
+
}
|
|
171
|
+
// 未定义 Fetcher 走 gotdx fallback 获取
|
|
172
|
+
return (this._fetcher as NonNullable<DataFetcher>)(s.source ?? 'gotdx', {
|
|
173
|
+
symbol: s.symbol,
|
|
174
|
+
startDate: formatDate(start),
|
|
175
|
+
endDate: formatDate(end),
|
|
176
|
+
period: s.period ?? 'daily',
|
|
177
|
+
adjust: s.adjust ?? 'none',
|
|
178
|
+
exchange: s.exchange,
|
|
179
|
+
})
|
|
180
|
+
}),
|
|
181
|
+
}
|
|
205
182
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
183
|
+
this._pendingFetch = pipe(
|
|
184
|
+
fetchKLine(spec, startTs, endTs),
|
|
185
|
+
Effect.provideService(KLineFetchService, service),
|
|
186
|
+
Effect.runPromise, // 链式传递返回值, Effect -> Promise -> run
|
|
187
|
+
)
|
|
188
|
+
.then((incoming) => {
|
|
189
|
+
if (this._disposed) return
|
|
211
190
|
|
|
212
191
|
const oldLength = this._data.length
|
|
213
192
|
const oldEarliestTs = oldLength > 0 ? this._data[0]!.timestamp : null
|
|
@@ -245,35 +224,15 @@ export class DataBuffer implements DataBufferLike {
|
|
|
245
224
|
}
|
|
246
225
|
}
|
|
247
226
|
})
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const attempt = (count: number): Promise<void> => {
|
|
251
|
-
return doFetch().catch((err) => {
|
|
252
|
-
if (this._disposed) return
|
|
253
|
-
|
|
254
|
-
if (count < FETCH_MAX_RETRIES) {
|
|
255
|
-
const delay = Math.pow(2, count) * 1000
|
|
256
|
-
console.warn(
|
|
257
|
-
`[DataBuffer] fetch failed, retry ${count + 1}/${FETCH_MAX_RETRIES} in ${delay}ms:`,
|
|
258
|
-
err,
|
|
259
|
-
)
|
|
260
|
-
return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => {
|
|
261
|
-
if (this._disposed) return
|
|
262
|
-
return attempt(count + 1)
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
|
|
227
|
+
.catch(() => {
|
|
267
228
|
this._attemptedBoundaries.delete(endTs)
|
|
268
229
|
})
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
})
|
|
230
|
+
.finally(() => {
|
|
231
|
+
this._pendingFetch = null
|
|
232
|
+
if (!this._disposed) {
|
|
233
|
+
this._loadingSignal.set(false)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
277
236
|
}
|
|
278
237
|
|
|
279
238
|
/**
|
|
@@ -8,5 +8,12 @@ export { TimeShareBuffer } from './timeShareBuffer'
|
|
|
8
8
|
export type { DataBufferLike } from './dataBufferTypes'
|
|
9
9
|
export { getRegisteredFetcher, getTimeShareFetcher, fetcherSupportsTimeShare } from './fetcherDefinitionRegistry'
|
|
10
10
|
export type { TimeShareFetcherFn, TimeShareFetchConfig } from './types'
|
|
11
|
+
export {
|
|
12
|
+
getPeriodDays,
|
|
13
|
+
fetchKLine,
|
|
14
|
+
fetchTimeShare,
|
|
15
|
+
KLineFetchService,
|
|
16
|
+
TimeShareFetchService,
|
|
17
|
+
} from './dataBuffer.effects'
|
|
11
18
|
import './gotdx'
|
|
12
19
|
import './tradingview'
|
|
@@ -5,14 +5,26 @@ import type { TimeShareFetcherFn } from './types'
|
|
|
5
5
|
import type { DataBufferLike } from './dataBufferTypes'
|
|
6
6
|
import type { DataWindow } from './dataBuffer'
|
|
7
7
|
import { routerTimeShareFetcher } from './router'
|
|
8
|
+
import { Effect, Fiber, pipe } from 'effect'
|
|
9
|
+
import type { Effect as EffectType } from 'effect/Effect'
|
|
10
|
+
import { fetchTimeShare, TimeShareFetchService } from './dataBuffer.effects'
|
|
8
11
|
|
|
9
12
|
export class TimeShareBuffer implements DataBufferLike {
|
|
13
|
+
// 当前持有的分时数据数组(内部可变副本)
|
|
10
14
|
private _data: TimeShareData[] = []
|
|
15
|
+
// 向外部广播只读数据快照的信号
|
|
11
16
|
private _dataSignal = createSignal<ReadonlyArray<TimeShareData>>([])
|
|
17
|
+
// 是否正在加载中,外部 UI 绑定用
|
|
12
18
|
private _loadingSignal = createSignal<boolean>(false)
|
|
19
|
+
// 可选的自定义 fetcher,优先级大于默认 fectcher
|
|
13
20
|
private _fetcher: TimeShareFetcherFn | null = null
|
|
21
|
+
// 指定查询的历史日期(0 = 当天)
|
|
14
22
|
private _queryDate = 0
|
|
23
|
+
// 请求序号,每次 load() 递增
|
|
15
24
|
private _requestSeq = 0
|
|
25
|
+
// 当前运行的 fetch Fiber 句柄,用于随时中断旧请求
|
|
26
|
+
private _fetchFiber: Fiber.RuntimeFiber<readonly TimeShareData[], unknown> | null = null
|
|
27
|
+
// 实例是否已销毁,阻止后续任何操作
|
|
16
28
|
private _disposed = false
|
|
17
29
|
|
|
18
30
|
get data(): Signal<ReadonlyArray<unknown>> {
|
|
@@ -51,30 +63,52 @@ export class TimeShareBuffer implements DataBufferLike {
|
|
|
51
63
|
return this._queryDate
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
load(spec: SymbolSpec): void {
|
|
55
67
|
if (this._disposed) return
|
|
56
|
-
|
|
68
|
+
|
|
69
|
+
if (this._fetchFiber) {
|
|
70
|
+
Fiber.interrupt(this._fetchFiber)
|
|
71
|
+
this._fetchFiber = null
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
const requestSeq = ++this._requestSeq
|
|
58
75
|
this._loadingSignal.set(true)
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this._dataSignal.set([...data])
|
|
73
|
-
} finally {
|
|
74
|
-
if (requestSeq === this._requestSeq) {
|
|
75
|
-
this._loadingSignal.set(false)
|
|
76
|
-
}
|
|
77
|
+
const timeShareService: {
|
|
78
|
+
readonly fetch: (s: SymbolSpec, date?: number) => EffectType<ReadonlyArray<TimeShareData>, unknown>
|
|
79
|
+
} = {
|
|
80
|
+
fetch: (s, date) =>
|
|
81
|
+
Effect.tryPromise(() => {
|
|
82
|
+
const fetcher = this._fetcher ?? routerTimeShareFetcher
|
|
83
|
+
return fetcher(s.source ?? 'gotdx', {
|
|
84
|
+
symbol: s.symbol,
|
|
85
|
+
exchange: s.exchange,
|
|
86
|
+
date,
|
|
87
|
+
})
|
|
88
|
+
}),
|
|
77
89
|
}
|
|
90
|
+
|
|
91
|
+
const effect = pipe(
|
|
92
|
+
fetchTimeShare(spec, this._queryDate || undefined),
|
|
93
|
+
Effect.provideService(TimeShareFetchService, timeShareService),
|
|
94
|
+
Effect.tap((data) =>
|
|
95
|
+
Effect.sync(() => {
|
|
96
|
+
if (this._disposed) return
|
|
97
|
+
this._queryDate = 0
|
|
98
|
+
this._data = [...data]
|
|
99
|
+
this._dataSignal.set([...data])
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
Effect.ensuring(
|
|
103
|
+
Effect.sync(() => {
|
|
104
|
+
if (requestSeq === this._requestSeq) {
|
|
105
|
+
this._loadingSignal.set(false)
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
this._fetchFiber = Effect.runFork(effect)
|
|
78
112
|
}
|
|
79
113
|
|
|
80
114
|
setInlineData(data: unknown[]): void {
|
|
@@ -83,9 +117,14 @@ export class TimeShareBuffer implements DataBufferLike {
|
|
|
83
117
|
this._dataSignal.set([...(data as TimeShareData[])])
|
|
84
118
|
}
|
|
85
119
|
|
|
120
|
+
// 销毁实例
|
|
86
121
|
dispose(): void {
|
|
87
122
|
this._disposed = true
|
|
88
123
|
this._requestSeq++
|
|
124
|
+
if (this._fetchFiber) {
|
|
125
|
+
Fiber.interrupt(this._fetchFiber)
|
|
126
|
+
this._fetchFiber = null
|
|
127
|
+
}
|
|
89
128
|
this._data = []
|
|
90
129
|
this._loadingSignal.set(false)
|
|
91
130
|
}
|
|
@@ -16,6 +16,7 @@ describe('PaneRenderer resize DPR mapping', () => {
|
|
|
16
16
|
pane,
|
|
17
17
|
{
|
|
18
18
|
rightAxisWidth: 80,
|
|
19
|
+
leftAxisWidth: 60,
|
|
19
20
|
yPaddingPx: 0,
|
|
20
21
|
priceLabelWidth: 60,
|
|
21
22
|
},
|
|
@@ -44,6 +45,7 @@ describe('PaneRenderer resize DPR mapping', () => {
|
|
|
44
45
|
pane,
|
|
45
46
|
{
|
|
46
47
|
rightAxisWidth: 100,
|
|
48
|
+
leftAxisWidth: 60,
|
|
47
49
|
yPaddingPx: 0,
|
|
48
50
|
priceLabelWidth: 70,
|
|
49
51
|
},
|
|
@@ -77,6 +79,7 @@ describe('PaneRenderer resize DPR mapping', () => {
|
|
|
77
79
|
pane,
|
|
78
80
|
{
|
|
79
81
|
rightAxisWidth: 100,
|
|
82
|
+
leftAxisWidth: 60,
|
|
80
83
|
yPaddingPx: 0,
|
|
81
84
|
priceLabelWidth: 70,
|
|
82
85
|
},
|
|
@@ -6,7 +6,12 @@ import type { IndicatorScheduler } from '../indicators/scheduler'
|
|
|
6
6
|
function createMockScheduler(): Partial<IndicatorScheduler> {
|
|
7
7
|
return {
|
|
8
8
|
getIndicatorMetadata: vi.fn((_id: string) => ({
|
|
9
|
-
|
|
9
|
+
name: _id,
|
|
10
|
+
displayName: 'Test',
|
|
11
|
+
category: 'sub' as const,
|
|
12
|
+
stateKey: _id,
|
|
13
|
+
defaultPaneId: 'sub',
|
|
14
|
+
rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0', paneId: 'sub', priority: 0, draw: vi.fn() })),
|
|
10
15
|
updateConfig: vi.fn(),
|
|
11
16
|
scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
|
|
12
17
|
})),
|
|
@@ -88,8 +93,13 @@ describe('SubPaneManager', () => {
|
|
|
88
93
|
it('should update scheduler config via definition.updateConfig', () => {
|
|
89
94
|
const updateConfigSpy = vi.fn()
|
|
90
95
|
const customScheduler: Partial<IndicatorScheduler> = {
|
|
91
|
-
getIndicatorMetadata: vi.fn(() => ({
|
|
92
|
-
|
|
96
|
+
getIndicatorMetadata: vi.fn((_id: string) => ({
|
|
97
|
+
name: _id,
|
|
98
|
+
displayName: 'Test',
|
|
99
|
+
category: 'sub' as const,
|
|
100
|
+
stateKey: _id,
|
|
101
|
+
defaultPaneId: 'sub',
|
|
102
|
+
rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0', paneId: 'sub', priority: 0, draw: vi.fn() })),
|
|
93
103
|
updateConfig: updateConfigSpy,
|
|
94
104
|
scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
|
|
95
105
|
})),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { KLineData, TimeShareData } from '../../types/price'
|
|
2
2
|
import type { SymbolSpec, DataFetcher, CustomDataSource } from '../../controllers/types'
|
|
3
3
|
import { createSignal, type Signal } from '../../reactivity/signal'
|
|
4
|
-
import { DataBuffer
|
|
4
|
+
import { DataBuffer } from '../../data-fetchers/dataBuffer'
|
|
5
|
+
import { getPeriodDays } from '../../data-fetchers/dataBuffer.effects'
|
|
5
6
|
import { TimeShareBuffer } from '../../data-fetchers/timeShareBuffer'
|
|
6
7
|
import type { DataBufferLike } from '../../data-fetchers/dataBufferTypes'
|
|
7
8
|
import type { TimeShareFetcherFn } from '../../data-fetchers/types'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DrawingToolId } from './toolConfig'
|
|
2
|
+
import { getAnchorCountForTool } from './toolConfig'
|
|
3
|
+
import type { DrawingAnchorInput } from './coordinateUtils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Accumulates pointer anchors until the required count is reached for a given tool.
|
|
7
|
+
* Single-anchor tools are not managed here — they create immediately on first click.
|
|
8
|
+
*/
|
|
9
|
+
export class AnchorCollector {
|
|
10
|
+
pendingAnchors: DrawingAnchorInput[] = []
|
|
11
|
+
|
|
12
|
+
/** Returns true when the tool uses multiple anchors (2 or 3). */
|
|
13
|
+
isMultiAnchorTool(toolId: DrawingToolId): boolean {
|
|
14
|
+
const count = getAnchorCountForTool(toolId)
|
|
15
|
+
return count === 2 || count === 3
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Returns the required anchor count for the given tool, or null for single-anchor / cursor. */
|
|
19
|
+
getRequiredCount(toolId: DrawingToolId): number | null {
|
|
20
|
+
return getAnchorCountForTool(toolId)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 当前已累积的锚点数量 */
|
|
24
|
+
getPendingCount(): number {
|
|
25
|
+
return this.pendingAnchors.length
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Add an anchor for the given tool.
|
|
30
|
+
* @returns the full anchor list if the required count is reached (caller should create drawing),
|
|
31
|
+
* or null if still accumulating.
|
|
32
|
+
*/
|
|
33
|
+
addAnchor(anchor: DrawingAnchorInput, toolId: DrawingToolId): DrawingAnchorInput[] | null {
|
|
34
|
+
const required = this.getRequiredCount(toolId)
|
|
35
|
+
if (required === null) return null
|
|
36
|
+
|
|
37
|
+
this.pendingAnchors.push(anchor)
|
|
38
|
+
|
|
39
|
+
if (this.pendingAnchors.length >= required) {
|
|
40
|
+
const result = [...this.pendingAnchors]
|
|
41
|
+
this.pendingAnchors = []
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Peek at the first pending anchor without modifying state. */
|
|
49
|
+
getFirst(): DrawingAnchorInput | undefined {
|
|
50
|
+
return this.pendingAnchors[0]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 清空累积的锚点 */
|
|
54
|
+
reset(): void {
|
|
55
|
+
this.pendingAnchors = []
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { DrawingObject, DrawingAnchor } from '../../plugin'
|
|
2
|
+
import type { DrawingChartAdapter } from '../../controllers/types'
|
|
3
|
+
import { resolveAnchorFromPointer, anchorToScreen, screenToAnchor } from './coordinateUtils'
|
|
4
|
+
|
|
5
|
+
// ---- Types ----
|
|
6
|
+
|
|
7
|
+
export interface DragState {
|
|
8
|
+
drawingId: string
|
|
9
|
+
anchorIndex?: number
|
|
10
|
+
snapshot: DrawingAnchor[]
|
|
11
|
+
startMouse: { x: number; y: number }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manages drag state and handles drag-move mutations for drawings.
|
|
16
|
+
* Does NOT own the drawings array — the caller retrieves and writes back.
|
|
17
|
+
*/
|
|
18
|
+
export class DragHandler {
|
|
19
|
+
private dragState: DragState | null = null
|
|
20
|
+
|
|
21
|
+
/** 当前是否有未结束的拖拽 */
|
|
22
|
+
isDragging(): boolean {
|
|
23
|
+
return this.dragState !== null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 拖拽中的图元 ID */
|
|
27
|
+
getDraggingDrawingId(): string | null {
|
|
28
|
+
return this.dragState?.drawingId ?? null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 开始拖拽。
|
|
33
|
+
* @param drawing 拖拽的图元
|
|
34
|
+
* @param anchorIndex 拖拽单个锚点时传锚点下标;拖拽整线时不传
|
|
35
|
+
* @param mouseX 起始鼠标 X(屏幕 px)
|
|
36
|
+
* @param mouseY 起始鼠标 Y(屏幕 px)
|
|
37
|
+
*/
|
|
38
|
+
startDrag(
|
|
39
|
+
drawing: DrawingObject,
|
|
40
|
+
anchorIndex: number | undefined,
|
|
41
|
+
mouseX: number,
|
|
42
|
+
mouseY: number,
|
|
43
|
+
): void {
|
|
44
|
+
this.dragState = {
|
|
45
|
+
drawingId: drawing.id,
|
|
46
|
+
anchorIndex,
|
|
47
|
+
snapshot: drawing.anchors.map((a) => ({ ...a })),
|
|
48
|
+
startMouse: { x: mouseX, y: mouseY },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle drag-move for the given drawing, mutating its anchors.
|
|
54
|
+
* @returns a new DrawingObject reference with updated anchors, or null if drag state is stale.
|
|
55
|
+
*/
|
|
56
|
+
handleDragMove(
|
|
57
|
+
drawing: DrawingObject,
|
|
58
|
+
e: PointerEvent,
|
|
59
|
+
container: HTMLElement,
|
|
60
|
+
adapter: DrawingChartAdapter,
|
|
61
|
+
): DrawingObject | null {
|
|
62
|
+
if (!this.dragState) return null
|
|
63
|
+
|
|
64
|
+
const newAnchor = resolveAnchorFromPointer(e, container, adapter)
|
|
65
|
+
const updatedAnchors = [...drawing.anchors]
|
|
66
|
+
|
|
67
|
+
if (this.dragState.anchorIndex !== undefined) {
|
|
68
|
+
// Dragging a single anchor point
|
|
69
|
+
if (!newAnchor) return null
|
|
70
|
+
const idx = this.dragState.anchorIndex
|
|
71
|
+
|
|
72
|
+
updatedAnchors[idx] = {
|
|
73
|
+
...updatedAnchors[idx]!,
|
|
74
|
+
index: newAnchor.index,
|
|
75
|
+
time: newAnchor.time,
|
|
76
|
+
price: newAnchor.price,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// flat-line: third anchor's index/time follows the second
|
|
80
|
+
if (drawing.kind === 'flat-line' && idx === 1 && updatedAnchors.length >= 3) {
|
|
81
|
+
updatedAnchors[2] = {
|
|
82
|
+
...updatedAnchors[2]!,
|
|
83
|
+
index: newAnchor.index,
|
|
84
|
+
time: newAnchor.time,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// Dragging the entire line — offset all anchors by mouse delta
|
|
89
|
+
const rect = container.getBoundingClientRect()
|
|
90
|
+
const mouseX = e.clientX - rect.left
|
|
91
|
+
const mouseY = e.clientY - rect.top
|
|
92
|
+
const dx = mouseX - this.dragState.startMouse.x
|
|
93
|
+
const dy = mouseY - this.dragState.startMouse.y
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < drawing.anchors.length; i++) {
|
|
96
|
+
const snap = this.dragState.snapshot[i]!
|
|
97
|
+
const snapScreen = anchorToScreen(snap, adapter)
|
|
98
|
+
if (!snapScreen) continue
|
|
99
|
+
|
|
100
|
+
const targetX = snapScreen.x + dx
|
|
101
|
+
const targetY = snapScreen.y + dy
|
|
102
|
+
const newFromScreen = screenToAnchor(targetX, targetY, adapter)
|
|
103
|
+
if (newFromScreen) {
|
|
104
|
+
updatedAnchors[i] = {
|
|
105
|
+
...updatedAnchors[i]!,
|
|
106
|
+
index: newFromScreen.index,
|
|
107
|
+
time: newFromScreen.time,
|
|
108
|
+
price: newFromScreen.price,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ...drawing, anchors: updatedAnchors }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 结束拖拽,清空状态 */
|
|
118
|
+
endDrag(): void {
|
|
119
|
+
this.dragState = null
|
|
120
|
+
}
|
|
121
|
+
}
|