@363045841yyt/klinechart-core 0.8.1-alpha.4 → 0.8.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/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +21 -1
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +6 -1
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +3 -3
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +5 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +85 -48
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/gotdx.d.ts +3 -0
- package/dist/data-fetchers/gotdx.d.ts.map +1 -0
- package/dist/data-fetchers/gotdx.js +101 -0
- package/dist/data-fetchers/gotdx.js.map +1 -0
- package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
- package/dist/data-fetchers/hundred-mock.js +28 -5
- package/dist/data-fetchers/hundred-mock.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +1 -0
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +1 -0
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +3 -0
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
- package/dist/data-fetchers/thousand-mock.js +24 -14
- package/dist/data-fetchers/thousand-mock.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +12 -6
- package/dist/data-fetchers/tradingview.js.map +1 -1
- package/dist/engine/chart.d.ts +29 -367
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +239 -1842
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartContext.d.ts +24 -0
- package/dist/engine/chartContext.d.ts.map +1 -0
- package/dist/engine/chartContext.js +19 -0
- package/dist/engine/chartContext.js.map +1 -0
- package/dist/engine/chartTypes.d.ts +77 -0
- package/dist/engine/chartTypes.d.ts.map +1 -0
- package/dist/engine/chartTypes.js +2 -0
- package/dist/engine/chartTypes.js.map +1 -0
- package/dist/engine/data/chartDataManager.d.ts +103 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +593 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +440 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.js.map +1 -1
- package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
- package/dist/engine/renderers/timeAxis.js +1 -0
- package/dist/engine/renderers/timeAxis.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/types.d.ts +3 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/semantic/index.d.ts +1 -1
- package/dist/semantic/index.d.ts.map +1 -1
- package/dist/semantic/index.js.map +1 -1
- package/dist/semantic/schema.json +1 -1
- package/dist/semantic/types.d.ts +2 -1
- package/dist/semantic/types.d.ts.map +1 -1
- package/dist/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.map +1 -1
- package/dist/utils/dateFormat.d.ts +25 -0
- package/dist/utils/dateFormat.d.ts.map +1 -1
- package/dist/utils/dateFormat.js +78 -0
- package/dist/utils/dateFormat.js.map +1 -1
- package/dist/utils/kLineDraw/axis.d.ts +2 -0
- package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/utils/kLineDraw/axis.js +11 -6
- package/dist/utils/kLineDraw/axis.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +39 -10
- package/src/controllers/types.ts +6 -1
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +70 -23
- package/src/data-fetchers/gotdx.ts +138 -0
- package/src/data-fetchers/hundred-mock.ts +35 -5
- package/src/data-fetchers/index.ts +1 -0
- package/src/data-fetchers/router.ts +3 -0
- package/src/data-fetchers/thousand-mock.ts +30 -14
- package/src/data-fetchers/tradingview.ts +12 -6
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +252 -2250
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/data/chartDataManager.ts +695 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +581 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
- package/src/engine/renderers/comparisonLine.ts +25 -11
- package/src/engine/renderers/timeAxis.ts +1 -0
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/index.ts +1 -0
- package/src/plugin/types.ts +3 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/schema.json +1 -1
- package/src/semantic/types.ts +3 -1
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/utils/dateFormat.ts +85 -0
- package/src/utils/kLineDraw/axis.ts +13 -6
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -626
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { ChartDom, Viewport, ViewportState } from '../chartTypes'
|
|
2
|
+
import type { VisibleRange, UpdateLevel } from '../layout/pane'
|
|
3
|
+
import { createSignal, type Signal } from '../../reactivity/signal'
|
|
4
|
+
|
|
5
|
+
export interface ViewportDependencies {
|
|
6
|
+
getDom: () => ChartDom
|
|
7
|
+
getBottomAxisHeight: () => number
|
|
8
|
+
getLeftLoadBufferWidth: () => number
|
|
9
|
+
getZoomLevel: () => number
|
|
10
|
+
getLastVisibleRange: () => VisibleRange
|
|
11
|
+
getKWidth: () => number
|
|
12
|
+
getKGap: () => number
|
|
13
|
+
scheduleDraw: (level?: UpdateLevel) => void
|
|
14
|
+
onResizeCompleted: () => void
|
|
15
|
+
resizeSharedWebGLSurface: (plotWidth: number, plotHeight: number, dpr: number) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ChartViewportManager {
|
|
19
|
+
private deps: ViewportDependencies
|
|
20
|
+
|
|
21
|
+
/** 精确 DPR(来自 ResizeObserver 的 devicePixelContentBoxSize) */
|
|
22
|
+
private preciseDpr = 0
|
|
23
|
+
|
|
24
|
+
/** 统一监听容器尺寸与 DPR 变化 */
|
|
25
|
+
private resizeObserver?: ResizeObserver
|
|
26
|
+
|
|
27
|
+
/** scroll 事件处理器引用(用于 cleanup) */
|
|
28
|
+
private onScroll?: () => void
|
|
29
|
+
|
|
30
|
+
/** 最近一次观测到的容器尺寸 */
|
|
31
|
+
private observedSize = { width: 0, height: 0 }
|
|
32
|
+
|
|
33
|
+
/** 缓存的 scrollLeft(通过 scroll 事件同步,避免每帧读取 DOM 触发强制回流) */
|
|
34
|
+
private cachedScrollLeft = 0
|
|
35
|
+
|
|
36
|
+
/** 待写入 DOM 的 scrollLeft(在 RAF 回调中应用,确保 Vue 已完成 DOM 更新) */
|
|
37
|
+
private _pendingScrollLeft: number | null = null
|
|
38
|
+
|
|
39
|
+
/** 内部视口状态 */
|
|
40
|
+
private _internalViewport: Viewport | null = null
|
|
41
|
+
|
|
42
|
+
/** 视口状态信号 */
|
|
43
|
+
private _viewportSignal = createSignal<ViewportState>({
|
|
44
|
+
zoomLevel: 1,
|
|
45
|
+
plotWidth: 0,
|
|
46
|
+
plotHeight: 0,
|
|
47
|
+
dpr: 1,
|
|
48
|
+
visibleFrom: 0,
|
|
49
|
+
visibleTo: 0,
|
|
50
|
+
kWidth: 0,
|
|
51
|
+
kGap: 1,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
constructor(deps: ViewportDependencies) {
|
|
55
|
+
this.deps = deps
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 视口状态信号 */
|
|
59
|
+
get viewportSignal(): Signal<ViewportState> {
|
|
60
|
+
return this._viewportSignal
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 获取缓存的 scrollLeft(避免读取 DOM 触发强制回流) */
|
|
64
|
+
getCachedScrollLeft(): number {
|
|
65
|
+
return this.cachedScrollLeft
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 获取逻辑 scrollLeft(减去左侧加载缓冲宽度,可为负值) */
|
|
69
|
+
getLogicalScrollLeft(): number {
|
|
70
|
+
return this.cachedScrollLeft - this.deps.getLeftLoadBufferWidth()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 获取当前视口 */
|
|
74
|
+
getViewport(): Viewport | null {
|
|
75
|
+
return this._internalViewport
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 获取有效 DPR */
|
|
79
|
+
getEffectiveDpr(): number {
|
|
80
|
+
let dpr = this.preciseDpr > 0
|
|
81
|
+
? this.preciseDpr
|
|
82
|
+
: Math.round((window.devicePixelRatio || 1) * 64) / 64
|
|
83
|
+
if (dpr < 1) dpr = 1
|
|
84
|
+
return dpr
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 获取观测到的容器尺寸 */
|
|
88
|
+
getObservedSize(): { width: number; height: number } {
|
|
89
|
+
return this.observedSize
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 设置滚动位置(缓存 + 待写入) */
|
|
93
|
+
setScrollLeft(v: number): void {
|
|
94
|
+
this.cachedScrollLeft = v
|
|
95
|
+
this._pendingScrollLeft = v
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 仅设置缓存 scrollLeft(由 DataManager 内部使用) */
|
|
99
|
+
setCachedScrollLeft(v: number): void {
|
|
100
|
+
this.cachedScrollLeft = v
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 仅设置待写入 scrollLeft(由 DataManager 内部使用) */
|
|
104
|
+
setPendingScrollLeft(v: number): void {
|
|
105
|
+
this._pendingScrollLeft = v
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 在 RAF 回调中应用待写入的 scrollLeft */
|
|
109
|
+
applyPendingScrollLeft(container: HTMLElement): void {
|
|
110
|
+
if (this._pendingScrollLeft !== null) {
|
|
111
|
+
container.scrollLeft = this._pendingScrollLeft
|
|
112
|
+
this.cachedScrollLeft = container.scrollLeft
|
|
113
|
+
this._pendingScrollLeft = null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 初始化 ResizeObserver 和 scroll 监听 */
|
|
118
|
+
init(): void {
|
|
119
|
+
if (typeof ResizeObserver === 'undefined') return
|
|
120
|
+
|
|
121
|
+
const target = this.deps.getDom().container
|
|
122
|
+
if (!target) return
|
|
123
|
+
|
|
124
|
+
// 初始化 scrollLeft 缓存
|
|
125
|
+
this.cachedScrollLeft = target.scrollLeft
|
|
126
|
+
this.onScroll = () => { this.cachedScrollLeft = target.scrollLeft }
|
|
127
|
+
target.addEventListener('scroll', this.onScroll, { passive: true })
|
|
128
|
+
|
|
129
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
130
|
+
const entry = entries[0]
|
|
131
|
+
if (!entry) return
|
|
132
|
+
|
|
133
|
+
const prevWidth = this.observedSize.width
|
|
134
|
+
const prevHeight = this.observedSize.height
|
|
135
|
+
const prevDpr = this.preciseDpr
|
|
136
|
+
|
|
137
|
+
this.updateObservedMetrics(entry)
|
|
138
|
+
|
|
139
|
+
const widthChanged = this.observedSize.width !== prevWidth
|
|
140
|
+
const heightChanged = this.observedSize.height !== prevHeight
|
|
141
|
+
const dprChanged = this.preciseDpr !== prevDpr
|
|
142
|
+
if ((import.meta as any).env?.MODE !== 'production') {
|
|
143
|
+
console.log(
|
|
144
|
+
`[Chart] resize observer: ` +
|
|
145
|
+
`size ${prevWidth}x${prevHeight} -> ${this.observedSize.width}x${this.observedSize.height} ` +
|
|
146
|
+
`dpr ${prevDpr} -> ${this.preciseDpr} ` +
|
|
147
|
+
`changed: ${widthChanged || heightChanged ? 'size' : ''}${widthChanged || heightChanged && dprChanged ? '+' : ''}${dprChanged ? 'dpr' : ''}`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
if (widthChanged || heightChanged || dprChanged) {
|
|
151
|
+
this.deps.onResizeCompleted()
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.resizeObserver.observe(target, { box: 'device-pixel-content-box' as ResizeObserverBoxOptions })
|
|
157
|
+
} catch {
|
|
158
|
+
this.resizeObserver.observe(target)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 销毁 */
|
|
163
|
+
destroy(): void {
|
|
164
|
+
this.resizeObserver?.disconnect()
|
|
165
|
+
this.resizeObserver = undefined
|
|
166
|
+
this.preciseDpr = 0
|
|
167
|
+
this.observedSize = { width: 0, height: 0 }
|
|
168
|
+
|
|
169
|
+
if (this.onScroll) {
|
|
170
|
+
this.deps.getDom().container?.removeEventListener('scroll', this.onScroll)
|
|
171
|
+
this.onScroll = undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._internalViewport = null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 计算视口
|
|
179
|
+
*/
|
|
180
|
+
computeViewport(): Viewport | null {
|
|
181
|
+
const container = this.deps.getDom().container
|
|
182
|
+
if (!container) return null
|
|
183
|
+
|
|
184
|
+
const observedWidth = this.observedSize.width
|
|
185
|
+
const observedHeight = this.observedSize.height
|
|
186
|
+
const viewWidth = observedWidth > 0
|
|
187
|
+
? observedWidth
|
|
188
|
+
: Math.max(1, Math.round(container.clientWidth))
|
|
189
|
+
const viewHeight = observedHeight > 0
|
|
190
|
+
? observedHeight
|
|
191
|
+
: Math.max(1, Math.round(container.clientHeight))
|
|
192
|
+
|
|
193
|
+
const plotWidth = Math.round(viewWidth)
|
|
194
|
+
const plotHeight = Math.round(viewHeight - this.deps.getBottomAxisHeight())
|
|
195
|
+
|
|
196
|
+
let dpr = this.getEffectiveDpr()
|
|
197
|
+
|
|
198
|
+
const MAX_CANVAS_PIXELS = 16 * 1024 * 1024
|
|
199
|
+
const requestedPixels = viewWidth * dpr * (viewHeight * dpr)
|
|
200
|
+
if (requestedPixels > MAX_CANVAS_PIXELS) {
|
|
201
|
+
dpr = Math.sqrt(MAX_CANVAS_PIXELS / (viewWidth * viewHeight))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 对齐 scrollLeft,消除 translate 亚像素偏移
|
|
205
|
+
const scrollLeft = Math.round(this.getLogicalScrollLeft() * dpr) / dpr
|
|
206
|
+
|
|
207
|
+
const dom = this.deps.getDom()
|
|
208
|
+
|
|
209
|
+
const canvasLayerWidth = `${viewWidth}px`
|
|
210
|
+
if (dom.canvasLayer.style.width !== canvasLayerWidth) {
|
|
211
|
+
dom.canvasLayer.style.width = canvasLayerWidth
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const canvasLayerHeight = `${viewHeight}px`
|
|
215
|
+
if (dom.canvasLayer.style.height !== canvasLayerHeight) {
|
|
216
|
+
dom.canvasLayer.style.height = canvasLayerHeight
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const xAxisWidth = Math.round(plotWidth * dpr)
|
|
220
|
+
if (dom.xAxisCanvas.width !== xAxisWidth) {
|
|
221
|
+
dom.xAxisCanvas.width = xAxisWidth
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const xAxisHeight = Math.round(this.deps.getBottomAxisHeight() * dpr)
|
|
225
|
+
if (dom.xAxisCanvas.height !== xAxisHeight) {
|
|
226
|
+
dom.xAxisCanvas.height = xAxisHeight
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const xAxisCssWidth = `${xAxisWidth / dpr}px`
|
|
230
|
+
if (dom.xAxisCanvas.style.width !== xAxisCssWidth) {
|
|
231
|
+
dom.xAxisCanvas.style.width = xAxisCssWidth
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const xAxisCssHeight = `${xAxisHeight / dpr}px`
|
|
235
|
+
if (dom.xAxisCanvas.style.height !== xAxisCssHeight) {
|
|
236
|
+
dom.xAxisCanvas.style.height = xAxisCssHeight
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.deps.resizeSharedWebGLSurface(plotWidth, plotHeight, dpr)
|
|
240
|
+
|
|
241
|
+
const vp: Viewport = {
|
|
242
|
+
viewWidth,
|
|
243
|
+
viewHeight,
|
|
244
|
+
plotWidth,
|
|
245
|
+
plotHeight,
|
|
246
|
+
scrollLeft,
|
|
247
|
+
dpr,
|
|
248
|
+
}
|
|
249
|
+
const prevViewport = this._internalViewport
|
|
250
|
+
const viewportChanged = !prevViewport
|
|
251
|
+
|| prevViewport.viewWidth !== vp.viewWidth
|
|
252
|
+
|| prevViewport.viewHeight !== vp.viewHeight
|
|
253
|
+
|| prevViewport.plotWidth !== vp.plotWidth
|
|
254
|
+
|| prevViewport.plotHeight !== vp.plotHeight
|
|
255
|
+
|| prevViewport.scrollLeft !== vp.scrollLeft
|
|
256
|
+
|| prevViewport.dpr !== vp.dpr
|
|
257
|
+
|
|
258
|
+
this._internalViewport = vp
|
|
259
|
+
if (viewportChanged) {
|
|
260
|
+
const current = this._viewportSignal.peek()
|
|
261
|
+
this._viewportSignal.set({
|
|
262
|
+
zoomLevel: current.zoomLevel,
|
|
263
|
+
plotWidth: vp.plotWidth,
|
|
264
|
+
plotHeight: vp.plotHeight,
|
|
265
|
+
dpr: vp.dpr > 0 ? vp.dpr : current.dpr,
|
|
266
|
+
visibleFrom: current.visibleFrom,
|
|
267
|
+
visibleTo: current.visibleTo,
|
|
268
|
+
kWidth: current.kWidth,
|
|
269
|
+
kGap: current.kGap,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
return vp
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 更新 viewport signal(用于滚动事件/缩放后的信号同步)
|
|
277
|
+
*/
|
|
278
|
+
updateViewportSignal(): void {
|
|
279
|
+
const vp = this._internalViewport
|
|
280
|
+
if (!vp) return
|
|
281
|
+
|
|
282
|
+
this._viewportSignal.set({
|
|
283
|
+
zoomLevel: this.deps.getZoomLevel(),
|
|
284
|
+
plotWidth: vp.plotWidth,
|
|
285
|
+
plotHeight: vp.plotHeight,
|
|
286
|
+
dpr: vp.dpr,
|
|
287
|
+
visibleFrom: this.deps.getLastVisibleRange().start,
|
|
288
|
+
visibleTo: this.deps.getLastVisibleRange().end,
|
|
289
|
+
kWidth: this.deps.getKWidth(),
|
|
290
|
+
kGap: this.deps.getKGap(),
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private updateObservedMetrics(entry: ResizeObserverEntry) {
|
|
295
|
+
const cssWidth = Math.max(1, Math.round(entry.contentRect.width))
|
|
296
|
+
const cssHeight = Math.max(1, Math.round(entry.contentRect.height))
|
|
297
|
+
this.observedSize.width = cssWidth
|
|
298
|
+
this.observedSize.height = cssHeight
|
|
299
|
+
|
|
300
|
+
const pixelSize = entry.devicePixelContentBoxSize?.[0]
|
|
301
|
+
const cssSize = entry.contentBoxSize?.[0]
|
|
302
|
+
if (!pixelSize || !cssSize || cssSize.inlineSize <= 0) {
|
|
303
|
+
this.preciseDpr = 0
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const raw = pixelSize.inlineSize / cssSize.inlineSize
|
|
308
|
+
this.preciseDpr = Math.round(raw * 64) / 64
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/index.ts
CHANGED
package/src/plugin/types.ts
CHANGED
|
@@ -273,8 +273,11 @@ export interface RenderContext {
|
|
|
273
273
|
ctx: CanvasRenderingContext2D
|
|
274
274
|
pane: PaneInfo
|
|
275
275
|
data: unknown[]
|
|
276
|
+
/** K线级别,如 'daily'、'5min'、'15min' */
|
|
277
|
+
period: string
|
|
276
278
|
comparisonData?: ReadonlyMap<string, ReadonlyArray<KLineData>>
|
|
277
279
|
comparisonSymbols?: ReadonlyArray<import('../controllers/types').SymbolSpec>
|
|
280
|
+
comparisonColors?: ReadonlyMap<string, string>
|
|
278
281
|
range: { start: number; end: number }
|
|
279
282
|
scrollLeft: number
|
|
280
283
|
kWidth: number
|
package/src/semantic/index.ts
CHANGED
package/src/semantic/schema.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"type": "string",
|
|
34
34
|
"enum": ["daily", "weekly", "monthly", "5min", "15min", "30min", "60min"]
|
|
35
35
|
},
|
|
36
|
-
"adjust": { "type": "string", "enum": ["qfq", "hfq", "none"] }
|
|
36
|
+
"adjust": { "type": "string", "enum": ["qfq", "hfq", "splits", "none"] }
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"IndicatorsConfig": {
|
package/src/semantic/types.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SemanticChartConfig {
|
|
|
19
19
|
// ============ 数据配置 ============
|
|
20
20
|
|
|
21
21
|
/** 数据配置 */
|
|
22
|
+
export type AdjustType = 'qfq' | 'hfq' | 'splits' | 'none'
|
|
23
|
+
|
|
22
24
|
export interface DataConfig {
|
|
23
25
|
source: 'baostock' | 'dongcai'
|
|
24
26
|
/** 股票代码(6位数字,不含前缀) */
|
|
@@ -30,7 +32,7 @@ export interface DataConfig {
|
|
|
30
32
|
/** 结束日期 YYYY-MM-DD */
|
|
31
33
|
endDate: string
|
|
32
34
|
period: 'daily' | 'weekly' | 'monthly' | '5min' | '15min' | '30min' | '60min'
|
|
33
|
-
adjust:
|
|
35
|
+
adjust: AdjustType
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// ============ 指标配置 ============
|
|
@@ -57,10 +57,6 @@ exports[`theme baseline — dark > CSS declaration block (snapshot) 1`] = `
|
|
|
57
57
|
--klc-color-text-tertiary: hsl(210, 6%, 60%);
|
|
58
58
|
--klc-color-text-weak: hsl(210, 5%, 45%);
|
|
59
59
|
--klc-color-text-white: rgba(255, 255, 255, 0.95);
|
|
60
|
-
--klc-color-price-up-light: rgba(255, 80, 100, 0.85);
|
|
61
|
-
--klc-color-price-up-tick: hsl(0, 70%, 60%);
|
|
62
|
-
--klc-color-price-down-light: rgba(60, 200, 160, 0.85);
|
|
63
|
-
--klc-color-price-down-tick: hsl(150, 50%, 65%);
|
|
64
60
|
--klc-color-price-last-price: rgba(230, 100, 115, 0.95);
|
|
65
61
|
--klc-color-tag-bg-white: rgb(40, 40, 55);
|
|
66
62
|
--klc-color-tag-bg-light-gray: rgba(50, 50, 65, 0.92);
|
|
@@ -254,11 +250,7 @@ exports[`theme baseline — light > CSS declaration block (snapshot) 1`] = `
|
|
|
254
250
|
--klc-color-text-tertiary: hsl(210, 8%, 50%);
|
|
255
251
|
--klc-color-text-weak: hsl(210, 7%, 65%);
|
|
256
252
|
--klc-color-text-white: rgba(255, 255, 255, 0.92);
|
|
257
|
-
--klc-color-price-
|
|
258
|
-
--klc-color-price-up-tick: hsl(0, 60%, 50%);
|
|
259
|
-
--klc-color-price-down-light: rgba(3, 123, 102, 0.92);
|
|
260
|
-
--klc-color-price-down-tick: hsl(150, 30%, 60%);
|
|
261
|
-
--klc-color-price-last-price: rgba(196, 74, 86, 0.95);
|
|
253
|
+
--klc-color-price-last-price: rgba(230, 100, 115, 0.95);
|
|
262
254
|
--klc-color-tag-bg-white: rgb(255, 255, 255);
|
|
263
255
|
--klc-color-tag-bg-light-gray: rgba(255, 255, 255, 0.92);
|
|
264
256
|
--klc-color-tag-bg-pure-white: #ffffff;
|
|
@@ -56,10 +56,6 @@ export function withAsiaMarketColors(theme: Theme): Theme {
|
|
|
56
56
|
// ── Nested: price accents ──
|
|
57
57
|
price: {
|
|
58
58
|
...theme.colors.price,
|
|
59
|
-
upLight: theme.colors.price.downLight,
|
|
60
|
-
downLight: theme.colors.price.upLight,
|
|
61
|
-
upTick: theme.colors.price.downTick,
|
|
62
|
-
downTick: theme.colors.price.upTick,
|
|
63
59
|
},
|
|
64
60
|
|
|
65
61
|
// ── Nested: MACD histogram bars ──
|
package/src/tokens/theme-dark.ts
CHANGED
|
@@ -96,10 +96,6 @@ export const darkTheme: Theme = {
|
|
|
96
96
|
white: 'rgba(255, 255, 255, 0.95)',
|
|
97
97
|
},
|
|
98
98
|
price: {
|
|
99
|
-
upLight: 'rgba(255, 80, 100, 0.85)',
|
|
100
|
-
upTick: 'hsl(0, 70%, 60%)',
|
|
101
|
-
downLight: 'rgba(60, 200, 160, 0.85)',
|
|
102
|
-
downTick: 'hsl(150, 50%, 65%)',
|
|
103
99
|
lastPrice: 'rgba(230, 100, 115, 0.95)',
|
|
104
100
|
},
|
|
105
101
|
tagBg: {
|
|
@@ -101,12 +101,8 @@ export const lightTheme: Theme = {
|
|
|
101
101
|
weak: 'hsl(210, 7%, 65%)',
|
|
102
102
|
white: 'rgba(255, 255, 255, 0.92)',
|
|
103
103
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
upTick: 'hsl(0, 60%, 50%)',
|
|
107
|
-
downLight: 'rgba(3, 123, 102, 0.92)',
|
|
108
|
-
downTick: 'hsl(150, 30%, 60%)',
|
|
109
|
-
lastPrice: 'rgba(196, 74, 86, 0.95)',
|
|
104
|
+
price: {
|
|
105
|
+
lastPrice: 'rgba(230, 100, 115, 0.95)',
|
|
110
106
|
},
|
|
111
107
|
tagBg: {
|
|
112
108
|
white: 'rgb(255, 255, 255)',
|
package/src/tokens/types.ts
CHANGED
|
@@ -85,10 +85,6 @@ export interface TextColors {
|
|
|
85
85
|
* (candleUpBody / candleDownBody); this group covers extras.
|
|
86
86
|
*/
|
|
87
87
|
export interface PriceColors {
|
|
88
|
-
readonly upLight: ColorValue
|
|
89
|
-
readonly upTick: ColorValue
|
|
90
|
-
readonly downLight: ColorValue
|
|
91
|
-
readonly downTick: ColorValue
|
|
92
88
|
readonly lastPrice: ColorValue
|
|
93
89
|
}
|
|
94
90
|
|
package/src/types/price.ts
CHANGED
package/src/utils/dateFormat.ts
CHANGED
|
@@ -137,6 +137,39 @@ export function monthKey(timestamp: number): number {
|
|
|
137
137
|
|
|
138
138
|
// ========== 便捷别名 ==========
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 格式化时间戳为日期/日期时间字符串,支持可配置时区
|
|
142
|
+
* @param timestamp - 时间戳(毫秒)
|
|
143
|
+
* @param options - 配置项
|
|
144
|
+
* @param options.timeZone - 时区,默认 'Asia/Shanghai'
|
|
145
|
+
* @param options.showTime - 是否显示时间,默认 false
|
|
146
|
+
* @returns 格式化后的字符串,如 "2026-05-15" 或 "2026-05-15 09:35"
|
|
147
|
+
*/
|
|
148
|
+
export function formatTimestamp(
|
|
149
|
+
timestamp: number,
|
|
150
|
+
options?: { timeZone?: string; showTime?: boolean }
|
|
151
|
+
): string {
|
|
152
|
+
const timeZone = options?.timeZone ?? 'Asia/Shanghai'
|
|
153
|
+
const showTime = options?.showTime ?? false
|
|
154
|
+
const formatter = new Intl.DateTimeFormat('zh-CN', {
|
|
155
|
+
timeZone,
|
|
156
|
+
year: 'numeric',
|
|
157
|
+
month: '2-digit',
|
|
158
|
+
day: '2-digit',
|
|
159
|
+
...(showTime ? { hour: '2-digit', minute: '2-digit', hour12: false } : {}),
|
|
160
|
+
})
|
|
161
|
+
const parts = formatter.formatToParts(new Date(timestamp))
|
|
162
|
+
let y = '', m = '', d = '', h = '', min = ''
|
|
163
|
+
for (const p of parts) {
|
|
164
|
+
if (p.type === 'year') y = p.value
|
|
165
|
+
else if (p.type === 'month') m = p.value
|
|
166
|
+
else if (p.type === 'day') d = p.value
|
|
167
|
+
else if (p.type === 'hour') h = p.value
|
|
168
|
+
else if (p.type === 'minute') min = p.value
|
|
169
|
+
}
|
|
170
|
+
return showTime ? `${y}-${m}-${d} ${h}:${min}` : `${y}-${m}-${d}`
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
/**
|
|
141
174
|
* formatDateToYYYYMMDD 的别名,保持与历史代码的兼容性
|
|
142
175
|
* timestamp 是"上海时区当天 00:00:00"映射到 UTC 的值;显示时强制按上海时区格式化
|
|
@@ -206,3 +239,55 @@ export function findMonthBoundaries(data: Array<{ timestamp: number } | undefine
|
|
|
206
239
|
_cacheResult = boundaries
|
|
207
240
|
return boundaries
|
|
208
241
|
}
|
|
242
|
+
|
|
243
|
+
// ========== 日边界查找 + 日标签格式化 ==========
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 查找每天第一个K线的索引
|
|
247
|
+
*/
|
|
248
|
+
export function findDayBoundaries(data: Array<{ timestamp: number } | undefined>): number[] {
|
|
249
|
+
if (data.length === 0) return []
|
|
250
|
+
|
|
251
|
+
const boundaries: number[] = [0]
|
|
252
|
+
let lastKey = dayKey(data[0]!.timestamp)
|
|
253
|
+
|
|
254
|
+
for (let i = 1; i < data.length; i++) {
|
|
255
|
+
const cur = data[i]
|
|
256
|
+
if (!cur) continue
|
|
257
|
+
const curKey = dayKey(cur.timestamp)
|
|
258
|
+
if (curKey !== lastKey) {
|
|
259
|
+
boundaries.push(i)
|
|
260
|
+
lastKey = curKey
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return boundaries
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function dayKey(timestamp: number): number {
|
|
268
|
+
const d = new Date(timestamp)
|
|
269
|
+
return d.getFullYear() * 366 + getDayOfYear(d)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 格式化日期为 "MM-DD" 或年初显示 "YYYY-MM-DD"
|
|
274
|
+
*/
|
|
275
|
+
export function formatDay(timestamp: number): { text: string; isYear: boolean } {
|
|
276
|
+
const d = new Date(timestamp)
|
|
277
|
+
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
278
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
279
|
+
const isYear = d.getMonth() === 0 && d.getDate() === 1
|
|
280
|
+
if (isYear) {
|
|
281
|
+
return { text: `${d.getFullYear()}-${month}-${day}`, isYear }
|
|
282
|
+
}
|
|
283
|
+
return { text: `${month}-${day}`, isYear }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 兼容 getDayOfYear — fallback when not on Date prototype
|
|
287
|
+
function getDayOfYear(date: Date): number {
|
|
288
|
+
const start = new Date(date.getFullYear(), 0, 0)
|
|
289
|
+
const diff = date.getTime() - start.getTime()
|
|
290
|
+
return Math.floor(diff / 86400000)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KLineData } from '../../types/price'
|
|
2
2
|
import { priceToY, yToPrice } from '../priceToY'
|
|
3
3
|
import { alignToPhysicalPixelCenter, roundToPhysicalPixel } from '../../engine/draw/pixelAlign'
|
|
4
|
-
import { formatYMDShanghai, formatMonthOrYear, findMonthBoundaries } from '../../utils/dateFormat'
|
|
4
|
+
import { formatYMDShanghai, formatMonthOrYear, formatDay, findMonthBoundaries, findDayBoundaries } from '../../utils/dateFormat'
|
|
5
5
|
import { resolveThemeColors } from '../../tokens'
|
|
6
6
|
import type { ColorPresetSettings } from '../../tokens'
|
|
7
7
|
import { getFont, setCanvasFont } from '../../engine/theme/fonts'
|
|
@@ -133,6 +133,8 @@ export interface TimeAxisOptions {
|
|
|
133
133
|
drawTopBorder?: boolean
|
|
134
134
|
/** 是否绘制底部边界线(默认 true,如果副图已有下边框则设为 false 避免重复) */
|
|
135
135
|
drawBottomBorder?: boolean
|
|
136
|
+
/** K线级别,如 'daily'、'5min'、'15min' */
|
|
137
|
+
period: string
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
export interface LastPriceLineOptions {
|
|
@@ -406,15 +408,21 @@ export function drawTimeAxis(ctx: CanvasRenderingContext2D, opts: TimeAxisOption
|
|
|
406
408
|
const regularFont = getFont(fontSize)
|
|
407
409
|
const boldFont = getFont(fontSize, { bold: true })
|
|
408
410
|
|
|
409
|
-
const
|
|
411
|
+
const isMinuteData = opts.period.includes('min')
|
|
412
|
+
const showOnlyYear = !isMinuteData && opts.period !== 'daily'
|
|
413
|
+
const boundaries = isMinuteData ? findDayBoundaries(data) : findMonthBoundaries(data)
|
|
410
414
|
const visibleBoundaries = boundaries.filter((idx: number) => idx >= startIndex && idx < endIndex)
|
|
411
415
|
|
|
412
|
-
let
|
|
416
|
+
let lastBold: boolean | null = null
|
|
417
|
+
const labelFn = isMinuteData ? formatDay : formatMonthOrYear
|
|
413
418
|
|
|
414
419
|
for (const idx of visibleBoundaries) {
|
|
415
420
|
const k = data[idx]
|
|
416
421
|
if (!k) continue
|
|
417
422
|
|
|
423
|
+
const { text, isYear } = labelFn(k.timestamp)
|
|
424
|
+
if (showOnlyYear && !isYear) continue
|
|
425
|
+
|
|
418
426
|
const worldX = startX + idx * unit + alignedKWidth / 2
|
|
419
427
|
const screenX = worldX - scrollLeft
|
|
420
428
|
|
|
@@ -423,10 +431,9 @@ export function drawTimeAxis(ctx: CanvasRenderingContext2D, opts: TimeAxisOption
|
|
|
423
431
|
|
|
424
432
|
if (screenX >= minX && screenX <= maxX) {
|
|
425
433
|
const drawX = Math.min(Math.max(screenX, minX), maxX)
|
|
426
|
-
|
|
427
|
-
if (lastWasYear !== isYear) {
|
|
434
|
+
if (lastBold !== isYear) {
|
|
428
435
|
setCanvasFont(ctx, isYear ? boldFont : regularFont)
|
|
429
|
-
|
|
436
|
+
lastBold = isYear
|
|
430
437
|
}
|
|
431
438
|
ctx.fillText(text, roundToPhysicalPixel(drawX, dpr), alignToPhysicalPixelCenter(textY, dpr))
|
|
432
439
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.8.
|
|
1
|
+
export const VERSION = "0.8.2"
|