@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.
Files changed (167) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +21 -1
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +6 -1
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.js +3 -3
  7. package/dist/data-fetchers/baostock.js.map +1 -1
  8. package/dist/data-fetchers/dataBuffer.d.ts +5 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +85 -48
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/gotdx.d.ts +3 -0
  13. package/dist/data-fetchers/gotdx.d.ts.map +1 -0
  14. package/dist/data-fetchers/gotdx.js +101 -0
  15. package/dist/data-fetchers/gotdx.js.map +1 -0
  16. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  17. package/dist/data-fetchers/hundred-mock.js +28 -5
  18. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  19. package/dist/data-fetchers/index.d.ts +1 -0
  20. package/dist/data-fetchers/index.d.ts.map +1 -1
  21. package/dist/data-fetchers/index.js +1 -0
  22. package/dist/data-fetchers/index.js.map +1 -1
  23. package/dist/data-fetchers/router.d.ts.map +1 -1
  24. package/dist/data-fetchers/router.js +3 -0
  25. package/dist/data-fetchers/router.js.map +1 -1
  26. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  27. package/dist/data-fetchers/thousand-mock.js +24 -14
  28. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  29. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  30. package/dist/data-fetchers/tradingview.js +12 -6
  31. package/dist/data-fetchers/tradingview.js.map +1 -1
  32. package/dist/engine/chart.d.ts +29 -367
  33. package/dist/engine/chart.d.ts.map +1 -1
  34. package/dist/engine/chart.js +239 -1842
  35. package/dist/engine/chart.js.map +1 -1
  36. package/dist/engine/chartContext.d.ts +24 -0
  37. package/dist/engine/chartContext.d.ts.map +1 -0
  38. package/dist/engine/chartContext.js +19 -0
  39. package/dist/engine/chartContext.js.map +1 -0
  40. package/dist/engine/chartTypes.d.ts +77 -0
  41. package/dist/engine/chartTypes.d.ts.map +1 -0
  42. package/dist/engine/chartTypes.js +2 -0
  43. package/dist/engine/chartTypes.js.map +1 -0
  44. package/dist/engine/data/chartDataManager.d.ts +103 -0
  45. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  46. package/dist/engine/data/chartDataManager.js +593 -0
  47. package/dist/engine/data/chartDataManager.js.map +1 -0
  48. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  49. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  50. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  51. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  52. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  53. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  54. package/dist/engine/layout/chartPaneLayout.js +388 -0
  55. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  56. package/dist/engine/render/chartRenderer.d.ts +86 -0
  57. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  58. package/dist/engine/render/chartRenderer.js +440 -0
  59. package/dist/engine/render/chartRenderer.js.map +1 -0
  60. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  61. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  62. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  63. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  64. package/dist/engine/renderers/comparisonLine.js +25 -11
  65. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  66. package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
  67. package/dist/engine/renderers/timeAxis.js +1 -0
  68. package/dist/engine/renderers/timeAxis.js.map +1 -1
  69. package/dist/engine/subPaneManager.d.ts +27 -6
  70. package/dist/engine/subPaneManager.d.ts.map +1 -1
  71. package/dist/engine/subPaneManager.js +54 -56
  72. package/dist/engine/subPaneManager.js.map +1 -1
  73. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  74. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  75. package/dist/engine/utils/chartZoomController.js +66 -0
  76. package/dist/engine/utils/chartZoomController.js.map +1 -0
  77. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  78. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  79. package/dist/engine/viewport/chartViewportManager.js +249 -0
  80. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/plugin/types.d.ts +3 -0
  86. package/dist/plugin/types.d.ts.map +1 -1
  87. package/dist/plugin/types.js.map +1 -1
  88. package/dist/semantic/index.d.ts +1 -1
  89. package/dist/semantic/index.d.ts.map +1 -1
  90. package/dist/semantic/index.js.map +1 -1
  91. package/dist/semantic/schema.json +1 -1
  92. package/dist/semantic/types.d.ts +2 -1
  93. package/dist/semantic/types.d.ts.map +1 -1
  94. package/dist/tokens/theme-china.d.ts.map +1 -1
  95. package/dist/tokens/theme-china.js +0 -4
  96. package/dist/tokens/theme-china.js.map +1 -1
  97. package/dist/tokens/theme-dark.d.ts.map +1 -1
  98. package/dist/tokens/theme-dark.js +0 -4
  99. package/dist/tokens/theme-dark.js.map +1 -1
  100. package/dist/tokens/theme-light.d.ts.map +1 -1
  101. package/dist/tokens/theme-light.js +1 -5
  102. package/dist/tokens/theme-light.js.map +1 -1
  103. package/dist/tokens/types.d.ts +0 -4
  104. package/dist/tokens/types.d.ts.map +1 -1
  105. package/dist/types/price.d.ts +2 -0
  106. package/dist/types/price.d.ts.map +1 -1
  107. package/dist/types/price.js.map +1 -1
  108. package/dist/utils/dateFormat.d.ts +25 -0
  109. package/dist/utils/dateFormat.d.ts.map +1 -1
  110. package/dist/utils/dateFormat.js +78 -0
  111. package/dist/utils/dateFormat.js.map +1 -1
  112. package/dist/utils/kLineDraw/axis.d.ts +2 -0
  113. package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
  114. package/dist/utils/kLineDraw/axis.js +11 -6
  115. package/dist/utils/kLineDraw/axis.js.map +1 -1
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.d.ts.map +1 -1
  118. package/dist/version.js +1 -1
  119. package/dist/version.js.map +1 -1
  120. package/package.json +1 -1
  121. package/src/controllers/createChartController.ts +39 -10
  122. package/src/controllers/types.ts +6 -1
  123. package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
  124. package/src/data-fetchers/baostock.ts +3 -3
  125. package/src/data-fetchers/dataBuffer.ts +70 -23
  126. package/src/data-fetchers/gotdx.ts +138 -0
  127. package/src/data-fetchers/hundred-mock.ts +35 -5
  128. package/src/data-fetchers/index.ts +1 -0
  129. package/src/data-fetchers/router.ts +3 -0
  130. package/src/data-fetchers/thousand-mock.ts +30 -14
  131. package/src/data-fetchers/tradingview.ts +12 -6
  132. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  133. package/src/engine/chart.ts +252 -2250
  134. package/src/engine/chartContext.ts +34 -0
  135. package/src/engine/chartTypes.ts +88 -0
  136. package/src/engine/data/chartDataManager.ts +695 -0
  137. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  138. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  139. package/src/engine/layout/chartPaneLayout.ts +474 -0
  140. package/src/engine/render/chartRenderer.ts +581 -0
  141. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  142. package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
  143. package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
  144. package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
  145. package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
  146. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
  147. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
  148. package/src/engine/renderers/comparisonLine.ts +25 -11
  149. package/src/engine/renderers/timeAxis.ts +1 -0
  150. package/src/engine/subPaneManager.ts +75 -59
  151. package/src/engine/utils/chartZoomController.ts +104 -0
  152. package/src/engine/viewport/chartViewportManager.ts +310 -0
  153. package/src/index.ts +1 -0
  154. package/src/plugin/types.ts +3 -0
  155. package/src/semantic/index.ts +1 -0
  156. package/src/semantic/schema.json +1 -1
  157. package/src/semantic/types.ts +3 -1
  158. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  159. package/src/tokens/theme-china.ts +0 -4
  160. package/src/tokens/theme-dark.ts +0 -4
  161. package/src/tokens/theme-light.ts +2 -6
  162. package/src/tokens/types.ts +0 -4
  163. package/src/types/price.ts +2 -0
  164. package/src/utils/dateFormat.ts +85 -0
  165. package/src/utils/kLineDraw/axis.ts +13 -6
  166. package/src/version.ts +1 -1
  167. 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
