@363045841yyt/klinechart-core 0.9.0-alpha.2 → 0.9.0-alpha.4
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/data/timeShareBuffer.d.ts +3 -0
- package/dist/data/timeShareBuffer.d.ts.map +1 -1
- package/dist/data/timeShareBuffer.js +13 -0
- package/dist/data/timeShareBuffer.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 +87 -101
- 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 +14 -18
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +55 -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/modes/timeShareMath.d.ts +56 -0
- package/dist/engine/modes/timeShareMath.d.ts.map +1 -0
- package/dist/engine/modes/timeShareMath.js +90 -0
- package/dist/engine/modes/timeShareMath.js.map +1 -0
- package/dist/engine/modes/timeShareMode.d.ts +5 -0
- package/dist/engine/modes/timeShareMode.d.ts.map +1 -1
- package/dist/engine/modes/timeShareMode.js +27 -27
- package/dist/engine/modes/timeShareMode.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 +143 -85
- 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 +73 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +182 -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/timeShare.d.ts.map +1 -1
- package/dist/engine/renderers/timeShare.js +20 -9
- package/dist/engine/renderers/timeShare.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/config/chartSettings.d.ts +0 -5
- package/dist/foundation/config/chartSettings.d.ts.map +1 -1
- package/dist/foundation/config/chartSettings.js +14 -0
- package/dist/foundation/config/chartSettings.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/foundation/utils/kLineDraw/axis.d.ts +3 -0
- package/dist/foundation/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/foundation/utils/kLineDraw/axis.js +40 -9
- package/dist/foundation/utils/kLineDraw/axis.js.map +1 -1
- package/dist/foundation/utils/sessionTimeLabels.d.ts +59 -0
- package/dist/foundation/utils/sessionTimeLabels.d.ts.map +1 -0
- package/dist/foundation/utils/sessionTimeLabels.js +170 -0
- package/dist/foundation/utils/sessionTimeLabels.js.map +1 -0
- package/dist/foundation/utils/timeShareAxisLabels.d.ts +27 -0
- package/dist/foundation/utils/timeShareAxisLabels.d.ts.map +1 -0
- package/dist/foundation/utils/timeShareAxisLabels.js +45 -0
- package/dist/foundation/utils/timeShareAxisLabels.js.map +1 -0
- 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/__tests__/timeShareBuffer.test.ts +45 -0
- package/src/data/binance.ts +1 -1
- package/src/data/timeShareBuffer.ts +15 -0
- package/src/engine/__tests__/chart.dpr.test.ts +31 -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 +105 -115
- 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 +64 -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/modes/__tests__/timeShareMath.test.ts +185 -0
- package/src/engine/modes/__tests__/timeShareMode.test.ts +81 -0
- package/src/engine/modes/timeShareMath.ts +150 -0
- package/src/engine/modes/timeShareMode.ts +35 -28
- package/src/engine/paneRenderer.ts +17 -10
- package/src/engine/render/chartRenderer.ts +214 -145
- 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 +290 -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/timeShare.ts +25 -9
- package/src/engine/renderers/yAxis.ts +57 -22
- package/src/engine/state/__tests__/settingsState.test.ts +9 -0
- 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/config/chartSettings.ts +13 -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/foundation/utils/__tests__/sessionTimeLabels.test.ts +91 -0
- package/src/foundation/utils/kLineDraw/axis.ts +50 -9
- package/src/foundation/utils/sessionTimeLabels.ts +237 -0
- package/src/foundation/utils/timeShareAxisLabels.ts +94 -0
- 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
|
@@ -2,6 +2,8 @@ import type { PaneRole } from '../../foundation/plugin/index'
|
|
|
2
2
|
import type { ChartDom, PaneSpec, Viewport } from '../chartTypes'
|
|
3
3
|
import { PaneRenderer } from '../paneRenderer'
|
|
4
4
|
import type { ScaleType } from '../utils/tickPosition'
|
|
5
|
+
import type { PaneStateModule } from '../state/paneState'
|
|
6
|
+
import type { ViewportStateModule } from '../state/viewportState'
|
|
5
7
|
|
|
6
8
|
import { Pane, UpdateLevel } from './pane'
|
|
7
9
|
import { normalizeVisiblePaneRatios as pureNormalizeVisiblePaneRatios } from './paneRatioMath'
|
|
@@ -16,14 +18,15 @@ export interface PaneLayoutDependencies {
|
|
|
16
18
|
paneGap?: number
|
|
17
19
|
defaultPaneMinHeightPx?: number
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
/** scroll / plot 几何 SSOT */
|
|
22
|
+
viewport: ViewportStateModule
|
|
20
23
|
setKnownPaneIds: (ids: string[]) => void
|
|
21
24
|
notifyPaneResize: (paneId: string, pane: Pane) => void
|
|
22
25
|
scheduleDraw: (level?: UpdateLevel) => void
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/** pane ratios / specs / scaleTypes SSOT */
|
|
27
|
+
pane: PaneStateModule
|
|
28
|
+
/** commitLayout 写入 kernel 后的副作用(如 ensurePaneScaleTypes) */
|
|
29
|
+
afterCommitLayout?: () => void
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -63,7 +66,7 @@ export class ChartPaneLayout {
|
|
|
63
66
|
* 公共读:specs 结构来自工作副本定义,ratio 字段取自 kernel(不写工作副本)。
|
|
64
67
|
*/
|
|
65
68
|
getPaneSpecs(): PaneSpec[] {
|
|
66
|
-
const kernelRatios = this.deps.
|
|
69
|
+
const kernelRatios = this.deps.pane.readonly.paneRatios.peek()
|
|
67
70
|
return this._paneSpecs.map((spec) => ({
|
|
68
71
|
...spec,
|
|
69
72
|
ratio: kernelRatios[spec.id] ?? spec.ratio,
|
|
@@ -75,11 +78,11 @@ export class ChartPaneLayout {
|
|
|
75
78
|
* 公共读:直接返回 kernel paneRatios 副本,不触碰算法工作副本。
|
|
76
79
|
*/
|
|
77
80
|
getInternalPaneRatios(): Map<string, number> {
|
|
78
|
-
return new Map(Object.entries(this.deps.
|
|
81
|
+
return new Map(Object.entries(this.deps.pane.readonly.paneRatios.peek()))
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
private syncRatiosFromKernel(): void {
|
|
82
|
-
const kernelRatios = this.deps.
|
|
85
|
+
const kernelRatios = this.deps.pane.readonly.paneRatios.peek()
|
|
83
86
|
this._internalPaneRatios = new Map(Object.entries(kernelRatios))
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -100,17 +103,36 @@ export class ChartPaneLayout {
|
|
|
100
103
|
return index === 0 ? 'price' : 'indicator'
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
private createAxisCanvas(
|
|
106
|
+
private createAxisCanvas(
|
|
107
|
+
spec: PaneSpec,
|
|
108
|
+
side: 'left' | 'right',
|
|
109
|
+
kind: 'base' | 'overlay' = 'base',
|
|
110
|
+
): HTMLCanvasElement {
|
|
104
111
|
const canvas = document.createElement('canvas')
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const isRight = side === 'right'
|
|
113
|
+
const isOverlay = kind === 'overlay'
|
|
114
|
+
canvas.id = `${spec.id}-${side}Axis${isOverlay ? 'Overlay' : ''}`
|
|
115
|
+
canvas.className = isRight
|
|
116
|
+
? isOverlay
|
|
117
|
+
? 'right-axis-overlay'
|
|
118
|
+
: 'right-axis'
|
|
119
|
+
: isOverlay
|
|
120
|
+
? 'left-axis-overlay'
|
|
121
|
+
: 'left-axis'
|
|
107
122
|
canvas.style.position = 'absolute'
|
|
108
123
|
canvas.style.left = '0'
|
|
124
|
+
if (isOverlay) {
|
|
125
|
+
canvas.style.pointerEvents = 'none'
|
|
126
|
+
canvas.style.backgroundColor = 'transparent'
|
|
127
|
+
canvas.style.zIndex = '1'
|
|
128
|
+
} else {
|
|
129
|
+
canvas.style.zIndex = '0'
|
|
130
|
+
}
|
|
109
131
|
return canvas
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
private initPanes() {
|
|
113
|
-
const kernelScaleTypes = this.deps.
|
|
135
|
+
const kernelScaleTypes = this.deps.pane.readonly.paneScaleTypes.peek()
|
|
114
136
|
const prevScaleTypes = new Map<string, ScaleType>()
|
|
115
137
|
for (const r of this.paneRenderers) {
|
|
116
138
|
prevScaleTypes.set(r.getPane().id, r.getPane().yAxis.getScaleType())
|
|
@@ -129,7 +151,8 @@ export class ChartPaneLayout {
|
|
|
129
151
|
|
|
130
152
|
const mainCanvas = document.createElement('canvas')
|
|
131
153
|
const overlayCanvas = document.createElement('canvas')
|
|
132
|
-
const yAxisCanvas = this.createAxisCanvas(spec,
|
|
154
|
+
const yAxisCanvas = this.createAxisCanvas(spec, 'right', 'base')
|
|
155
|
+
const yAxisOverlayCanvas = this.createAxisCanvas(spec, 'right', 'overlay')
|
|
133
156
|
|
|
134
157
|
const isMain = pane.role === 'price'
|
|
135
158
|
|
|
@@ -149,10 +172,18 @@ export class ChartPaneLayout {
|
|
|
149
172
|
overlayCanvas.style.backgroundColor = 'transparent'
|
|
150
173
|
overlayCanvas.style.zIndex = '2'
|
|
151
174
|
|
|
152
|
-
const leftYAxisCanvas = this.createAxisCanvas(spec,
|
|
175
|
+
const leftYAxisCanvas = this.createAxisCanvas(spec, 'left', 'base')
|
|
176
|
+
const leftYAxisOverlayCanvas = this.createAxisCanvas(spec, 'left', 'overlay')
|
|
153
177
|
|
|
154
178
|
const renderer = new PaneRenderer(
|
|
155
|
-
{
|
|
179
|
+
{
|
|
180
|
+
mainCanvas,
|
|
181
|
+
overlayCanvas,
|
|
182
|
+
yAxisCanvas,
|
|
183
|
+
yAxisOverlayCanvas,
|
|
184
|
+
leftYAxisCanvas,
|
|
185
|
+
leftYAxisOverlayCanvas,
|
|
186
|
+
},
|
|
156
187
|
pane,
|
|
157
188
|
{
|
|
158
189
|
rightAxisWidth: this.deps.getOption().rightAxisWidth,
|
|
@@ -177,11 +208,15 @@ export class ChartPaneLayout {
|
|
|
177
208
|
existingCanvases.forEach((canvas) => canvas.remove())
|
|
178
209
|
}
|
|
179
210
|
if (rightAxisLayer) {
|
|
180
|
-
const existingAxisCanvases = rightAxisLayer.querySelectorAll(
|
|
211
|
+
const existingAxisCanvases = rightAxisLayer.querySelectorAll(
|
|
212
|
+
'canvas.right-axis, canvas.right-axis-overlay',
|
|
213
|
+
)
|
|
181
214
|
existingAxisCanvases.forEach((canvas) => canvas.remove())
|
|
182
215
|
}
|
|
183
216
|
if (leftAxisLayer) {
|
|
184
|
-
const existingLeftAxisCanvases = leftAxisLayer.querySelectorAll(
|
|
217
|
+
const existingLeftAxisCanvases = leftAxisLayer.querySelectorAll(
|
|
218
|
+
'canvas.left-axis, canvas.left-axis-overlay',
|
|
219
|
+
)
|
|
185
220
|
existingLeftAxisCanvases.forEach((canvas) => canvas.remove())
|
|
186
221
|
}
|
|
187
222
|
|
|
@@ -190,8 +225,12 @@ export class ChartPaneLayout {
|
|
|
190
225
|
canvasLayer.appendChild(domEls.mainCanvas)
|
|
191
226
|
canvasLayer.appendChild(domEls.overlayCanvas)
|
|
192
227
|
rightAxisLayer.appendChild(domEls.yAxisCanvas)
|
|
228
|
+
rightAxisLayer.appendChild(domEls.yAxisOverlayCanvas)
|
|
193
229
|
if (leftAxisLayer && domEls.leftYAxisCanvas) {
|
|
194
230
|
leftAxisLayer.appendChild(domEls.leftYAxisCanvas)
|
|
231
|
+
if (domEls.leftYAxisOverlayCanvas) {
|
|
232
|
+
leftAxisLayer.appendChild(domEls.leftYAxisOverlayCanvas)
|
|
233
|
+
}
|
|
195
234
|
}
|
|
196
235
|
})
|
|
197
236
|
|
|
@@ -299,10 +338,16 @@ export class ChartPaneLayout {
|
|
|
299
338
|
return heights
|
|
300
339
|
}
|
|
301
340
|
|
|
341
|
+
/** viewWidth 为 0 表示尚未完成首帧尺寸 */
|
|
342
|
+
private peekViewport(): Viewport | null {
|
|
343
|
+
if (this.deps.viewport.readonly.viewWidth.peek() === 0) return null
|
|
344
|
+
return this.deps.viewport.readonly.viewport.peek()
|
|
345
|
+
}
|
|
346
|
+
|
|
302
347
|
layoutPanes(options?: { commit?: boolean }) {
|
|
303
348
|
this.syncRatiosFromKernel()
|
|
304
349
|
|
|
305
|
-
const vp = this.
|
|
350
|
+
const vp = this.peekViewport()
|
|
306
351
|
if (!vp) return
|
|
307
352
|
|
|
308
353
|
const visibleSpecs = this._paneSpecs.filter((p) => p.visible !== false)
|
|
@@ -338,10 +383,16 @@ export class ChartPaneLayout {
|
|
|
338
383
|
domEls.overlayCanvas.style.top = `${y}px`
|
|
339
384
|
domEls.yAxisCanvas.style.top = `${y}px`
|
|
340
385
|
domEls.yAxisCanvas.style.left = '0px'
|
|
386
|
+
domEls.yAxisOverlayCanvas.style.top = `${y}px`
|
|
387
|
+
domEls.yAxisOverlayCanvas.style.left = '0px'
|
|
341
388
|
if (domEls.leftYAxisCanvas) {
|
|
342
389
|
domEls.leftYAxisCanvas.style.top = `${y}px`
|
|
343
390
|
domEls.leftYAxisCanvas.style.left = '0px'
|
|
344
391
|
}
|
|
392
|
+
if (domEls.leftYAxisOverlayCanvas) {
|
|
393
|
+
domEls.leftYAxisOverlayCanvas.style.top = `${y}px`
|
|
394
|
+
domEls.leftYAxisOverlayCanvas.style.left = '0px'
|
|
395
|
+
}
|
|
345
396
|
|
|
346
397
|
y += h + gap
|
|
347
398
|
}
|
|
@@ -388,7 +439,7 @@ export class ChartPaneLayout {
|
|
|
388
439
|
* commitLayout 走 buildLayoutSpecsFromWorkingCopy,避免公共读污染算法中间态。
|
|
389
440
|
*/
|
|
390
441
|
getPaneLayoutSpecs(): PaneSpec[] {
|
|
391
|
-
return this.buildLayoutSpecs(this.deps.
|
|
442
|
+
return this.buildLayoutSpecs(this.deps.pane.readonly.paneRatios.peek())
|
|
392
443
|
}
|
|
393
444
|
|
|
394
445
|
/** 仅算法/commit 使用:读当前工作副本 ratios,不读 kernel */
|
|
@@ -422,7 +473,8 @@ export class ChartPaneLayout {
|
|
|
422
473
|
this._internalPaneRatios.forEach((ratio, id) => {
|
|
423
474
|
ratios[id] = ratio
|
|
424
475
|
})
|
|
425
|
-
this.deps.commitLayout(ratios, this.buildLayoutSpecsFromWorkingCopy())
|
|
476
|
+
this.deps.pane.actions.commitLayout(ratios, this.buildLayoutSpecsFromWorkingCopy())
|
|
477
|
+
this.deps.afterCommitLayout?.()
|
|
426
478
|
}
|
|
427
479
|
|
|
428
480
|
applyPaneLayoutSpecs(panes: PaneSpec[], options?: { preferIncomingRatios?: boolean }): void {
|
|
@@ -499,7 +551,7 @@ export class ChartPaneLayout {
|
|
|
499
551
|
|
|
500
552
|
resizePaneBoundary(upperPaneId: string, deltaY: number): boolean {
|
|
501
553
|
if (!Number.isFinite(deltaY) || deltaY === 0) return false
|
|
502
|
-
const vp = this.
|
|
554
|
+
const vp = this.peekViewport()
|
|
503
555
|
if (!vp) return false
|
|
504
556
|
|
|
505
557
|
this.syncRatiosFromKernel()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveTimeShareSlotTimestamp,
|
|
5
|
+
timeShareSlotCenterX,
|
|
6
|
+
} from '../../../foundation/utils/timeShareAxisLabels'
|
|
7
|
+
import {
|
|
8
|
+
ASHARE_TIMESHARE_SESSION_SLOTS,
|
|
9
|
+
computeTimeShareBarMetrics,
|
|
10
|
+
computeTimeSharePaneLayout,
|
|
11
|
+
computeTimeSharePriceRange,
|
|
12
|
+
computeTimeShareTimeLabelIndices,
|
|
13
|
+
computeTimeShareXLayout,
|
|
14
|
+
resolveTimeShareSessionSlots,
|
|
15
|
+
resolveTimeShareBaseline,
|
|
16
|
+
} from '../timeShareMath'
|
|
17
|
+
|
|
18
|
+
describe('resolveTimeShareBaseline', () => {
|
|
19
|
+
it('prefers finite non-zero preClose over first trade price', () => {
|
|
20
|
+
expect(resolveTimeShareBaseline({ preClose: 10.5, firstPrice: 11 })).toBe(10.5)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('falls back to first price when preClose is missing or zero', () => {
|
|
24
|
+
expect(resolveTimeShareBaseline({ preClose: 0, firstPrice: 11 })).toBe(11)
|
|
25
|
+
expect(resolveTimeShareBaseline({ firstPrice: 11 })).toBe(11)
|
|
26
|
+
expect(resolveTimeShareBaseline({ preClose: null, firstPrice: 9 })).toBe(9)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null when no valid baseline exists', () => {
|
|
30
|
+
expect(resolveTimeShareBaseline({ preClose: 0, firstPrice: 0 })).toBeNull()
|
|
31
|
+
expect(resolveTimeShareBaseline({})).toBeNull()
|
|
32
|
+
expect(resolveTimeShareBaseline({ preClose: NaN, firstPrice: Infinity })).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('computeTimeSharePriceRange', () => {
|
|
37
|
+
it('uses preClose baseline and pads around max absolute percent move', () => {
|
|
38
|
+
// open gap up: first trade 11, preClose 10 → +10%; later 10.5 → 5%
|
|
39
|
+
const range = computeTimeSharePriceRange([11, 10.5, 10.2], 10)
|
|
40
|
+
expect(range).not.toBeNull()
|
|
41
|
+
// maxAbsPct=10, padding=max(1, 0.5)=1 → display 11%
|
|
42
|
+
expect(range!.maxPrice).toBeCloseTo(10 * 1.11, 8)
|
|
43
|
+
expect(range!.minPrice).toBeCloseTo(10 * 0.89, 8)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('still applies minimum padding when all prices equal baseline (flat day)', () => {
|
|
47
|
+
const range = computeTimeSharePriceRange([10, 10, 10], 10)
|
|
48
|
+
expect(range).not.toBeNull()
|
|
49
|
+
// maxAbsPct=0, padding=min 0.5% → ±0.5%
|
|
50
|
+
expect(range!.maxPrice).toBeCloseTo(10.05, 8)
|
|
51
|
+
expect(range!.minPrice).toBeCloseTo(9.95, 8)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns null for invalid baseline', () => {
|
|
55
|
+
expect(computeTimeSharePriceRange([10], 0)).toBeNull()
|
|
56
|
+
expect(computeTimeSharePriceRange([10], NaN)).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('resolveTimeShareSessionSlots', () => {
|
|
61
|
+
it('defaults to A-share 240 one-minute slots (sessions SSOT)', () => {
|
|
62
|
+
expect(ASHARE_TIMESHARE_SESSION_SLOTS).toBe(240)
|
|
63
|
+
expect(resolveTimeShareSessionSlots(0)).toBe(240)
|
|
64
|
+
expect(resolveTimeShareSessionSlots(60)).toBe(240)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('does not expand when arrived points exceed session slots', () => {
|
|
68
|
+
expect(resolveTimeShareSessionSlots(300)).toBe(240)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('computeTimeShareBarMetrics', () => {
|
|
73
|
+
it('divides viewWidth by full session slots, not arrived point count', () => {
|
|
74
|
+
// 盘中仅 60 点:宽度仍按 240 槽划分,不把 60 点拉满屏
|
|
75
|
+
const partial = computeTimeShareBarMetrics(60, 320, 1)
|
|
76
|
+
const full = computeTimeShareBarMetrics(240, 320, 1)
|
|
77
|
+
expect(partial).not.toBeNull()
|
|
78
|
+
expect(full).not.toBeNull()
|
|
79
|
+
expect(partial!.kWidth).toBeCloseTo(full!.kWidth, 8)
|
|
80
|
+
expect(partial!.kGap).toBeCloseTo(full!.kGap, 8)
|
|
81
|
+
expect((partial!.kWidth + partial!.kGap) * 240).toBeCloseTo(320, 6)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('fits full session into viewWidth even when per-bar width is sub-pixel (DPR=1 narrow)', () => {
|
|
85
|
+
const m = computeTimeShareBarMetrics(240, 320, 1)
|
|
86
|
+
expect(m).not.toBeNull()
|
|
87
|
+
const unit = m!.kWidth + m!.kGap
|
|
88
|
+
expect(unit * 240).toBeCloseTo(320, 6)
|
|
89
|
+
expect(m!.kWidth).toBeGreaterThan(0)
|
|
90
|
+
expect(m!.kGap).toBeGreaterThanOrEqual(0)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('keeps full session visible at high DPR', () => {
|
|
94
|
+
const m = computeTimeShareBarMetrics(240, 320, 2)
|
|
95
|
+
expect(m).not.toBeNull()
|
|
96
|
+
expect((m!.kWidth + m!.kGap) * 240).toBeCloseTo(320, 6)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns null for empty data or invalid width', () => {
|
|
100
|
+
expect(computeTimeShareBarMetrics(0, 320, 1)).toBeNull()
|
|
101
|
+
expect(computeTimeShareBarMetrics(10, 0, 1)).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('computeTimeShareXLayout', () => {
|
|
106
|
+
it('places partial-day points on session timeline leaving right-side blank', () => {
|
|
107
|
+
const layout = computeTimeShareXLayout({
|
|
108
|
+
arrivedCount: 60,
|
|
109
|
+
sessionSlots: 240,
|
|
110
|
+
totalWidth: 480,
|
|
111
|
+
dpr: 1,
|
|
112
|
+
})
|
|
113
|
+
expect(layout).not.toBeNull()
|
|
114
|
+
// step = 480/240 = 2;第 0 点中心 1,第 59 点中心 119,未到右缘 480
|
|
115
|
+
expect(layout!.centers[0]).toBeCloseTo(1, 6)
|
|
116
|
+
expect(layout!.centers[59]).toBeCloseTo(119, 6)
|
|
117
|
+
expect(layout!.centers[59]!).toBeLessThan(480 * 0.3)
|
|
118
|
+
expect(layout!.step).toBeCloseTo(2, 6)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('fills full width only when arrivedCount covers full session', () => {
|
|
122
|
+
const layout = computeTimeShareXLayout({
|
|
123
|
+
arrivedCount: 240,
|
|
124
|
+
sessionSlots: 240,
|
|
125
|
+
totalWidth: 480,
|
|
126
|
+
dpr: 1,
|
|
127
|
+
})
|
|
128
|
+
expect(layout).not.toBeNull()
|
|
129
|
+
expect(layout!.centers[0]).toBeCloseTo(1, 6)
|
|
130
|
+
expect(layout!.centers[239]).toBeCloseTo(479, 6)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('computeTimeSharePaneLayout', () => {
|
|
135
|
+
it('splits price area above volume area without overlap', () => {
|
|
136
|
+
const layout = computeTimeSharePaneLayout(400, 0.25)
|
|
137
|
+
expect(layout.priceAreaHeight).toBe(300)
|
|
138
|
+
expect(layout.volumeAreaHeight).toBe(100)
|
|
139
|
+
expect(layout.priceTop).toBe(0)
|
|
140
|
+
expect(layout.volumeTop).toBe(300)
|
|
141
|
+
expect(layout.priceTop + layout.priceAreaHeight).toBe(layout.volumeTop)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('computeTimeShareTimeLabelIndices', () => {
|
|
146
|
+
it('only session closed-side endpoints: 9:30 / 13:00 / 15:00', () => {
|
|
147
|
+
const labels = computeTimeShareTimeLabelIndices({
|
|
148
|
+
axisWidth: 800,
|
|
149
|
+
sessionSlots: 240,
|
|
150
|
+
})
|
|
151
|
+
// 9:30 → 0;13:00 → 120;15:00 → 239
|
|
152
|
+
expect(labels).toEqual([0, 120, 239])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('returns empty for invalid axis width', () => {
|
|
156
|
+
expect(
|
|
157
|
+
computeTimeShareTimeLabelIndices({
|
|
158
|
+
axisWidth: 0,
|
|
159
|
+
sessionSlots: 240,
|
|
160
|
+
}),
|
|
161
|
+
).toEqual([])
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('timeShare slot time/x helpers', () => {
|
|
166
|
+
it('maps A-share slots across lunch break', () => {
|
|
167
|
+
// 2026-07-21 local
|
|
168
|
+
const day = new Date(2026, 6, 21, 10, 0, 0, 0).getTime()
|
|
169
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 0)).getHours()).toBe(9)
|
|
170
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 0)).getMinutes()).toBe(30)
|
|
171
|
+
// slot 120 = 13:00
|
|
172
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 120)).getHours()).toBe(13)
|
|
173
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 120)).getMinutes()).toBe(0)
|
|
174
|
+
// slot 239 = 14:59
|
|
175
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 239)).getHours()).toBe(14)
|
|
176
|
+
expect(new Date(resolveTimeShareSlotTimestamp(day, 239)).getMinutes()).toBe(59)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('places last session slot near right edge of axis', () => {
|
|
180
|
+
const x0 = timeShareSlotCenterX(0, 480, 240, 1)
|
|
181
|
+
const xLast = timeShareSlotCenterX(239, 480, 240, 1)
|
|
182
|
+
expect(x0).toBeCloseTo(1, 6)
|
|
183
|
+
expect(xLast).toBeCloseTo(479, 6)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Pane } from '../../layout/pane'
|
|
4
|
+
import { TimeShareMode } from '../timeShareMode'
|
|
5
|
+
import type { TimeShareData } from '../../../foundation/types/price'
|
|
6
|
+
|
|
7
|
+
function ts(price: number, i = 0): TimeShareData {
|
|
8
|
+
return { timestamp: i, price, average: price, volume: 1, amount: price }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mockDm(points: TimeShareData[], preClose: number | null = null) {
|
|
12
|
+
return {
|
|
13
|
+
getTimeShareData: () => points,
|
|
14
|
+
getTimeSharePreClose: () => preClose,
|
|
15
|
+
} as unknown as import('../../data/chartDataManager').ChartDataManager
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('TimeShareMode', () => {
|
|
19
|
+
it('computeKWidth fits full session into narrow view (no DPR truncation)', () => {
|
|
20
|
+
const mode = new TimeShareMode()
|
|
21
|
+
const m = mode.computeKWidth(240, 320, 1)
|
|
22
|
+
expect(m).not.toBeNull()
|
|
23
|
+
expect((m!.kWidth + m!.kGap) * 240).toBeCloseTo(320, 6)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('computeKWidth for partial day matches full-session bar size', () => {
|
|
27
|
+
const mode = new TimeShareMode()
|
|
28
|
+
const partial = mode.computeKWidth(60, 320, 1)
|
|
29
|
+
const full = mode.computeKWidth(240, 320, 1)
|
|
30
|
+
expect(partial).not.toBeNull()
|
|
31
|
+
expect(full).not.toBeNull()
|
|
32
|
+
expect(partial!.kWidth).toBeCloseTo(full!.kWidth, 8)
|
|
33
|
+
expect(partial!.kGap).toBeCloseTo(full!.kGap, 8)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('setMarketSession switches bar metrics to HK 330 slots', () => {
|
|
37
|
+
const mode = new TimeShareMode()
|
|
38
|
+
const ashare = mode.computeKWidth(100, 330, 1)!
|
|
39
|
+
mode.setMarketSession({
|
|
40
|
+
timeZone: 'Asia/Hong_Kong',
|
|
41
|
+
sessions: [
|
|
42
|
+
{ open: 9 * 60 + 30, close: 12 * 60 },
|
|
43
|
+
{ open: 13 * 60, close: 16 * 60 },
|
|
44
|
+
],
|
|
45
|
+
slotMinutes: 1,
|
|
46
|
+
})
|
|
47
|
+
const hk = mode.computeKWidth(100, 330, 1)!
|
|
48
|
+
// A 股 240 槽 unit=330/240;港股 330 槽 unit=1
|
|
49
|
+
expect(ashare.kWidth + ashare.kGap).toBeCloseTo(330 / 240, 6)
|
|
50
|
+
expect(hk.kWidth + hk.kGap).toBeCloseTo(1, 6)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('updatePaneRange uses preClose as basePrice and covers open gap', () => {
|
|
54
|
+
const mode = new TimeShareMode()
|
|
55
|
+
const pane = new Pane('main')
|
|
56
|
+
const setBase = vi.spyOn(pane.yAxis, 'setBasePrice')
|
|
57
|
+
const setRange = vi.spyOn(pane.yAxis, 'setRange')
|
|
58
|
+
|
|
59
|
+
// open gap: first trade 11, preClose 10
|
|
60
|
+
mode.updatePaneRange(pane, { start: 0, end: 3 }, mockDm([ts(11), ts(10.5), ts(10.2)], 10))
|
|
61
|
+
|
|
62
|
+
expect(setBase).toHaveBeenCalledWith(10)
|
|
63
|
+
expect(setRange).toHaveBeenCalled()
|
|
64
|
+
const range = setRange.mock.calls[0]![0] as { minPrice: number; maxPrice: number }
|
|
65
|
+
// maxAbs 10% + 1% padding → ±11%
|
|
66
|
+
expect(range.maxPrice).toBeCloseTo(11.1, 6)
|
|
67
|
+
expect(range.minPrice).toBeCloseTo(8.9, 6)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('updatePaneRange still sets range on flat day', () => {
|
|
71
|
+
const mode = new TimeShareMode()
|
|
72
|
+
const pane = new Pane('main')
|
|
73
|
+
const setRange = vi.spyOn(pane.yAxis, 'setRange')
|
|
74
|
+
|
|
75
|
+
mode.updatePaneRange(pane, { start: 0, end: 2 }, mockDm([ts(10), ts(10)], 10))
|
|
76
|
+
expect(setRange).toHaveBeenCalled()
|
|
77
|
+
const range = setRange.mock.calls[0]![0] as { minPrice: number; maxPrice: number }
|
|
78
|
+
expect(range.maxPrice).toBeCloseTo(10.05, 6)
|
|
79
|
+
expect(range.minPrice).toBeCloseTo(9.95, 6)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASHARE_MARKET_SESSION,
|
|
3
|
+
resolveMarketSessionSlots,
|
|
4
|
+
type MarketSessionConfig,
|
|
5
|
+
} from '../../foundation/utils/timeShareAxisLabels'
|
|
6
|
+
|
|
7
|
+
export type TimeShareBaselineInput = {
|
|
8
|
+
preClose?: number | null
|
|
9
|
+
firstPrice?: number | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveTimeShareBaseline(input: TimeShareBaselineInput): number | null {
|
|
13
|
+
const candidates = [input.preClose, input.firstPrice]
|
|
14
|
+
for (const v of candidates) {
|
|
15
|
+
if (typeof v === 'number' && Number.isFinite(v) && v !== 0) return v
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TimeSharePriceRange = {
|
|
21
|
+
minPrice: number
|
|
22
|
+
maxPrice: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 分时 Y 轴价格区间:以 baseline(昨收)为中心,对称覆盖可见最大绝对涨跌幅并加 padding。
|
|
27
|
+
* 全天平盘时仍保留最小 0.5% 边距,避免 range 退化。
|
|
28
|
+
*/
|
|
29
|
+
export function computeTimeSharePriceRange(
|
|
30
|
+
prices: ReadonlyArray<number | undefined | null>,
|
|
31
|
+
baseline: number,
|
|
32
|
+
): TimeSharePriceRange | null {
|
|
33
|
+
if (!Number.isFinite(baseline) || baseline === 0) return null
|
|
34
|
+
|
|
35
|
+
let maxAbsPct = 0
|
|
36
|
+
for (const p of prices) {
|
|
37
|
+
if (typeof p !== 'number' || !Number.isFinite(p)) continue
|
|
38
|
+
const pct = Math.abs((p - baseline) / baseline) * 100
|
|
39
|
+
if (pct > maxAbsPct) maxAbsPct = pct
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const padding = Math.max(maxAbsPct * 0.1, 0.5)
|
|
43
|
+
const displayPct = maxAbsPct + padding
|
|
44
|
+
return {
|
|
45
|
+
minPrice: baseline * (1 - displayPct / 100),
|
|
46
|
+
maxPrice: baseline * (1 + displayPct / 100),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A 股默认全天 1 分钟槽位数(兼容旧导出) */
|
|
51
|
+
export const ASHARE_TIMESHARE_SESSION_SLOTS = resolveMarketSessionSlots(ASHARE_MARKET_SESSION)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 全天交易槽位数:以 marketSession 为 SSOT,不因 arrivedCount 放大。
|
|
55
|
+
* @deprecated 优先用 resolveMarketSessionSlots(config)
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTimeShareSessionSlots(
|
|
58
|
+
arrivedCount: number,
|
|
59
|
+
marketSession: MarketSessionConfig = ASHARE_MARKET_SESSION,
|
|
60
|
+
): number {
|
|
61
|
+
void arrivedCount
|
|
62
|
+
return resolveMarketSessionSlots(marketSession)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 分时 bar 宽度:按全天 sessionSlots 均分 viewWidth,而非按已到达点数。
|
|
67
|
+
* 盘中部分数据时右侧留白;允许亚像素,避免窄屏截断。
|
|
68
|
+
*/
|
|
69
|
+
export function computeTimeShareBarMetrics(
|
|
70
|
+
dataLength: number,
|
|
71
|
+
viewWidth: number,
|
|
72
|
+
dpr: number,
|
|
73
|
+
marketSession: MarketSessionConfig = ASHARE_MARKET_SESSION,
|
|
74
|
+
): { kWidth: number; kGap: number } | null {
|
|
75
|
+
if (dataLength <= 0 || viewWidth <= 0 || !(dpr > 0)) return null
|
|
76
|
+
|
|
77
|
+
const sessionSlots = resolveMarketSessionSlots(marketSession)
|
|
78
|
+
if (sessionSlots <= 0) return null
|
|
79
|
+
const unit = viewWidth / sessionSlots
|
|
80
|
+
const preferredGap = 1 / dpr
|
|
81
|
+
const kGap = Math.min(preferredGap, unit * 0.2)
|
|
82
|
+
const kWidth = Math.max(unit - kGap, unit * 0.01)
|
|
83
|
+
return { kWidth, kGap: unit - kWidth }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type TimeShareXLayoutInput = {
|
|
87
|
+
arrivedCount: number
|
|
88
|
+
sessionSlots: number
|
|
89
|
+
totalWidth: number
|
|
90
|
+
dpr: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type TimeShareXLayout = {
|
|
94
|
+
step: number
|
|
95
|
+
centers: number[]
|
|
96
|
+
lefts: number[]
|
|
97
|
+
barWidth: number
|
|
98
|
+
kWidthPx: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 分时 X 布局:step = totalWidth / sessionSlots,已到达点按 slot 索引落位,未到时段留白。
|
|
103
|
+
*/
|
|
104
|
+
export function computeTimeShareXLayout(input: TimeShareXLayoutInput): TimeShareXLayout | null {
|
|
105
|
+
const { arrivedCount, sessionSlots, totalWidth, dpr } = input
|
|
106
|
+
if (arrivedCount <= 0 || sessionSlots <= 0 || totalWidth <= 0 || !(dpr > 0)) return null
|
|
107
|
+
|
|
108
|
+
const step = totalWidth / sessionSlots
|
|
109
|
+
const centers: number[] = new Array(arrivedCount)
|
|
110
|
+
const lefts: number[] = new Array(arrivedCount)
|
|
111
|
+
for (let i = 0; i < arrivedCount; i++) {
|
|
112
|
+
centers[i] = Math.round((i + 0.5) * step * dpr) / dpr
|
|
113
|
+
lefts[i] = Math.round(i * step * dpr) / dpr
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const logicalBarWidth = Math.max(step * 0.6, 1 / dpr)
|
|
117
|
+
const barWidthPx = Math.max(1, Math.round(logicalBarWidth * dpr))
|
|
118
|
+
const barWidth = barWidthPx / dpr
|
|
119
|
+
const kWidthPx = Math.max(1, Math.round(step * dpr))
|
|
120
|
+
|
|
121
|
+
return { step, centers, lefts, barWidth, kWidthPx }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type TimeSharePaneLayout = {
|
|
125
|
+
priceTop: number
|
|
126
|
+
priceAreaHeight: number
|
|
127
|
+
volumeTop: number
|
|
128
|
+
volumeAreaHeight: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function computeTimeSharePaneLayout(
|
|
132
|
+
paneHeight: number,
|
|
133
|
+
volumeRatio: number,
|
|
134
|
+
): TimeSharePaneLayout {
|
|
135
|
+
const ratio = Math.min(1, Math.max(0, volumeRatio))
|
|
136
|
+
const volumeAreaHeight = paneHeight * ratio
|
|
137
|
+
const priceAreaHeight = paneHeight - volumeAreaHeight
|
|
138
|
+
return {
|
|
139
|
+
priceTop: 0,
|
|
140
|
+
priceAreaHeight,
|
|
141
|
+
volumeTop: priceAreaHeight,
|
|
142
|
+
volumeAreaHeight,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
TIMESHARE_MIN_LABEL_SPACING_PX,
|
|
148
|
+
computeTimeShareTimeLabelIndices,
|
|
149
|
+
type TimeShareTimeLabelInput,
|
|
150
|
+
} from '../../foundation/utils/timeShareAxisLabels'
|