@363045841yyt/klinechart-core 0.9.0-alpha.2 → 0.9.0-alpha.3
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 +9 -8
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/types.d.ts +5 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data/binance.d.ts +1 -1
- package/dist/data/binance.d.ts.map +1 -1
- package/dist/data/binance.js +1 -1
- package/dist/data/binance.js.map +1 -1
- package/dist/engine/chart.d.ts +9 -9
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +69 -96
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartTypes.d.ts +5 -0
- package/dist/engine/chartTypes.d.ts.map +1 -1
- package/dist/engine/controller/interaction.d.ts +4 -2
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +18 -10
- package/dist/engine/controller/interaction.js.map +1 -1
- package/dist/engine/data/chartDataManager.d.ts +12 -18
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +46 -37
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
- package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
- package/dist/engine/data/incrementalLoadHint.js +5 -2
- package/dist/engine/data/incrementalLoadHint.js.map +1 -1
- package/dist/engine/data/scrollCompensator.d.ts +3 -13
- package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
- package/dist/engine/data/scrollCompensator.js +13 -14
- package/dist/engine/data/scrollCompensator.js.map +1 -1
- package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
- package/dist/engine/indicators/chartIndicatorManager.js +37 -37
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
- package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
- package/dist/engine/layout/chartPaneLayout.js +58 -16
- package/dist/engine/layout/chartPaneLayout.js.map +1 -1
- package/dist/engine/paneRenderer.d.ts +2 -0
- package/dist/engine/paneRenderer.d.ts.map +1 -1
- package/dist/engine/paneRenderer.js +15 -5
- package/dist/engine/paneRenderer.js.map +1 -1
- package/dist/engine/render/chartRenderer.d.ts +28 -22
- package/dist/engine/render/chartRenderer.d.ts.map +1 -1
- package/dist/engine/render/chartRenderer.js +117 -69
- package/dist/engine/render/chartRenderer.js.map +1 -1
- package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
- package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
- package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
- package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
- package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
- package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
- package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/yAxisLayer.js +11 -2
- package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +72 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +177 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
- package/dist/engine/renderers/leftYAxis.d.ts +15 -2
- package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
- package/dist/engine/renderers/leftYAxis.js +56 -16
- package/dist/engine/renderers/leftYAxis.js.map +1 -1
- package/dist/engine/renderers/yAxis.d.ts +16 -6
- package/dist/engine/renderers/yAxis.d.ts.map +1 -1
- package/dist/engine/renderers/yAxis.js +49 -16
- package/dist/engine/renderers/yAxis.js.map +1 -1
- package/dist/engine/state/viewportState.d.ts +4 -1
- package/dist/engine/state/viewportState.d.ts.map +1 -1
- package/dist/engine/state/viewportState.js +40 -8
- package/dist/engine/state/viewportState.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +5 -3
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +3 -2
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +9 -7
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
- package/dist/engine/utils/chartZoomController.js +13 -12
- package/dist/engine/utils/chartZoomController.js.map +1 -1
- package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
- package/dist/engine/viewport/chartViewportManager.js +7 -48
- package/dist/engine/viewport/chartViewportManager.js.map +1 -1
- package/dist/engine/viewport/viewport.d.ts +14 -0
- package/dist/engine/viewport/viewport.d.ts.map +1 -1
- package/dist/engine/viewport/viewport.js +10 -0
- package/dist/engine/viewport/viewport.js.map +1 -1
- package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
- package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
- package/dist/foundation/plugin/types.d.ts +3 -0
- package/dist/foundation/plugin/types.d.ts.map +1 -1
- package/dist/foundation/plugin/types.js.map +1 -1
- package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
- package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
- package/dist/foundation/reactivity/frameTransaction.js +17 -2
- package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
- package/dist/rendering/render/Renderer.d.ts +58 -74
- package/dist/rendering/render/Renderer.d.ts.map +1 -1
- package/dist/rendering/render/Renderer.js +8 -18
- package/dist/rendering/render/Renderer.js.map +1 -1
- package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
- package/dist/rendering/render/createWebGPURenderer.js +30 -17
- package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
- package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
- package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
- package/dist/rendering/render/physicalLine.d.ts +4 -0
- package/dist/rendering/render/physicalLine.d.ts.map +1 -0
- package/dist/rendering/render/physicalLine.js +37 -0
- package/dist/rendering/render/physicalLine.js.map +1 -0
- package/dist/rendering/render/physicalRegion.d.ts +14 -0
- package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
- package/dist/rendering/render/physicalRegion.js +15 -0
- package/dist/rendering/render/physicalRegion.js.map +1 -0
- package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
- package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
- package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
- package/dist/rendering/scene/createScene.d.ts.map +1 -1
- package/dist/rendering/scene/createScene.js +4 -3
- package/dist/rendering/scene/createScene.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/stateKernel.test.ts +1 -0
- package/src/controllers/createChartController.ts +9 -8
- package/src/controllers/index.ts +11 -0
- package/src/controllers/types.ts +7 -0
- package/src/data/binance.ts +1 -1
- package/src/engine/__tests__/chart.dpr.test.ts +3 -2
- package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
- package/src/engine/__tests__/subPaneManager.test.ts +5 -3
- package/src/engine/chart.ts +76 -109
- package/src/engine/chartTypes.ts +5 -0
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
- package/src/engine/controller/interaction.ts +18 -10
- package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
- package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
- package/src/engine/data/chartDataManager.ts +54 -59
- package/src/engine/data/incrementalLoadHint.ts +8 -7
- package/src/engine/data/scrollCompensator.ts +16 -24
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
- package/src/engine/indicators/chartIndicatorManager.ts +62 -62
- package/src/engine/layout/chartPaneLayout.ts +73 -21
- package/src/engine/paneRenderer.ts +17 -10
- package/src/engine/render/chartRenderer.ts +187 -129
- package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
- package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
- package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
- package/src/engine/render/layers/yAxisLayer.ts +29 -7
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
- package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +282 -0
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
- package/src/engine/renderers/leftYAxis.ts +60 -18
- package/src/engine/renderers/yAxis.ts +57 -22
- package/src/engine/state/viewportState.ts +44 -8
- package/src/engine/subPaneManager.ts +8 -5
- package/src/engine/utils/chartZoomController.ts +22 -19
- package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
- package/src/engine/viewport/chartViewportManager.ts +9 -60
- package/src/engine/viewport/viewport.ts +14 -0
- package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
- package/src/foundation/plugin/types.ts +3 -0
- package/src/foundation/reactivity/frameTransaction.ts +20 -6
- package/src/rendering/render/Renderer.ts +69 -85
- package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
- package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
- package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
- package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
- package/src/rendering/render/createWebGPURenderer.ts +30 -17
- package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
- package/src/rendering/render/physicalLine.ts +45 -0
- package/src/rendering/render/physicalRegion.ts +32 -0
- package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
- package/src/rendering/scene/createScene.ts +4 -3
- package/src/version.ts +1 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '../../foundation/reactivity/signal'
|
|
8
8
|
import type { Viewport, ViewportState } from '../chartTypes'
|
|
9
9
|
import type { VisibleRange } from '../layout/pane'
|
|
10
|
-
import { getVisibleRange } from '../viewport/viewport'
|
|
10
|
+
import { clampVisibleRange, getVisibleRange } from '../viewport/viewport'
|
|
11
11
|
import {
|
|
12
12
|
computeLeftLoadBufferWidth as pureLeftBuffer,
|
|
13
13
|
computeContentWidth as pureContentWidth,
|
|
@@ -197,8 +197,8 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
|
|
|
197
197
|
return vp
|
|
198
198
|
})
|
|
199
199
|
|
|
200
|
-
let
|
|
201
|
-
const
|
|
200
|
+
let _cachedRawVisibleRange: VisibleRange | null = null
|
|
201
|
+
const cachedRawVisibleRange = computed<VisibleRange>(() => {
|
|
202
202
|
const vp = cachedViewport()
|
|
203
203
|
const opts = signalDeps.options$()
|
|
204
204
|
const vr = getVisibleRange(
|
|
@@ -209,13 +209,32 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
|
|
|
209
209
|
signalDeps.dataLength$(),
|
|
210
210
|
vp.dpr,
|
|
211
211
|
)
|
|
212
|
-
if (
|
|
213
|
-
|
|
212
|
+
if (
|
|
213
|
+
_cachedRawVisibleRange &&
|
|
214
|
+
_cachedRawVisibleRange.start === vr.start &&
|
|
215
|
+
_cachedRawVisibleRange.end === vr.end
|
|
216
|
+
) {
|
|
217
|
+
return _cachedRawVisibleRange
|
|
214
218
|
}
|
|
215
|
-
|
|
219
|
+
_cachedRawVisibleRange = vr
|
|
216
220
|
return vr
|
|
217
221
|
})
|
|
218
222
|
|
|
223
|
+
// 可索引可见区间(start>=0)— 绘制 / hit-test / 指标 / ViewportState 的 SSOT
|
|
224
|
+
let _cachedVisibleRange: VisibleRange | null = null
|
|
225
|
+
const cachedVisibleRange = computed<VisibleRange>(() => {
|
|
226
|
+
const clamped = clampVisibleRange(cachedRawVisibleRange())
|
|
227
|
+
if (
|
|
228
|
+
_cachedVisibleRange &&
|
|
229
|
+
_cachedVisibleRange.start === clamped.start &&
|
|
230
|
+
_cachedVisibleRange.end === clamped.end
|
|
231
|
+
) {
|
|
232
|
+
return _cachedVisibleRange
|
|
233
|
+
}
|
|
234
|
+
_cachedVisibleRange = clamped
|
|
235
|
+
return clamped
|
|
236
|
+
})
|
|
237
|
+
|
|
219
238
|
let _cachedViewportState: ViewportState | null = null
|
|
220
239
|
const cachedViewportState = computed<ViewportState>(() => {
|
|
221
240
|
const vp = cachedViewport()
|
|
@@ -355,6 +374,9 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
|
|
|
355
374
|
scrollLeftLogical,
|
|
356
375
|
kGap,
|
|
357
376
|
viewport: cachedViewport,
|
|
377
|
+
/** raw:含左右扩窗,start 可能为 -1(增量加载左缘检测) */
|
|
378
|
+
rawVisibleRange: cachedRawVisibleRange,
|
|
379
|
+
/** clamped:start>=0,绘制 / hit-test / 指标 / ViewportState 的 SSOT */
|
|
358
380
|
visibleRange: cachedVisibleRange,
|
|
359
381
|
viewportState: cachedViewportState,
|
|
360
382
|
}
|
|
@@ -414,7 +436,7 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
|
|
|
414
436
|
/**
|
|
415
437
|
* 初始化视口状态。
|
|
416
438
|
*
|
|
417
|
-
* @remarks 从 DOM
|
|
439
|
+
* @remarks 从 DOM 容器读取首帧尺寸与 scrollLeft,并挂载 canvas 尺寸同步 effect。
|
|
418
440
|
* 重复调用安全(仅首次生效)。
|
|
419
441
|
*/
|
|
420
442
|
init() {
|
|
@@ -423,7 +445,21 @@ export function createViewportState(signalDeps: ViewportSignalDeps) {
|
|
|
423
445
|
const container = _getDom().container
|
|
424
446
|
if (!container) return
|
|
425
447
|
signals.initialized.set(true)
|
|
426
|
-
|
|
448
|
+
// 首帧尺寸写入 kernel,避免 paint 路径读 DOM fallback
|
|
449
|
+
const w = Math.max(0, Math.round(container.clientWidth))
|
|
450
|
+
const h = Math.max(0, Math.round(container.clientHeight))
|
|
451
|
+
batch(() => {
|
|
452
|
+
if (w > 0 && h > 0) {
|
|
453
|
+
signals.viewWidth.set(w)
|
|
454
|
+
signals.viewHeight.set(h)
|
|
455
|
+
}
|
|
456
|
+
// 与 resize 一致:首帧 scrollLeft 为 0 时落到 viewWidth
|
|
457
|
+
if (container.scrollLeft === 0 && w > 0) {
|
|
458
|
+
signals.requestedScrollLeft.set(w)
|
|
459
|
+
} else {
|
|
460
|
+
setRequestedScrollLeft(container.scrollLeft)
|
|
461
|
+
}
|
|
462
|
+
})
|
|
427
463
|
setupCanvasSync()
|
|
428
464
|
},
|
|
429
465
|
},
|
|
@@ -44,9 +44,11 @@ export interface SubPaneContext {
|
|
|
44
44
|
removeRenderer: (name: string) => void
|
|
45
45
|
setRendererEnabled: (name: string, enabled: boolean) => void
|
|
46
46
|
updateRendererConfig: (name: string, config: Record<string, unknown>) => void
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
getOption: () => {
|
|
48
|
+
rightAxisWidth: number
|
|
49
|
+
priceLabelWidth?: number
|
|
50
|
+
yPaddingPx: number
|
|
51
|
+
}
|
|
50
52
|
getCrosshairPos: () => { x: number; y: number } | null
|
|
51
53
|
getCrosshairPrice: () => number | null
|
|
52
54
|
getActivePaneId: () => string | null
|
|
@@ -203,7 +205,8 @@ export class SubPaneManager {
|
|
|
203
205
|
return
|
|
204
206
|
}
|
|
205
207
|
const definition = ctx.getIndicatorScheduler().getIndicatorMetadata(entry.indicatorId)
|
|
206
|
-
const
|
|
208
|
+
const opt = ctx.getOption()
|
|
209
|
+
const axisWidth = opt.rightAxisWidth + (opt.priceLabelWidth ?? 60)
|
|
207
210
|
const getCrosshair = () => {
|
|
208
211
|
const pos = ctx.getCrosshairPos()
|
|
209
212
|
const price = ctx.getCrosshairPrice()
|
|
@@ -213,7 +216,7 @@ export class SubPaneManager {
|
|
|
213
216
|
const options = {
|
|
214
217
|
axisWidth,
|
|
215
218
|
paneId: entry.paneId,
|
|
216
|
-
yPaddingPx:
|
|
219
|
+
yPaddingPx: opt.yPaddingPx,
|
|
217
220
|
getCrosshair,
|
|
218
221
|
}
|
|
219
222
|
const plugin = definition?.scaleRendererFactory
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { computeZoom, deriveKGap } from './zoom'
|
|
2
|
+
import type { ReadonlySignal } from '../../foundation/reactivity/signal'
|
|
3
|
+
import type { OptionsStateModule } from '../state/optionsState'
|
|
2
4
|
import type { ZoomStateModule } from '../state/zoomState'
|
|
5
|
+
import type { ViewportStateModule } from '../state/viewportState'
|
|
3
6
|
|
|
4
7
|
export interface ZoomDependencies {
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
/** scroll / dpr 几何,读写走 kernel.viewport */
|
|
9
|
+
viewport: ViewportStateModule
|
|
10
|
+
/** min/max kWidth / zoomLevelCount SSOT */
|
|
11
|
+
options: OptionsStateModule
|
|
12
|
+
/** 当前周期(kGap 推导) */
|
|
13
|
+
period$: ReadonlySignal<string>
|
|
7
14
|
getClientWidth: () => number
|
|
8
15
|
getDataLength: () => number
|
|
9
16
|
getPlotWidth: () => number
|
|
10
|
-
setScrollLeft: (v: number) => void
|
|
11
17
|
onChange?: () => void
|
|
12
|
-
getMinKWidth: () => number
|
|
13
|
-
getMaxKWidth: () => number
|
|
14
|
-
getPeriod: () => string
|
|
15
|
-
zoomLevelCount: number
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -39,17 +41,17 @@ export class ChartZoomController {
|
|
|
39
41
|
get currentKGap(): number {
|
|
40
42
|
return deriveKGap({
|
|
41
43
|
kWidth: this.currentKWidth,
|
|
42
|
-
dpr: this.deps.
|
|
43
|
-
period: this.deps.
|
|
44
|
+
dpr: this.deps.viewport.readonly.dpr.peek(),
|
|
45
|
+
period: this.deps.period$.peek(),
|
|
44
46
|
})
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
get zoomLevelCount(): number {
|
|
48
|
-
return this.deps.zoomLevelCount
|
|
50
|
+
return this.deps.options.readonly.options.peek().zoomLevelCount
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
zoomToLevel(level: number, anchorX?: number): void {
|
|
52
|
-
const clamped = Math.max(1, Math.min(this.
|
|
54
|
+
const clamped = Math.max(1, Math.min(this.zoomLevelCount, Math.round(level)))
|
|
53
55
|
this.applyZoom(clamped, anchorX)
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -65,7 +67,7 @@ export class ChartZoomController {
|
|
|
65
67
|
const delta = deltaY > 0 ? -1 : 1
|
|
66
68
|
const targetLevel = Math.max(
|
|
67
69
|
1,
|
|
68
|
-
Math.min(this.
|
|
70
|
+
Math.min(this.zoomLevelCount, this.currentZoomLevel + delta),
|
|
69
71
|
)
|
|
70
72
|
if (targetLevel === this.currentZoomLevel) return
|
|
71
73
|
this.applyZoom(targetLevel, viewportX)
|
|
@@ -74,7 +76,7 @@ export class ChartZoomController {
|
|
|
74
76
|
handlePinch(delta: number, centerClientX: number): void {
|
|
75
77
|
const targetLevel = Math.max(
|
|
76
78
|
1,
|
|
77
|
-
Math.min(this.
|
|
79
|
+
Math.min(this.zoomLevelCount, this.currentZoomLevel + delta),
|
|
78
80
|
)
|
|
79
81
|
if (targetLevel === this.currentZoomLevel) return
|
|
80
82
|
this.applyZoom(targetLevel, centerClientX)
|
|
@@ -84,8 +86,9 @@ export class ChartZoomController {
|
|
|
84
86
|
if (targetLevel === this.currentZoomLevel) return
|
|
85
87
|
|
|
86
88
|
const delta = targetLevel - this.currentZoomLevel
|
|
87
|
-
const logicalScrollLeft = this.deps.
|
|
88
|
-
const dpr = this.deps.
|
|
89
|
+
const logicalScrollLeft = this.deps.viewport.readonly.scrollLeftLogical.peek()
|
|
90
|
+
const dpr = this.deps.viewport.readonly.dpr.peek()
|
|
91
|
+
const opt = this.deps.options.readonly.options.peek()
|
|
89
92
|
|
|
90
93
|
const result = computeZoom(
|
|
91
94
|
delta,
|
|
@@ -95,9 +98,9 @@ export class ChartZoomController {
|
|
|
95
98
|
this.currentKWidth,
|
|
96
99
|
this.currentKGap,
|
|
97
100
|
{
|
|
98
|
-
minKWidth:
|
|
99
|
-
maxKWidth:
|
|
100
|
-
zoomLevelCount:
|
|
101
|
+
minKWidth: opt.minKWidth,
|
|
102
|
+
maxKWidth: opt.maxKWidth,
|
|
103
|
+
zoomLevelCount: opt.zoomLevelCount,
|
|
101
104
|
dpr,
|
|
102
105
|
dataLength: this.deps.getDataLength(),
|
|
103
106
|
plotWidth: this.deps.getPlotWidth(),
|
|
@@ -108,7 +111,7 @@ export class ChartZoomController {
|
|
|
108
111
|
if (!result) return
|
|
109
112
|
|
|
110
113
|
this.zoomState.actions.setZoomLevel(result.targetLevel)
|
|
111
|
-
this.deps.
|
|
114
|
+
this.deps.viewport.actions.scrollTo(result.newDomScrollLeft)
|
|
112
115
|
this.deps.onChange?.()
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createSignal } from '../../../foundation/reactivity/signal'
|
|
4
|
+
import { createViewportState } from '../../state/viewportState'
|
|
5
|
+
import { clampVisibleRange, getVisibleRange } from '../viewport'
|
|
6
|
+
|
|
7
|
+
describe('clampVisibleRange', () => {
|
|
8
|
+
it('clamps negative start to 0 and preserves end', () => {
|
|
9
|
+
expect(clampVisibleRange({ start: -1, end: 40 })).toEqual({ start: 0, end: 40 })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('leaves non-negative start unchanged', () => {
|
|
13
|
+
expect(clampVisibleRange({ start: 12, end: 50 })).toEqual({ start: 12, end: 50 })
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('getVisibleRange raw expansion', () => {
|
|
18
|
+
it('may return start < 0 when scroll is near the left edge (expansion pad)', () => {
|
|
19
|
+
// scrollLeft=0, first bars sit near plot origin → start-1 扩窗
|
|
20
|
+
const raw = getVisibleRange(0, 800, 8, 2, 100, 1)
|
|
21
|
+
expect(raw.start).toBeLessThan(0)
|
|
22
|
+
expect(raw.end).toBeGreaterThan(0)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('viewportState visibleRange SSOT', () => {
|
|
27
|
+
it('exposes clamped visibleRange/visibleFrom while keeping rawVisibleRange for load triggers', async () => {
|
|
28
|
+
const dataLength$ = createSignal(20)
|
|
29
|
+
const module = createViewportState({
|
|
30
|
+
options$: (() => ({ bottomAxisHeight: 30, kWidth: 8, kGap: 2 })) as any,
|
|
31
|
+
dataLength$,
|
|
32
|
+
period$: (() => 'daily') as any,
|
|
33
|
+
zoomLevel$: (() => 3) as any,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// resize 后 scrollLeftLogical≈0(右对齐短序列或左缘),raw start 常为 -1
|
|
37
|
+
module.actions.resize(800, 400, 1)
|
|
38
|
+
module.actions.scrollTo(module.readonly.leftLoadBufferWidth.peek())
|
|
39
|
+
|
|
40
|
+
const raw = module.readonly.rawVisibleRange()
|
|
41
|
+
const clamped = module.readonly.visibleRange()
|
|
42
|
+
const vs = module.readonly.viewportState()
|
|
43
|
+
|
|
44
|
+
expect(raw.start).toBeLessThan(0)
|
|
45
|
+
expect(clamped.start).toBe(0)
|
|
46
|
+
expect(clamped.end).toBe(raw.end)
|
|
47
|
+
expect(vs.visibleFrom).toBe(0)
|
|
48
|
+
expect(vs.visibleTo).toBe(clamped.end)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -1,69 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ChartDom, Viewport, ViewportState } from '../chartTypes'
|
|
1
|
+
import type { ChartDom } from '../chartTypes'
|
|
3
2
|
import type { ChartStateKernel } from '../state/chartStateKernel'
|
|
4
3
|
|
|
5
|
-
/**
|
|
4
|
+
/** DOM 生命周期依赖:ResizeObserver / scroll 监听 */
|
|
6
5
|
export interface ViewportDependencies {
|
|
7
6
|
getDom: () => ChartDom
|
|
8
7
|
onResizeCompleted: () => void
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Viewport DOM 适配层:只挂 ResizeObserver 与 scroll,读写一律走 kernel。
|
|
12
|
+
*/
|
|
11
13
|
export class ChartViewportManager {
|
|
12
14
|
private deps: ViewportDependencies
|
|
13
15
|
private kernel: ChartStateKernel
|
|
14
16
|
private resizeObserver?: ResizeObserver
|
|
15
17
|
private onScroll?: (e: Event) => void
|
|
16
18
|
|
|
17
|
-
get viewportSignal(): ReadonlySignal<ViewportState> {
|
|
18
|
-
return this.kernel.viewport.readonly.viewportState
|
|
19
|
-
}
|
|
20
|
-
|
|
21
19
|
constructor(deps: ViewportDependencies, kernel: ChartStateKernel) {
|
|
22
20
|
this.deps = deps
|
|
23
21
|
this.kernel = kernel
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
getCachedScrollLeft(): number {
|
|
27
|
-
return this.kernel.viewport.readonly.scrollLeft.peek()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
getLogicalScrollLeft(): number {
|
|
31
|
-
return this.kernel.viewport.readonly.scrollLeftLogical.peek()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
getViewport(): Viewport | null {
|
|
35
|
-
if (this.kernel.viewport.readonly.viewWidth.peek() === 0) return null
|
|
36
|
-
return this.kernel.viewport.readonly.viewport.peek()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getEffectiveDpr(): number {
|
|
40
|
-
return this.kernel.viewport.readonly.dpr.peek()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getObservedSize(): { width: number; height: number } {
|
|
44
|
-
return {
|
|
45
|
-
width: this.kernel.viewport.readonly.viewWidth.peek(),
|
|
46
|
-
height: this.kernel.viewport.readonly.viewHeight.peek(),
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
setScrollLeft(v: number): void {
|
|
51
|
-
this.kernel.viewport.actions.scrollTo(v)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
24
|
init(): void {
|
|
55
|
-
if (typeof ResizeObserver === 'undefined') return
|
|
56
|
-
|
|
57
25
|
const target = this.deps.getDom().container
|
|
58
26
|
if (!target) return
|
|
59
27
|
|
|
28
|
+
// 首帧尺寸写入 kernel,不依赖 ResizeObserver 是否可用
|
|
60
29
|
this.kernel.initViewport()
|
|
61
30
|
|
|
62
|
-
this.onScroll = (
|
|
31
|
+
this.onScroll = () => {
|
|
63
32
|
this.kernel.viewport.actions.syncFromDomScroll()
|
|
64
33
|
}
|
|
65
34
|
target.addEventListener('scroll', this.onScroll, { passive: true })
|
|
66
35
|
|
|
36
|
+
if (typeof ResizeObserver === 'undefined') return
|
|
37
|
+
|
|
67
38
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
68
39
|
const entry = entries[0]
|
|
69
40
|
if (!entry) return
|
|
@@ -119,26 +90,4 @@ export class ChartViewportManager {
|
|
|
119
90
|
this.onScroll = undefined
|
|
120
91
|
}
|
|
121
92
|
}
|
|
122
|
-
|
|
123
|
-
computeViewport(): Viewport | null {
|
|
124
|
-
const container = this.deps.getDom().container
|
|
125
|
-
if (!container) return null
|
|
126
|
-
|
|
127
|
-
const viewWidth = this.kernel.viewport.readonly.viewWidth.peek()
|
|
128
|
-
const viewHeight = this.kernel.viewport.readonly.viewHeight.peek()
|
|
129
|
-
if (viewWidth === 0 || viewHeight === 0) {
|
|
130
|
-
const fallbackW = Math.max(1, Math.round(container.clientWidth))
|
|
131
|
-
const fallbackH = Math.max(1, Math.round(container.clientHeight))
|
|
132
|
-
if (fallbackW > 0 && fallbackH > 0) {
|
|
133
|
-
this.kernel.viewport.actions.resize(fallbackW, fallbackH, this.kernel.viewport.readonly.preciseDpr.peek())
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return this.kernel.viewport.readonly.viewport.peek()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** @deprecated No-op — viewportState is now computed(). */
|
|
141
|
-
updateViewportSignal(): void {
|
|
142
|
-
// no-op
|
|
143
|
-
}
|
|
144
93
|
}
|
|
@@ -40,6 +40,20 @@ export function getVisibleRange(
|
|
|
40
40
|
return { start, end }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 将 raw visible range 钳制为可索引区间。
|
|
45
|
+
*
|
|
46
|
+
* @remarks getVisibleRange 左右各扩 1 根时 start 可能为 -1;
|
|
47
|
+
* 绘制 / hit-test / 数据下标必须用 clamp 后的 start>=0。
|
|
48
|
+
* 增量加载检测仍读 raw(start 小于 0 表示已滚到左缘扩窗)。
|
|
49
|
+
*/
|
|
50
|
+
export function clampVisibleRange(range: {
|
|
51
|
+
start: number
|
|
52
|
+
end: number
|
|
53
|
+
}): { start: number; end: number } {
|
|
54
|
+
return { start: Math.max(0, range.start), end: range.end }
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* 计算指定索引区间内的价格范围(max/min)。
|
|
45
59
|
*
|
|
@@ -2,6 +2,24 @@ import { describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import { RendererPluginManager } from './rendererPluginManager'
|
|
4
4
|
|
|
5
|
+
describe('RendererPluginManager lifecycle', () => {
|
|
6
|
+
it('installs a renderer plugin exactly once', () => {
|
|
7
|
+
const manager = new RendererPluginManager()
|
|
8
|
+
const onInstall = vi.fn()
|
|
9
|
+
manager.setPluginHost({} as never)
|
|
10
|
+
|
|
11
|
+
manager.register({
|
|
12
|
+
name: 'test',
|
|
13
|
+
paneId: 'main',
|
|
14
|
+
priority: 0,
|
|
15
|
+
draw: vi.fn(),
|
|
16
|
+
onInstall,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
expect(onInstall).toHaveBeenCalledTimes(1)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
5
23
|
describe('RendererPluginManager transaction', () => {
|
|
6
24
|
it('coalesces renderer invalidations into one callback', () => {
|
|
7
25
|
const manager = new RendererPluginManager()
|
|
@@ -304,7 +304,10 @@ export interface RenderContext {
|
|
|
304
304
|
crosshairIndex?: number | null
|
|
305
305
|
// 可选的其他 Canvas 上下文
|
|
306
306
|
yAxisCtx?: CanvasRenderingContext2D
|
|
307
|
+
/** 轴区动态层(最新价标签、十字线价签) */
|
|
308
|
+
yAxisOverlayCtx?: CanvasRenderingContext2D
|
|
307
309
|
leftAxisCtx?: CanvasRenderingContext2D
|
|
310
|
+
leftAxisOverlayCtx?: CanvasRenderingContext2D
|
|
308
311
|
xAxisCtx?: CanvasRenderingContext2D
|
|
309
312
|
borderCtx?: CanvasRenderingContext2D
|
|
310
313
|
/** 覆盖层 Canvas 上下文(用于十字线、Tooltip 等动态内容) */
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
* 一帧固定走 capture → derive → seal → render → publish → complete。
|
|
11
11
|
* render 或 publish 期间的 writeInput 一律进入下一代 pending。
|
|
12
12
|
* 非 idle 时调用 flush 不会嵌套发布,仅保留 dirty 供外层完成后调度。
|
|
13
|
+
*
|
|
14
|
+
* @remarks 适用范围
|
|
15
|
+
* 这是一个半通用的 rAF 合帧器(semi-general frame coalescer),
|
|
16
|
+
* 专为 ChartRenderer 的绘制管线设计,并非全场景通用的高频处理方案。
|
|
17
|
+
*
|
|
18
|
+
* @Todo 如需在其他场景(WebSocket 消息合并、键盘事件批处理)复用,需补充:
|
|
19
|
+
* - 深层合并 / 自定义 merge 策略(目前 mergeInput 仅浅合并)
|
|
20
|
+
* - 快照深层不可变保证(目前 sealSnapshotRoot 只冻根对象)
|
|
21
|
+
* - 超时/失帧策略(目前纯乐观逐帧)
|
|
22
|
+
* - 背压与优先级集成(FrameBudget 在 scheduler 层但未在此集成)
|
|
23
|
+
* - 非 Record 输入类型支持(目前 TInput 约束为 Record)
|
|
13
24
|
*/
|
|
14
25
|
|
|
15
26
|
import { createSignal, type ReadonlySignal } from './signal'
|
|
@@ -88,10 +99,9 @@ export interface FrameTransaction<TInput extends Record<string, unknown>, TSnaps
|
|
|
88
99
|
* @typeParam TInput - 可合并的输入形状(浅层 Partial 合并)
|
|
89
100
|
* @typeParam TSnapshot - derive 产出的不可变快照类型
|
|
90
101
|
*/
|
|
91
|
-
export function createFrameTransaction<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
>(options: FrameTransactionOptions<TInput, TSnapshot>): FrameTransaction<TInput, TSnapshot> {
|
|
102
|
+
export function createFrameTransaction<TInput extends Record<string, unknown>, TSnapshot>(
|
|
103
|
+
options: FrameTransactionOptions<TInput, TSnapshot>,
|
|
104
|
+
): FrameTransaction<TInput, TSnapshot> {
|
|
95
105
|
const { derive, render } = options
|
|
96
106
|
const schedule =
|
|
97
107
|
options.schedule ??
|
|
@@ -145,24 +155,27 @@ export function createFrameTransaction<
|
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
let sealedInput: TInput | undefined
|
|
158
|
+
// 封存输入,后续写入只能进下一代
|
|
148
159
|
phase = 'capturing'
|
|
149
160
|
try {
|
|
150
161
|
sealedInput = pending
|
|
151
162
|
dirty = false
|
|
152
|
-
// 本帧 capture 之后的新写入进入 nextPending,不得污染 sealedInput
|
|
153
163
|
pending = { ...sealedInput }
|
|
154
164
|
|
|
165
|
+
// 从封存输入纯推导不可变快照
|
|
155
166
|
phase = 'deriving'
|
|
156
167
|
const nextGeneration = generation + 1
|
|
157
168
|
const snapshot = derive(sealedInput, nextGeneration)
|
|
158
169
|
|
|
170
|
+
// 冻结快照根对象
|
|
159
171
|
phase = 'sealing'
|
|
160
172
|
sealSnapshotRoot(snapshot)
|
|
161
173
|
|
|
174
|
+
// 用本帧快照绘制
|
|
162
175
|
phase = 'rendering'
|
|
163
176
|
render?.(snapshot)
|
|
164
177
|
|
|
165
|
-
//
|
|
178
|
+
// 先推进代际再发布,保证订阅者读到一致代际
|
|
166
179
|
phase = 'publishing'
|
|
167
180
|
generation = nextGeneration
|
|
168
181
|
published.set(snapshot)
|
|
@@ -191,6 +204,7 @@ export function createFrameTransaction<
|
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
function scheduleFlush(): void {
|
|
207
|
+
// 已有 raf 帧,
|
|
194
208
|
if (scheduleQueued) return
|
|
195
209
|
if (!dirty && nextPending === null) return
|
|
196
210
|
scheduleQueued = true
|