@363045841yyt/klinechart 0.7.4 → 0.7.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -153
- package/dist/index.cjs +2 -2
- package/dist/index.js +15 -9
- package/dist/klinechart.css +1 -1
- package/package.json +82 -76
- package/src/__tests__/_mockController.ts +192 -192
- package/src/__tests__/contract.test.ts +132 -132
- package/src/components/DrawingStyleToolbar.vue +199 -199
- package/src/components/IndicatorParams.vue +570 -570
- package/src/components/IndicatorSelector.vue +1169 -1169
- package/src/components/KLineChart.vue +1570 -1570
- package/src/components/KLineTooltip.vue +200 -200
- package/src/components/LeftToolbar.vue +844 -844
- package/src/components/MarkerTooltip.vue +155 -155
- package/src/components/index.ts +7 -7
- package/src/composables/useFullscreenTeleportTarget.ts +18 -18
- package/src/debug/canvasProfiler.ts +296 -296
- package/src/index.ts +402 -402
- package/src/version.ts +3 -3
package/src/index.ts
CHANGED
|
@@ -1,402 +1,402 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @363045841yyt/klinechart — public API surface.
|
|
3
|
-
*
|
|
4
|
-
* Vue 3 bindings for @363045841yyt/klinechart-core. Bridges core signals to Vue's
|
|
5
|
-
* reactivity via `shallowRef` + `effect` so each adapter owns its own
|
|
6
|
-
* reactivity boundary — no proxy wrapping of immutable signal values.
|
|
7
|
-
*
|
|
8
|
-
* Backward-compatibility contract: `KMapPlugin.install(app)` MUST exist
|
|
9
|
-
* because legacy users of `@363045841yyt/klinechart` consume it.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
defineComponent,
|
|
14
|
-
effectScope,
|
|
15
|
-
h,
|
|
16
|
-
onBeforeUnmount,
|
|
17
|
-
onMounted,
|
|
18
|
-
onScopeDispose,
|
|
19
|
-
onUnmounted,
|
|
20
|
-
shallowRef,
|
|
21
|
-
watch,
|
|
22
|
-
type App,
|
|
23
|
-
type PropType,
|
|
24
|
-
type Ref,
|
|
25
|
-
} from 'vue'
|
|
26
|
-
import type { Signal } from '@363045841yyt/klinechart-core/reactivity'
|
|
27
|
-
import type {
|
|
28
|
-
ChartController,
|
|
29
|
-
ChartControllerFactory,
|
|
30
|
-
ChartMountOptions,
|
|
31
|
-
ChartViewport,
|
|
32
|
-
IndicatorInstance,
|
|
33
|
-
InteractionSnapshot,
|
|
34
|
-
KLineData,
|
|
35
|
-
} from '@363045841yyt/klinechart-core'
|
|
36
|
-
|
|
37
|
-
export type {
|
|
38
|
-
ChartController,
|
|
39
|
-
ChartMountOptions,
|
|
40
|
-
ChartViewport,
|
|
41
|
-
} from '@363045841yyt/klinechart-core'
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// SFC components (for consumers using Vite / SFC compiler)
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
export {
|
|
48
|
-
DrawingStyleToolbar,
|
|
49
|
-
IndicatorParams,
|
|
50
|
-
IndicatorSelector,
|
|
51
|
-
KLineChartVue,
|
|
52
|
-
KLineTooltip,
|
|
53
|
-
LeftToolbar,
|
|
54
|
-
MarkerTooltip,
|
|
55
|
-
} from './components/index'
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Controller factory injection
|
|
59
|
-
//
|
|
60
|
-
// The concrete `createChartController` from packages/core/src/controllers/
|
|
61
|
-
// (Phase 1A deliverable) is not yet wired. Adapters and tests inject a
|
|
62
|
-
// factory via `__setControllerFactory` so the public API surface stays stable.
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
let controllerFactory: ChartControllerFactory | null = null
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Inject the ChartController factory. Called by:
|
|
69
|
-
* - the core package's bootstrap once `createChartController` is implemented
|
|
70
|
-
* - tests that need a mock controller
|
|
71
|
-
*/
|
|
72
|
-
export function __setControllerFactory(
|
|
73
|
-
factory: ChartControllerFactory | null,
|
|
74
|
-
): void {
|
|
75
|
-
controllerFactory = factory
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// createChart �?imperative mount
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Imperative mount API. Returns a controller; caller is responsible for `dispose`.
|
|
84
|
-
*
|
|
85
|
-
* Throws if container is null/undefined (SSR-safe guard).
|
|
86
|
-
*/
|
|
87
|
-
export function createChart(opts: ChartMountOptions): ChartController {
|
|
88
|
-
if (opts.container == null) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
'[@363045841yyt/klinechart] createChart: `container` is required and must be a non-null HTMLElement',
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
if (controllerFactory === null) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
'[@363045841yyt/klinechart] createChart: no ChartController factory registered. ' +
|
|
96
|
-
'Call __setControllerFactory(...) before mounting (the core package wires this in production).',
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
return controllerFactory(opts)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// coreSignalToVueRef �?reactivity bridge
|
|
104
|
-
//
|
|
105
|
-
// Subscribe to a core Signal and mirror its value in a shallowRef. Auto-cleanup
|
|
106
|
-
// on component teardown via onScopeDispose (works inside effectScope or SFC).
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Bridge a core Signal<T> into a Vue Ref<T> backed by `shallowRef`.
|
|
111
|
-
*
|
|
112
|
-
* We use `shallowRef` (not `ref`) because:
|
|
113
|
-
* - core signal values are treated as immutable; deep proxying is wasteful
|
|
114
|
-
* - `Object.is` short-circuits in the core depend on referential equality,
|
|
115
|
-
* which Vue's deep reactivity would silently break
|
|
116
|
-
*
|
|
117
|
-
* Subscription is torn down via `onScopeDispose`, so this is safe to call
|
|
118
|
-
* inside a Vue component setup, a composable, or a manually-created
|
|
119
|
-
* `effectScope`. Calling it outside any scope still returns a working ref �?
|
|
120
|
-
* the caller is then responsible for unsubscribing.
|
|
121
|
-
*/
|
|
122
|
-
export function coreSignalToVueRef<T>(signal: Signal<T>): Ref<T> {
|
|
123
|
-
const ref = shallowRef(signal.peek()) as Ref<T>
|
|
124
|
-
const unsub = signal.subscribe(() => {
|
|
125
|
-
ref.value = signal.peek()
|
|
126
|
-
})
|
|
127
|
-
onScopeDispose(unsub)
|
|
128
|
-
return ref
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// useChart �?composable
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Composable. Pass a template ref to the container element.
|
|
137
|
-
*
|
|
138
|
-
* Watches `containerRef` to populate; once it does, calls `createChart`
|
|
139
|
-
* and exposes the controller via a `shallowRef`. Disposes on scope teardown.
|
|
140
|
-
*/
|
|
141
|
-
export function useChart(
|
|
142
|
-
containerRef: Ref<HTMLElement | null>,
|
|
143
|
-
opts: Omit<ChartMountOptions, 'container'>,
|
|
144
|
-
): { chart: Ref<ChartController | null> } {
|
|
145
|
-
const chart = shallowRef<ChartController | null>(null)
|
|
146
|
-
|
|
147
|
-
const mountIfReady = (el: HTMLElement | null): void => {
|
|
148
|
-
if (el == null || chart.value != null) return
|
|
149
|
-
chart.value = createChart({ ...opts, container: el })
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Mount synchronously if the ref is already populated (e.g. SFC where the
|
|
153
|
-
// template ref is set before this composable's effect runs).
|
|
154
|
-
mountIfReady(containerRef.value)
|
|
155
|
-
|
|
156
|
-
// Otherwise watch for the ref to populate.
|
|
157
|
-
const stopWatch = watch(
|
|
158
|
-
containerRef,
|
|
159
|
-
(el) => {
|
|
160
|
-
mountIfReady(el)
|
|
161
|
-
},
|
|
162
|
-
{ immediate: true, flush: 'post' },
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
const dispose = (): void => {
|
|
166
|
-
stopWatch()
|
|
167
|
-
const ctrl = chart.value
|
|
168
|
-
if (ctrl != null) {
|
|
169
|
-
ctrl.dispose()
|
|
170
|
-
chart.value = null
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
onScopeDispose(dispose)
|
|
175
|
-
// Belt-and-braces: SFC components running outside a manual scope still get
|
|
176
|
-
// unmount cleanup via the component lifecycle hook.
|
|
177
|
-
onBeforeUnmount(dispose)
|
|
178
|
-
|
|
179
|
-
return { chart }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// useIndicators �?composable
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Bridge the Chart's indicators signal into a Vue shallowRef.
|
|
188
|
-
*/
|
|
189
|
-
export function useIndicators(controller: ChartController): {
|
|
190
|
-
indicators: Ref<ReadonlyArray<IndicatorInstance>>
|
|
191
|
-
add: ChartController['addIndicator']
|
|
192
|
-
remove: ChartController['removeIndicator']
|
|
193
|
-
updateParams: ChartController['updateIndicatorParams']
|
|
194
|
-
} {
|
|
195
|
-
const indicators = shallowRef(controller.indicators.peek()) as Ref<
|
|
196
|
-
ReadonlyArray<IndicatorInstance>
|
|
197
|
-
>
|
|
198
|
-
const unsub = controller.indicators.subscribe(() => {
|
|
199
|
-
indicators.value = controller.indicators.peek()
|
|
200
|
-
})
|
|
201
|
-
onScopeDispose(unsub)
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
indicators,
|
|
205
|
-
add: controller.addIndicator.bind(controller),
|
|
206
|
-
remove: controller.removeIndicator.bind(controller),
|
|
207
|
-
updateParams: controller.updateIndicatorParams.bind(controller),
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Bridge the Chart's interactionState signal into a Vue shallowRef.
|
|
213
|
-
*/
|
|
214
|
-
export function useInteractionState(
|
|
215
|
-
controller: ChartController,
|
|
216
|
-
): Ref<InteractionSnapshot> {
|
|
217
|
-
const state = shallowRef(controller.interactionState.peek()) as Ref<InteractionSnapshot>
|
|
218
|
-
const unsub = controller.interactionState.subscribe(() => {
|
|
219
|
-
state.value = controller.interactionState.peek()
|
|
220
|
-
})
|
|
221
|
-
onScopeDispose(unsub)
|
|
222
|
-
return state
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Bridge the Chart's paneRatios signal into a Vue shallowRef.
|
|
227
|
-
*/
|
|
228
|
-
export function usePaneRatios(
|
|
229
|
-
controller: ChartController,
|
|
230
|
-
): Ref<Readonly<Record<string, number>>> {
|
|
231
|
-
const ratios = shallowRef(controller.paneRatios.peek()) as Ref<
|
|
232
|
-
Readonly<Record<string, number>>
|
|
233
|
-
>
|
|
234
|
-
const unsub = controller.paneRatios.subscribe(() => {
|
|
235
|
-
ratios.value = controller.paneRatios.peek()
|
|
236
|
-
})
|
|
237
|
-
onScopeDispose(unsub)
|
|
238
|
-
return ratios
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Bridge the Chart's viewport signal into a Vue shallowRef.
|
|
243
|
-
*/
|
|
244
|
-
export function useViewport(
|
|
245
|
-
controller: ChartController,
|
|
246
|
-
): Ref<ChartViewport> {
|
|
247
|
-
const vp = shallowRef(controller.viewport.peek()) as Ref<ChartViewport>
|
|
248
|
-
const unsub = controller.viewport.subscribe(() => {
|
|
249
|
-
vp.value = controller.viewport.peek()
|
|
250
|
-
})
|
|
251
|
-
onScopeDispose(unsub)
|
|
252
|
-
return vp
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// <KLineChart /> SFC-equivalent component
|
|
257
|
-
//
|
|
258
|
-
// Implemented with defineComponent + render function rather than a `.vue`
|
|
259
|
-
// SFC to keep the package buildable with plain `tsc` (no SFC compiler in
|
|
260
|
-
// the publishable pipeline). Mirrors the legacy KLineChart.vue prop names
|
|
261
|
-
// that downstream consumers depend on.
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
|
|
264
|
-
export const KLineChart = defineComponent({
|
|
265
|
-
name: 'KLineChart',
|
|
266
|
-
props: {
|
|
267
|
-
data: {
|
|
268
|
-
type: Array as PropType<ReadonlyArray<KLineData>>,
|
|
269
|
-
required: true,
|
|
270
|
-
},
|
|
271
|
-
initialZoomLevel: { type: Number, default: 3 },
|
|
272
|
-
zoomLevels: { type: Number, default: 20 },
|
|
273
|
-
theme: {
|
|
274
|
-
type: String as PropType<'light' | 'dark'>,
|
|
275
|
-
default: 'light',
|
|
276
|
-
},
|
|
277
|
-
/** custom class for the chart container root */
|
|
278
|
-
containerClass: { type: String, default: '' },
|
|
279
|
-
},
|
|
280
|
-
emits: {
|
|
281
|
-
ready: (_controller: ChartController) => true,
|
|
282
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
283
|
-
zoomLevelChange: (_level: number, _kWidth: number) => true,
|
|
284
|
-
},
|
|
285
|
-
setup(props, { emit, expose }) {
|
|
286
|
-
const containerRef = shallowRef<HTMLElement | null>(null)
|
|
287
|
-
const scope = effectScope()
|
|
288
|
-
|
|
289
|
-
const chart = shallowRef<ChartController | null>(null)
|
|
290
|
-
|
|
291
|
-
onMounted(() => {
|
|
292
|
-
const el = containerRef.value
|
|
293
|
-
if (el == null) return
|
|
294
|
-
scope.run(() => {
|
|
295
|
-
chart.value = createChart({
|
|
296
|
-
container: el,
|
|
297
|
-
data: props.data,
|
|
298
|
-
initialZoomLevel: props.initialZoomLevel,
|
|
299
|
-
zoomLevels: props.zoomLevels,
|
|
300
|
-
theme: props.theme,
|
|
301
|
-
})
|
|
302
|
-
if (chart.value != null) {
|
|
303
|
-
emit('ready', chart.value)
|
|
304
|
-
// Bridge viewport changes back out as zoomLevelChange.
|
|
305
|
-
const ctrl = chart.value
|
|
306
|
-
const emitViewport = (): void => {
|
|
307
|
-
const vp = ctrl.viewport.peek()
|
|
308
|
-
emit('zoomLevelChange', vp.zoomLevel, vp.kWidth)
|
|
309
|
-
}
|
|
310
|
-
emitViewport()
|
|
311
|
-
const unsub = ctrl.viewport.subscribe(emitViewport)
|
|
312
|
-
onScopeDispose(unsub)
|
|
313
|
-
}
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
// React to prop changes: data + theme.
|
|
317
|
-
watch(
|
|
318
|
-
() => props.data,
|
|
319
|
-
(next) => {
|
|
320
|
-
chart.value?.setData(next)
|
|
321
|
-
},
|
|
322
|
-
)
|
|
323
|
-
watch(
|
|
324
|
-
() => props.theme,
|
|
325
|
-
(next) => {
|
|
326
|
-
chart.value?.setTheme(next)
|
|
327
|
-
},
|
|
328
|
-
)
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
onUnmounted(() => {
|
|
332
|
-
chart.value?.dispose()
|
|
333
|
-
chart.value = null
|
|
334
|
-
scope.stop()
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
expose({
|
|
338
|
-
getController: (): ChartController | null => chart.value,
|
|
339
|
-
handlePointerEvent: (
|
|
340
|
-
e: PointerEvent,
|
|
341
|
-
drawingController?: Parameters<ChartController['handlePointerEvent']>[1],
|
|
342
|
-
): boolean => chart.value?.handlePointerEvent(e, drawingController) ?? false,
|
|
343
|
-
handleWheelEvent: (e: WheelEvent): void => chart.value?.handleWheelEvent(e),
|
|
344
|
-
handleScrollEvent: (): void => chart.value?.handleScrollEvent(),
|
|
345
|
-
zoomToLevel: (level: number, anchorX?: number): void =>
|
|
346
|
-
chart.value?.zoomToLevel(level, anchorX),
|
|
347
|
-
zoomIn: (anchorX?: number): void => chart.value?.zoomIn(anchorX),
|
|
348
|
-
zoomOut: (anchorX?: number): void => chart.value?.zoomOut(anchorX),
|
|
349
|
-
addIndicator: (
|
|
350
|
-
definitionId: string,
|
|
351
|
-
role: 'main' | 'sub',
|
|
352
|
-
params?: Record<string, unknown>,
|
|
353
|
-
): string | null => chart.value?.addIndicator(definitionId, role, params) ?? null,
|
|
354
|
-
removeIndicator: (instanceId: string): boolean =>
|
|
355
|
-
chart.value?.removeIndicator(instanceId) ?? false,
|
|
356
|
-
setTheme: (theme: 'light' | 'dark'): void => chart.value?.setTheme(theme),
|
|
357
|
-
setData: (next: ReadonlyArray<KLineData>): void => chart.value?.setData(next),
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
const setContainerRef = (el: unknown): void => {
|
|
361
|
-
containerRef.value = (el as HTMLElement | null) ?? null
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return () =>
|
|
365
|
-
h('div', {
|
|
366
|
-
ref: setContainerRef,
|
|
367
|
-
class: ['klinechart-quant-root', props.containerClass]
|
|
368
|
-
.filter(Boolean)
|
|
369
|
-
.join(' '),
|
|
370
|
-
style: { width: '100%', height: '100%' },
|
|
371
|
-
})
|
|
372
|
-
},
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
// ---------------------------------------------------------------------------
|
|
376
|
-
// KMapPlugin �?legacy Vue plugin
|
|
377
|
-
//
|
|
378
|
-
// PRESERVE THIS EXACT SHAPE �?legacy consumers do:
|
|
379
|
-
// import { KMapPlugin } from '@363045841yyt/klinechart'
|
|
380
|
-
// app.use(KMapPlugin)
|
|
381
|
-
// ---------------------------------------------------------------------------
|
|
382
|
-
|
|
383
|
-
export const KMapPlugin = {
|
|
384
|
-
install(app: App): void {
|
|
385
|
-
app.component('KLineChart', KLineChart)
|
|
386
|
-
},
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ---------------------------------------------------------------------------
|
|
390
|
-
// Auto-register the production ChartControllerFactory
|
|
391
|
-
//
|
|
392
|
-
// Consumers don't need to call __setControllerFactory manually unless they
|
|
393
|
-
// want to inject a custom backing (e.g. for testing). Contract tests
|
|
394
|
-
// override via __setControllerFactory in their setup and reset to null in
|
|
395
|
-
// afterEach, so this default registration is transparent to them.
|
|
396
|
-
//
|
|
397
|
-
// Importing the factory is side-effect-free at module load �?the engine's
|
|
398
|
-
// DOM access only happens when `createChart(opts)` is actually called.
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
import { createChartController } from '@363045841yyt/klinechart-core'
|
|
401
|
-
__setControllerFactory(createChartController)
|
|
402
|
-
export { VERSION, CORE_VERSION } from './version'
|
|
1
|
+
/**
|
|
2
|
+
* @363045841yyt/klinechart — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* Vue 3 bindings for @363045841yyt/klinechart-core. Bridges core signals to Vue's
|
|
5
|
+
* reactivity via `shallowRef` + `effect` so each adapter owns its own
|
|
6
|
+
* reactivity boundary — no proxy wrapping of immutable signal values.
|
|
7
|
+
*
|
|
8
|
+
* Backward-compatibility contract: `KMapPlugin.install(app)` MUST exist
|
|
9
|
+
* because legacy users of `@363045841yyt/klinechart` consume it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
defineComponent,
|
|
14
|
+
effectScope,
|
|
15
|
+
h,
|
|
16
|
+
onBeforeUnmount,
|
|
17
|
+
onMounted,
|
|
18
|
+
onScopeDispose,
|
|
19
|
+
onUnmounted,
|
|
20
|
+
shallowRef,
|
|
21
|
+
watch,
|
|
22
|
+
type App,
|
|
23
|
+
type PropType,
|
|
24
|
+
type Ref,
|
|
25
|
+
} from 'vue'
|
|
26
|
+
import type { Signal } from '@363045841yyt/klinechart-core/reactivity'
|
|
27
|
+
import type {
|
|
28
|
+
ChartController,
|
|
29
|
+
ChartControllerFactory,
|
|
30
|
+
ChartMountOptions,
|
|
31
|
+
ChartViewport,
|
|
32
|
+
IndicatorInstance,
|
|
33
|
+
InteractionSnapshot,
|
|
34
|
+
KLineData,
|
|
35
|
+
} from '@363045841yyt/klinechart-core'
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
ChartController,
|
|
39
|
+
ChartMountOptions,
|
|
40
|
+
ChartViewport,
|
|
41
|
+
} from '@363045841yyt/klinechart-core'
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// SFC components (for consumers using Vite / SFC compiler)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
DrawingStyleToolbar,
|
|
49
|
+
IndicatorParams,
|
|
50
|
+
IndicatorSelector,
|
|
51
|
+
KLineChartVue,
|
|
52
|
+
KLineTooltip,
|
|
53
|
+
LeftToolbar,
|
|
54
|
+
MarkerTooltip,
|
|
55
|
+
} from './components/index'
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Controller factory injection
|
|
59
|
+
//
|
|
60
|
+
// The concrete `createChartController` from packages/core/src/controllers/
|
|
61
|
+
// (Phase 1A deliverable) is not yet wired. Adapters and tests inject a
|
|
62
|
+
// factory via `__setControllerFactory` so the public API surface stays stable.
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
let controllerFactory: ChartControllerFactory | null = null
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Inject the ChartController factory. Called by:
|
|
69
|
+
* - the core package's bootstrap once `createChartController` is implemented
|
|
70
|
+
* - tests that need a mock controller
|
|
71
|
+
*/
|
|
72
|
+
export function __setControllerFactory(
|
|
73
|
+
factory: ChartControllerFactory | null,
|
|
74
|
+
): void {
|
|
75
|
+
controllerFactory = factory
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// createChart �?imperative mount
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Imperative mount API. Returns a controller; caller is responsible for `dispose`.
|
|
84
|
+
*
|
|
85
|
+
* Throws if container is null/undefined (SSR-safe guard).
|
|
86
|
+
*/
|
|
87
|
+
export function createChart(opts: ChartMountOptions): ChartController {
|
|
88
|
+
if (opts.container == null) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'[@363045841yyt/klinechart] createChart: `container` is required and must be a non-null HTMLElement',
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
if (controllerFactory === null) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'[@363045841yyt/klinechart] createChart: no ChartController factory registered. ' +
|
|
96
|
+
'Call __setControllerFactory(...) before mounting (the core package wires this in production).',
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return controllerFactory(opts)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// coreSignalToVueRef �?reactivity bridge
|
|
104
|
+
//
|
|
105
|
+
// Subscribe to a core Signal and mirror its value in a shallowRef. Auto-cleanup
|
|
106
|
+
// on component teardown via onScopeDispose (works inside effectScope or SFC).
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Bridge a core Signal<T> into a Vue Ref<T> backed by `shallowRef`.
|
|
111
|
+
*
|
|
112
|
+
* We use `shallowRef` (not `ref`) because:
|
|
113
|
+
* - core signal values are treated as immutable; deep proxying is wasteful
|
|
114
|
+
* - `Object.is` short-circuits in the core depend on referential equality,
|
|
115
|
+
* which Vue's deep reactivity would silently break
|
|
116
|
+
*
|
|
117
|
+
* Subscription is torn down via `onScopeDispose`, so this is safe to call
|
|
118
|
+
* inside a Vue component setup, a composable, or a manually-created
|
|
119
|
+
* `effectScope`. Calling it outside any scope still returns a working ref �?
|
|
120
|
+
* the caller is then responsible for unsubscribing.
|
|
121
|
+
*/
|
|
122
|
+
export function coreSignalToVueRef<T>(signal: Signal<T>): Ref<T> {
|
|
123
|
+
const ref = shallowRef(signal.peek()) as Ref<T>
|
|
124
|
+
const unsub = signal.subscribe(() => {
|
|
125
|
+
ref.value = signal.peek()
|
|
126
|
+
})
|
|
127
|
+
onScopeDispose(unsub)
|
|
128
|
+
return ref
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// useChart �?composable
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Composable. Pass a template ref to the container element.
|
|
137
|
+
*
|
|
138
|
+
* Watches `containerRef` to populate; once it does, calls `createChart`
|
|
139
|
+
* and exposes the controller via a `shallowRef`. Disposes on scope teardown.
|
|
140
|
+
*/
|
|
141
|
+
export function useChart(
|
|
142
|
+
containerRef: Ref<HTMLElement | null>,
|
|
143
|
+
opts: Omit<ChartMountOptions, 'container'>,
|
|
144
|
+
): { chart: Ref<ChartController | null> } {
|
|
145
|
+
const chart = shallowRef<ChartController | null>(null)
|
|
146
|
+
|
|
147
|
+
const mountIfReady = (el: HTMLElement | null): void => {
|
|
148
|
+
if (el == null || chart.value != null) return
|
|
149
|
+
chart.value = createChart({ ...opts, container: el })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Mount synchronously if the ref is already populated (e.g. SFC where the
|
|
153
|
+
// template ref is set before this composable's effect runs).
|
|
154
|
+
mountIfReady(containerRef.value)
|
|
155
|
+
|
|
156
|
+
// Otherwise watch for the ref to populate.
|
|
157
|
+
const stopWatch = watch(
|
|
158
|
+
containerRef,
|
|
159
|
+
(el) => {
|
|
160
|
+
mountIfReady(el)
|
|
161
|
+
},
|
|
162
|
+
{ immediate: true, flush: 'post' },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const dispose = (): void => {
|
|
166
|
+
stopWatch()
|
|
167
|
+
const ctrl = chart.value
|
|
168
|
+
if (ctrl != null) {
|
|
169
|
+
ctrl.dispose()
|
|
170
|
+
chart.value = null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onScopeDispose(dispose)
|
|
175
|
+
// Belt-and-braces: SFC components running outside a manual scope still get
|
|
176
|
+
// unmount cleanup via the component lifecycle hook.
|
|
177
|
+
onBeforeUnmount(dispose)
|
|
178
|
+
|
|
179
|
+
return { chart }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// useIndicators �?composable
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Bridge the Chart's indicators signal into a Vue shallowRef.
|
|
188
|
+
*/
|
|
189
|
+
export function useIndicators(controller: ChartController): {
|
|
190
|
+
indicators: Ref<ReadonlyArray<IndicatorInstance>>
|
|
191
|
+
add: ChartController['addIndicator']
|
|
192
|
+
remove: ChartController['removeIndicator']
|
|
193
|
+
updateParams: ChartController['updateIndicatorParams']
|
|
194
|
+
} {
|
|
195
|
+
const indicators = shallowRef(controller.indicators.peek()) as Ref<
|
|
196
|
+
ReadonlyArray<IndicatorInstance>
|
|
197
|
+
>
|
|
198
|
+
const unsub = controller.indicators.subscribe(() => {
|
|
199
|
+
indicators.value = controller.indicators.peek()
|
|
200
|
+
})
|
|
201
|
+
onScopeDispose(unsub)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
indicators,
|
|
205
|
+
add: controller.addIndicator.bind(controller),
|
|
206
|
+
remove: controller.removeIndicator.bind(controller),
|
|
207
|
+
updateParams: controller.updateIndicatorParams.bind(controller),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Bridge the Chart's interactionState signal into a Vue shallowRef.
|
|
213
|
+
*/
|
|
214
|
+
export function useInteractionState(
|
|
215
|
+
controller: ChartController,
|
|
216
|
+
): Ref<InteractionSnapshot> {
|
|
217
|
+
const state = shallowRef(controller.interactionState.peek()) as Ref<InteractionSnapshot>
|
|
218
|
+
const unsub = controller.interactionState.subscribe(() => {
|
|
219
|
+
state.value = controller.interactionState.peek()
|
|
220
|
+
})
|
|
221
|
+
onScopeDispose(unsub)
|
|
222
|
+
return state
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bridge the Chart's paneRatios signal into a Vue shallowRef.
|
|
227
|
+
*/
|
|
228
|
+
export function usePaneRatios(
|
|
229
|
+
controller: ChartController,
|
|
230
|
+
): Ref<Readonly<Record<string, number>>> {
|
|
231
|
+
const ratios = shallowRef(controller.paneRatios.peek()) as Ref<
|
|
232
|
+
Readonly<Record<string, number>>
|
|
233
|
+
>
|
|
234
|
+
const unsub = controller.paneRatios.subscribe(() => {
|
|
235
|
+
ratios.value = controller.paneRatios.peek()
|
|
236
|
+
})
|
|
237
|
+
onScopeDispose(unsub)
|
|
238
|
+
return ratios
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Bridge the Chart's viewport signal into a Vue shallowRef.
|
|
243
|
+
*/
|
|
244
|
+
export function useViewport(
|
|
245
|
+
controller: ChartController,
|
|
246
|
+
): Ref<ChartViewport> {
|
|
247
|
+
const vp = shallowRef(controller.viewport.peek()) as Ref<ChartViewport>
|
|
248
|
+
const unsub = controller.viewport.subscribe(() => {
|
|
249
|
+
vp.value = controller.viewport.peek()
|
|
250
|
+
})
|
|
251
|
+
onScopeDispose(unsub)
|
|
252
|
+
return vp
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// <KLineChart /> SFC-equivalent component
|
|
257
|
+
//
|
|
258
|
+
// Implemented with defineComponent + render function rather than a `.vue`
|
|
259
|
+
// SFC to keep the package buildable with plain `tsc` (no SFC compiler in
|
|
260
|
+
// the publishable pipeline). Mirrors the legacy KLineChart.vue prop names
|
|
261
|
+
// that downstream consumers depend on.
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
export const KLineChart = defineComponent({
|
|
265
|
+
name: 'KLineChart',
|
|
266
|
+
props: {
|
|
267
|
+
data: {
|
|
268
|
+
type: Array as PropType<ReadonlyArray<KLineData>>,
|
|
269
|
+
required: true,
|
|
270
|
+
},
|
|
271
|
+
initialZoomLevel: { type: Number, default: 3 },
|
|
272
|
+
zoomLevels: { type: Number, default: 20 },
|
|
273
|
+
theme: {
|
|
274
|
+
type: String as PropType<'light' | 'dark'>,
|
|
275
|
+
default: 'light',
|
|
276
|
+
},
|
|
277
|
+
/** custom class for the chart container root */
|
|
278
|
+
containerClass: { type: String, default: '' },
|
|
279
|
+
},
|
|
280
|
+
emits: {
|
|
281
|
+
ready: (_controller: ChartController) => true,
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
283
|
+
zoomLevelChange: (_level: number, _kWidth: number) => true,
|
|
284
|
+
},
|
|
285
|
+
setup(props, { emit, expose }) {
|
|
286
|
+
const containerRef = shallowRef<HTMLElement | null>(null)
|
|
287
|
+
const scope = effectScope()
|
|
288
|
+
|
|
289
|
+
const chart = shallowRef<ChartController | null>(null)
|
|
290
|
+
|
|
291
|
+
onMounted(() => {
|
|
292
|
+
const el = containerRef.value
|
|
293
|
+
if (el == null) return
|
|
294
|
+
scope.run(() => {
|
|
295
|
+
chart.value = createChart({
|
|
296
|
+
container: el,
|
|
297
|
+
data: props.data,
|
|
298
|
+
initialZoomLevel: props.initialZoomLevel,
|
|
299
|
+
zoomLevels: props.zoomLevels,
|
|
300
|
+
theme: props.theme,
|
|
301
|
+
})
|
|
302
|
+
if (chart.value != null) {
|
|
303
|
+
emit('ready', chart.value)
|
|
304
|
+
// Bridge viewport changes back out as zoomLevelChange.
|
|
305
|
+
const ctrl = chart.value
|
|
306
|
+
const emitViewport = (): void => {
|
|
307
|
+
const vp = ctrl.viewport.peek()
|
|
308
|
+
emit('zoomLevelChange', vp.zoomLevel, vp.kWidth)
|
|
309
|
+
}
|
|
310
|
+
emitViewport()
|
|
311
|
+
const unsub = ctrl.viewport.subscribe(emitViewport)
|
|
312
|
+
onScopeDispose(unsub)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// React to prop changes: data + theme.
|
|
317
|
+
watch(
|
|
318
|
+
() => props.data,
|
|
319
|
+
(next) => {
|
|
320
|
+
chart.value?.setData(next)
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
watch(
|
|
324
|
+
() => props.theme,
|
|
325
|
+
(next) => {
|
|
326
|
+
chart.value?.setTheme(next)
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
onUnmounted(() => {
|
|
332
|
+
chart.value?.dispose()
|
|
333
|
+
chart.value = null
|
|
334
|
+
scope.stop()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
expose({
|
|
338
|
+
getController: (): ChartController | null => chart.value,
|
|
339
|
+
handlePointerEvent: (
|
|
340
|
+
e: PointerEvent,
|
|
341
|
+
drawingController?: Parameters<ChartController['handlePointerEvent']>[1],
|
|
342
|
+
): boolean => chart.value?.handlePointerEvent(e, drawingController) ?? false,
|
|
343
|
+
handleWheelEvent: (e: WheelEvent): void => chart.value?.handleWheelEvent(e),
|
|
344
|
+
handleScrollEvent: (): void => chart.value?.handleScrollEvent(),
|
|
345
|
+
zoomToLevel: (level: number, anchorX?: number): void =>
|
|
346
|
+
chart.value?.zoomToLevel(level, anchorX),
|
|
347
|
+
zoomIn: (anchorX?: number): void => chart.value?.zoomIn(anchorX),
|
|
348
|
+
zoomOut: (anchorX?: number): void => chart.value?.zoomOut(anchorX),
|
|
349
|
+
addIndicator: (
|
|
350
|
+
definitionId: string,
|
|
351
|
+
role: 'main' | 'sub',
|
|
352
|
+
params?: Record<string, unknown>,
|
|
353
|
+
): string | null => chart.value?.addIndicator(definitionId, role, params) ?? null,
|
|
354
|
+
removeIndicator: (instanceId: string): boolean =>
|
|
355
|
+
chart.value?.removeIndicator(instanceId) ?? false,
|
|
356
|
+
setTheme: (theme: 'light' | 'dark'): void => chart.value?.setTheme(theme),
|
|
357
|
+
setData: (next: ReadonlyArray<KLineData>): void => chart.value?.setData(next),
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const setContainerRef = (el: unknown): void => {
|
|
361
|
+
containerRef.value = (el as HTMLElement | null) ?? null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return () =>
|
|
365
|
+
h('div', {
|
|
366
|
+
ref: setContainerRef,
|
|
367
|
+
class: ['klinechart-quant-root', props.containerClass]
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.join(' '),
|
|
370
|
+
style: { width: '100%', height: '100%' },
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// KMapPlugin �?legacy Vue plugin
|
|
377
|
+
//
|
|
378
|
+
// PRESERVE THIS EXACT SHAPE �?legacy consumers do:
|
|
379
|
+
// import { KMapPlugin } from '@363045841yyt/klinechart'
|
|
380
|
+
// app.use(KMapPlugin)
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
export const KMapPlugin = {
|
|
384
|
+
install(app: App): void {
|
|
385
|
+
app.component('KLineChart', KLineChart)
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Auto-register the production ChartControllerFactory
|
|
391
|
+
//
|
|
392
|
+
// Consumers don't need to call __setControllerFactory manually unless they
|
|
393
|
+
// want to inject a custom backing (e.g. for testing). Contract tests
|
|
394
|
+
// override via __setControllerFactory in their setup and reset to null in
|
|
395
|
+
// afterEach, so this default registration is transparent to them.
|
|
396
|
+
//
|
|
397
|
+
// Importing the factory is side-effect-free at module load �?the engine's
|
|
398
|
+
// DOM access only happens when `createChart(opts)` is actually called.
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
import { createChartController } from '@363045841yyt/klinechart-core'
|
|
401
|
+
__setControllerFactory(createChartController)
|
|
402
|
+
export { VERSION, CORE_VERSION } from './version'
|