@@ -2,3 +2,4 @@ export * from './reactivity'
2
2
  export * from './controllers'
3
3
  export { VERSION } from './version'
4
4
  export * from './tokens'
5
+ export { formatTimestamp } from './utils/dateFormat'
@@ -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
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  SemanticChartConfig,
3
+ AdjustType,
3
4
  DataConfig,
4
5
  IndicatorsConfig,
5
6
  MainIndicatorConfig,
@@ -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": {
@@ -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: 'qfq' | 'hfq' | 'none'
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-up-light: rgba(214, 10, 34, 0.92);
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 ──
@@ -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
- price: {
105
- upLight: 'rgba(214, 10, 34, 0.92)',
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)',
@@ -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
 
@@ -1,6 +1,8 @@
1
1
  export interface KLineData {
2
2
  /* 时间戳(毫秒) */
3
3
  timestamp: number
4
+ /** 日期字符串(如 "2025-06-16"),用于跨品种精确匹配 */
5
+ date?: string
4
6
  /* 开盘价 */
5
7
  open: number
6
8
  /* 最高价 */
@@ -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 boundaries = findMonthBoundaries(data)
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 lastWasYear: boolean | null = null
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
- const { text, isYear } = formatMonthOrYear(k.timestamp)
427
- if (lastWasYear !== isYear) {
434
+ if (lastBold !== isYear) {
428
435
  setCanvasFont(ctx, isYear ? boldFont : regularFont)
429
- lastWasYear = isYear
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-alpha.4"
1
+ export const VERSION = "0.8.2"