@363045841yyt/klinechart-core 0.7.3 → 0.7.5

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 (247) hide show
  1. package/README.md +201 -201
  2. package/README.zh-CN.md +201 -201
  3. package/dist/controllers/index.d.ts +1 -0
  4. package/dist/controllers/index.d.ts.map +1 -1
  5. package/dist/controllers/index.js +1 -0
  6. package/dist/controllers/index.js.map +1 -1
  7. package/dist/engine/chart.d.ts +11 -19
  8. package/dist/engine/chart.d.ts.map +1 -1
  9. package/dist/engine/chart.js +92 -109
  10. package/dist/engine/chart.js.map +1 -1
  11. package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
  12. package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
  13. package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
  14. package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
  15. package/dist/engine/renderers/webgl/candleSurface.js +47 -47
  16. package/dist/engine/subPaneManager.d.ts +4 -0
  17. package/dist/engine/subPaneManager.d.ts.map +1 -1
  18. package/dist/engine/subPaneManager.js +13 -0
  19. package/dist/engine/subPaneManager.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.d.ts.map +1 -1
  22. package/dist/version.js +1 -2
  23. package/dist/version.js.map +1 -1
  24. package/package.json +129 -122
  25. package/src/__tests__/signal.test.ts +124 -124
  26. package/src/config/chartSettings.ts +66 -66
  27. package/src/controllers/__tests__/drawing.test.ts +214 -214
  28. package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
  29. package/src/controllers/__tests__/toolbar.test.ts +225 -225
  30. package/src/controllers/createChartController.ts +665 -665
  31. package/src/controllers/createDrawingController.ts +96 -96
  32. package/src/controllers/createIndicatorSelectorController.ts +307 -307
  33. package/src/controllers/createToolbarController.ts +146 -146
  34. package/src/controllers/index.ts +20 -19
  35. package/src/controllers/types.ts +284 -284
  36. package/src/engine/__tests__/chart.dpr.test.ts +401 -401
  37. package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
  38. package/src/engine/chart-store.ts +121 -121
  39. package/src/engine/chart.d.ts +617 -617
  40. package/src/engine/chart.ts +2803 -2815
  41. package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
  42. package/src/engine/controller/interaction.ts +722 -722
  43. package/src/engine/controller/markerInteraction.ts +130 -130
  44. package/src/engine/controller/pinchTracker.ts +82 -82
  45. package/src/engine/controller/tooltipPosition.ts +48 -48
  46. package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
  47. package/src/engine/draw/pixelAlign.ts +259 -259
  48. package/src/engine/drawing/index.ts +655 -655
  49. package/src/engine/drawing/interaction.ts +842 -842
  50. package/src/engine/drawing/plugin.ts +343 -343
  51. package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
  52. package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
  53. package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
  54. package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
  55. package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
  56. package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
  57. package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
  58. package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
  59. package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
  60. package/src/engine/indicators/__tests__/atr.test.ts +153 -153
  61. package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
  62. package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
  63. package/src/engine/indicators/__tests__/dema.test.ts +73 -73
  64. package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
  65. package/src/engine/indicators/__tests__/hma.test.ts +73 -73
  66. package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
  67. package/src/engine/indicators/__tests__/kama.test.ts +80 -80
  68. package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
  69. package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
  70. package/src/engine/indicators/__tests__/roc.test.ts +68 -68
  71. package/src/engine/indicators/__tests__/sar.test.ts +86 -86
  72. package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
  73. package/src/engine/indicators/__tests__/soa.test.ts +533 -533
  74. package/src/engine/indicators/__tests__/structure.test.ts +110 -110
  75. package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
  76. package/src/engine/indicators/__tests__/tema.test.ts +68 -68
  77. package/src/engine/indicators/__tests__/trix.test.ts +70 -70
  78. package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
  79. package/src/engine/indicators/__tests__/volume.test.ts +115 -115
  80. package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
  81. package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
  82. package/src/engine/indicators/__tests__/wma.test.ts +112 -112
  83. package/src/engine/indicators/__tests__/zones.test.ts +95 -95
  84. package/src/engine/indicators/atrState.ts +27 -27
  85. package/src/engine/indicators/bollState.ts +51 -51
  86. package/src/engine/indicators/calculators.ts +2593 -2593
  87. package/src/engine/indicators/cciState.ts +25 -25
  88. package/src/engine/indicators/chaikinVolState.ts +32 -32
  89. package/src/engine/indicators/cmfState.ts +27 -27
  90. package/src/engine/indicators/demaState.ts +27 -27
  91. package/src/engine/indicators/donchianState.ts +43 -43
  92. package/src/engine/indicators/eneState.ts +43 -43
  93. package/src/engine/indicators/expmaState.ts +43 -43
  94. package/src/engine/indicators/fastkState.ts +25 -25
  95. package/src/engine/indicators/fibState.ts +41 -41
  96. package/src/engine/indicators/hmaState.ts +27 -27
  97. package/src/engine/indicators/hvState.ts +28 -28
  98. package/src/engine/indicators/ichimokuState.ts +70 -70
  99. package/src/engine/indicators/indicator.worker.ts +169 -169
  100. package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
  101. package/src/engine/indicators/indicatorMetadata.ts +110 -110
  102. package/src/engine/indicators/indicatorRegistry.ts +106 -106
  103. package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
  104. package/src/engine/indicators/kamaState.ts +34 -34
  105. package/src/engine/indicators/keltnerState.ts +49 -49
  106. package/src/engine/indicators/kstState.ts +42 -42
  107. package/src/engine/indicators/maState.ts +36 -36
  108. package/src/engine/indicators/macdState.ts +76 -76
  109. package/src/engine/indicators/mfiState.ts +27 -27
  110. package/src/engine/indicators/momState.ts +25 -25
  111. package/src/engine/indicators/obvState.ts +25 -25
  112. package/src/engine/indicators/parkinsonState.ts +28 -28
  113. package/src/engine/indicators/pivotState.ts +51 -51
  114. package/src/engine/indicators/pvtState.ts +25 -25
  115. package/src/engine/indicators/rocState.ts +27 -27
  116. package/src/engine/indicators/rsiState.ts +65 -65
  117. package/src/engine/indicators/sarState.ts +41 -41
  118. package/src/engine/indicators/scheduler.ts +1205 -1205
  119. package/src/engine/indicators/soa.ts +352 -352
  120. package/src/engine/indicators/stateComposer.ts +1262 -1262
  121. package/src/engine/indicators/stochState.ts +26 -26
  122. package/src/engine/indicators/structureState.ts +69 -69
  123. package/src/engine/indicators/supertrendState.ts +37 -37
  124. package/src/engine/indicators/temaState.ts +27 -27
  125. package/src/engine/indicators/trixState.ts +35 -35
  126. package/src/engine/indicators/vmaState.ts +27 -27
  127. package/src/engine/indicators/volumeProfileState.ts +63 -63
  128. package/src/engine/indicators/vwapState.ts +29 -29
  129. package/src/engine/indicators/wmaState.ts +27 -27
  130. package/src/engine/indicators/wmsrState.ts +25 -25
  131. package/src/engine/indicators/workerProtocol.ts +613 -613
  132. package/src/engine/indicators/zonesState.ts +47 -47
  133. package/src/engine/layout/pane.ts +161 -161
  134. package/src/engine/marker/registry.ts +265 -265
  135. package/src/engine/paneRenderer.ts +169 -169
  136. package/src/engine/renderers/Indicator/atr.ts +237 -237
  137. package/src/engine/renderers/Indicator/boll.ts +317 -317
  138. package/src/engine/renderers/Indicator/cci.ts +275 -275
  139. package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
  140. package/src/engine/renderers/Indicator/cmf.ts +137 -137
  141. package/src/engine/renderers/Indicator/dema.ts +136 -136
  142. package/src/engine/renderers/Indicator/donchian.ts +137 -137
  143. package/src/engine/renderers/Indicator/ene.ts +271 -271
  144. package/src/engine/renderers/Indicator/expma.ts +197 -197
  145. package/src/engine/renderers/Indicator/fastk.ts +316 -316
  146. package/src/engine/renderers/Indicator/fib.ts +141 -141
  147. package/src/engine/renderers/Indicator/hma.ts +136 -136
  148. package/src/engine/renderers/Indicator/hv.ts +124 -124
  149. package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
  150. package/src/engine/renderers/Indicator/index.ts +241 -241
  151. package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
  152. package/src/engine/renderers/Indicator/kama.ts +136 -136
  153. package/src/engine/renderers/Indicator/keltner.ts +137 -137
  154. package/src/engine/renderers/Indicator/kst.ts +302 -302
  155. package/src/engine/renderers/Indicator/ma.ts +200 -200
  156. package/src/engine/renderers/Indicator/macd.ts +477 -477
  157. package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
  158. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
  159. package/src/engine/renderers/Indicator/mfi.ts +142 -142
  160. package/src/engine/renderers/Indicator/mom.ts +311 -311
  161. package/src/engine/renderers/Indicator/obv.ts +123 -123
  162. package/src/engine/renderers/Indicator/parkinson.ts +124 -124
  163. package/src/engine/renderers/Indicator/pivot.ts +131 -131
  164. package/src/engine/renderers/Indicator/pvt.ts +123 -123
  165. package/src/engine/renderers/Indicator/roc.ts +143 -143
  166. package/src/engine/renderers/Indicator/rsi.ts +390 -390
  167. package/src/engine/renderers/Indicator/sar.ts +113 -113
  168. package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
  169. package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
  170. package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
  171. package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
  172. package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
  173. package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
  174. package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
  175. package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
  176. package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
  177. package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
  178. package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
  179. package/src/engine/renderers/Indicator/stoch.ts +359 -359
  180. package/src/engine/renderers/Indicator/structure.ts +126 -126
  181. package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
  182. package/src/engine/renderers/Indicator/supertrend.ts +115 -115
  183. package/src/engine/renderers/Indicator/tema.ts +136 -136
  184. package/src/engine/renderers/Indicator/trix.ts +158 -158
  185. package/src/engine/renderers/Indicator/vma.ts +124 -124
  186. package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
  187. package/src/engine/renderers/Indicator/vwap.ts +123 -123
  188. package/src/engine/renderers/Indicator/wma.ts +136 -136
  189. package/src/engine/renderers/Indicator/wmsr.ts +328 -328
  190. package/src/engine/renderers/Indicator/zones.ts +104 -104
  191. package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
  192. package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
  193. package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
  194. package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
  195. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
  196. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
  197. package/src/engine/renderers/candle.ts +459 -459
  198. package/src/engine/renderers/crosshair.ts +69 -69
  199. package/src/engine/renderers/customMarkers.ts +162 -162
  200. package/src/engine/renderers/extremaMarkers.ts +246 -246
  201. package/src/engine/renderers/gridLines.ts +90 -90
  202. package/src/engine/renderers/lastPrice.ts +97 -97
  203. package/src/engine/renderers/paneTitle.ts +136 -136
  204. package/src/engine/renderers/subVolume.ts +236 -236
  205. package/src/engine/renderers/timeAxis.ts +121 -121
  206. package/src/engine/renderers/webgl/candleSurface.ts +955 -955
  207. package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
  208. package/src/engine/renderers/yAxis.ts +105 -105
  209. package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
  210. package/src/engine/scale/logFormula.ts +130 -130
  211. package/src/engine/scale/price.ts +39 -39
  212. package/src/engine/scale/priceScale.ts +264 -264
  213. package/src/engine/subPaneManager.ts +442 -427
  214. package/src/engine/theme/colors.ts +642 -642
  215. package/src/engine/theme/fonts.ts +20 -20
  216. package/src/engine/utils/klineConfig.ts +49 -49
  217. package/src/engine/utils/tickCount.ts +11 -11
  218. package/src/engine/utils/tickPosition.ts +214 -214
  219. package/src/engine/utils/zoom.ts +83 -83
  220. package/src/engine/viewport/viewport.ts +67 -67
  221. package/src/index.ts +3 -3
  222. package/src/plugin/ConfigManager.ts +93 -93
  223. package/src/plugin/EventBus.ts +77 -77
  224. package/src/plugin/HookSystem.ts +106 -106
  225. package/src/plugin/PluginHost.ts +243 -243
  226. package/src/plugin/PluginRegistry.ts +92 -92
  227. package/src/plugin/StateStore.ts +73 -73
  228. package/src/plugin/index.ts +19 -19
  229. package/src/plugin/rendererPluginManager.ts +368 -368
  230. package/src/plugin/stateKeys.ts +8 -8
  231. package/src/plugin/types.ts +526 -526
  232. package/src/reactivity/index.ts +2 -2
  233. package/src/reactivity/signal.ts +119 -119
  234. package/src/semantic/controller.ts +251 -251
  235. package/src/semantic/drawShape.ts +260 -260
  236. package/src/semantic/index.ts +28 -28
  237. package/src/semantic/schema.json +256 -256
  238. package/src/semantic/types.ts +251 -251
  239. package/src/semantic/validator.ts +349 -349
  240. package/src/types/kLine.ts +13 -13
  241. package/src/types/price.ts +56 -56
  242. package/src/types/volumePrice.ts +33 -33
  243. package/src/utils/dateFormat.ts +208 -208
  244. package/src/utils/kLineDraw/axis.ts +562 -562
  245. package/src/utils/priceToY.ts +34 -34
  246. package/src/utils/volumePrice.ts +202 -202
  247. package/src/version.ts +1 -1
@@ -1,246 +1,246 @@
1
- import type { RendererPlugin, RenderContext } from '../../plugin'
2
- import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../plugin'
3
- import type { KLineData } from '../../types/price'
4
- import { roundToPhysicalPixel, alignToPhysicalPixelCenter, createHorizontalLineRect } from '../draw/pixelAlign'
5
- import { getColors, type ThemeColors } from '../theme/colors'
6
- import { getFont, setCanvasFont } from '../theme/fonts'
7
-
8
- const textWidthCache = new Map<string, number>()
9
- const TEXT_WIDTH_CACHE_LIMIT = 256
10
-
11
- // 模块级常量,避免每次重复创建
12
- const PADDING = 4
13
- const LINE_LENGTH = 30
14
- const DOT_RADIUS = 2
15
- const MARKER_FONT = getFont(12)
16
- const TAU = Math.PI * 2
17
-
18
- // Marker 数据接口,用于批量绘制
19
- interface MarkerData {
20
- x: number
21
- y: number
22
- price: number
23
- text: string
24
- textWidth: number
25
- drawLeft: boolean
26
- lineStartX: number
27
- lineEndX: number
28
- endX: number
29
- alignedY: number
30
- textX: number
31
- }
32
-
33
- function measureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
34
- // 使用固定字体,缓存更稳定
35
- const key = MARKER_FONT + '|' + text
36
- const cached = textWidthCache.get(key)
37
- if (cached !== undefined) {
38
- return cached
39
- }
40
-
41
- const savedFont = ctx.font
42
- ctx.font = MARKER_FONT
43
- const width = ctx.measureText(text).width
44
- ctx.font = savedFont
45
-
46
- if (textWidthCache.size >= TEXT_WIDTH_CACHE_LIMIT) {
47
- textWidthCache.clear()
48
- }
49
- textWidthCache.set(key, width)
50
- return width
51
- }
52
-
53
- /**
54
- * 批量绘制所有 marker
55
- * 分三个阶段:线条 → 圆点 → 文字,避免 Canvas 状态频繁切换
56
- */
57
- function drawAllMarkers(
58
- ctx: CanvasRenderingContext2D,
59
- markers: MarkerData[],
60
- dpr: number,
61
- colors: ThemeColors
62
- ) {
63
- if (markers.length === 0) return
64
-
65
- ctx.save()
66
-
67
- // ========== 阶段1:批量绘制所有线条(同一 fillStyle)==========
68
- ctx.fillStyle = colors.TEXT.WEAK
69
- for (const m of markers) {
70
- const lineRect = createHorizontalLineRect(m.lineStartX, m.lineEndX, m.y, dpr)
71
- if (lineRect) {
72
- ctx.fillRect(lineRect.x, lineRect.y, lineRect.width, lineRect.height)
73
- }
74
- }
75
-
76
- // ========== 阶段2:批量绘制所有圆点(复用 fillStyle)==========
77
- ctx.beginPath()
78
- for (const m of markers) {
79
- ctx.moveTo(m.endX + DOT_RADIUS, m.alignedY)
80
- ctx.arc(m.endX, m.alignedY, DOT_RADIUS, 0, TAU)
81
- }
82
- ctx.fill()
83
-
84
- // ========== 阶段3:批量绘制所有文字(同一 font/baseline/fillStyle)==========
85
- setCanvasFont(ctx, MARKER_FONT)
86
- ctx.textBaseline = 'middle'
87
- ctx.fillStyle = colors.PRICE.NEUTRAL
88
-
89
- for (const m of markers) {
90
- ctx.textAlign = m.drawLeft ? 'right' : 'left'
91
- ctx.fillText(m.text, m.textX, m.alignedY)
92
- }
93
-
94
- ctx.restore()
95
- }
96
-
97
- /**
98
- * 创建可视区最高/最低价标注渲染器插件
99
- */
100
- export function createExtremaMarkersRendererPlugin(): RendererPlugin {
101
- return {
102
- name: 'extremaMarkers',
103
- version: '1.0.0',
104
- description: '可视区最高/最低价标注渲染器',
105
- debugName: '极值标记',
106
- paneId: GLOBAL_PANE_ID,
107
- priority: RENDERER_PRIORITY.OVERLAY,
108
-
109
- draw(context: RenderContext) {
110
- const { ctx, pane, data, range, scrollLeft, dpr, paneWidth, kLineCenters } = context
111
- const colors = getColors(context.theme)
112
- const klineData = data as KLineData[]
113
- if (!klineData.length) return
114
- if (pane.role !== 'price') return
115
-
116
- const start = Math.max(0, range.start)
117
- const end = Math.min(klineData.length, range.end)
118
- if (end - start <= 0) return
119
-
120
- let max = -Infinity
121
- let min = Infinity
122
- let maxIndex = start
123
- let minIndex = start
124
-
125
- for (let i = start; i < end; i++) {
126
- const e = klineData[i]
127
- if (!e) continue
128
- if (e.high >= max) {
129
- max = e.high
130
- maxIndex = i
131
- }
132
- if (e.low <= min) {
133
- min = e.low
134
- minIndex = i
135
- }
136
- }
137
-
138
- if (!Number.isFinite(max) || !Number.isFinite(min)) return
139
-
140
- const getCenterX = (i: number) => {
141
- const localIdx = i - range.start
142
- if (localIdx < 0 || localIdx >= kLineCenters.length) return 0
143
- return kLineCenters[localIdx]!
144
- }
145
-
146
- if (!context.yAxisLabels) context.yAxisLabels = []
147
-
148
- context.yAxisLabels.push({
149
- dataIndex: maxIndex,
150
- price: max,
151
- y: pane.yAxis.priceToY(max),
152
- style: {
153
- bgColor: colors.LAST_PRICE_LABEL.BG,
154
- borderColor: colors.PRICE.LAST_PRICE,
155
- textColor: colors.PRICE.LAST_PRICE,
156
- }
157
- })
158
-
159
- context.yAxisLabels.push({
160
- dataIndex: minIndex,
161
- price: min,
162
- y: pane.yAxis.priceToY(min),
163
- style: {
164
- bgColor: colors.LAST_PRICE_LABEL.BG,
165
- borderColor: colors.PRICE.LAST_PRICE,
166
- textColor: colors.PRICE.LAST_PRICE,
167
- }
168
- })
169
-
170
- // 收集所有 marker 数据
171
- const markers: MarkerData[] = []
172
-
173
- const maxMarker = createMarkerData(
174
- getCenterX(maxIndex),
175
- pane.yAxis.priceToY(max),
176
- max,
177
- dpr,
178
- paneWidth,
179
- ctx
180
- )
181
- if (maxMarker) markers.push(maxMarker)
182
-
183
- const minMarker = createMarkerData(
184
- getCenterX(minIndex),
185
- pane.yAxis.priceToY(min),
186
- min,
187
- dpr,
188
- paneWidth,
189
- ctx
190
- )
191
- if (minMarker) markers.push(minMarker)
192
-
193
- // 批量绘制所有 markers
194
- ctx.save()
195
- ctx.translate(-scrollLeft, 0)
196
- drawAllMarkers(ctx, markers, dpr, colors)
197
- ctx.restore()
198
- },
199
- }
200
- }
201
-
202
- /**
203
- * 创建 marker 数据(不绘制,只计算)
204
- */
205
- function createMarkerData(
206
- x: number,
207
- y: number,
208
- price: number,
209
- dpr: number,
210
- paneWidth: number,
211
- ctx: CanvasRenderingContext2D
212
- ): MarkerData | null {
213
- const text = price.toFixed(2)
214
- const textWidth = measureTextWidth(ctx, text)
215
-
216
- const visibleX = x
217
- const rightEdge = visibleX + LINE_LENGTH + PADDING + textWidth
218
- const drawLeft = rightEdge > paneWidth
219
-
220
- let lineStartX = x
221
- let lineEndX = drawLeft ? x - LINE_LENGTH : x + LINE_LENGTH
222
- if (lineStartX > lineEndX) {
223
- ;[lineStartX, lineEndX] = [lineEndX, lineStartX]
224
- }
225
-
226
- const endX = roundToPhysicalPixel(lineEndX, dpr)
227
- const alignedY = alignToPhysicalPixelCenter(y, dpr)
228
- const textX = roundToPhysicalPixel(
229
- drawLeft ? x - LINE_LENGTH - PADDING : x + LINE_LENGTH + PADDING,
230
- dpr
231
- )
232
-
233
- return {
234
- x,
235
- y,
236
- price,
237
- text,
238
- textWidth,
239
- drawLeft,
240
- lineStartX,
241
- lineEndX,
242
- endX,
243
- alignedY,
244
- textX,
245
- }
246
- }
1
+ import type { RendererPlugin, RenderContext } from '../../plugin'
2
+ import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../plugin'
3
+ import type { KLineData } from '../../types/price'
4
+ import { roundToPhysicalPixel, alignToPhysicalPixelCenter, createHorizontalLineRect } from '../draw/pixelAlign'
5
+ import { getColors, type ThemeColors } from '../theme/colors'
6
+ import { getFont, setCanvasFont } from '../theme/fonts'
7
+
8
+ const textWidthCache = new Map<string, number>()
9
+ const TEXT_WIDTH_CACHE_LIMIT = 256
10
+
11
+ // 模块级常量,避免每次重复创建
12
+ const PADDING = 4
13
+ const LINE_LENGTH = 30
14
+ const DOT_RADIUS = 2
15
+ const MARKER_FONT = getFont(12)
16
+ const TAU = Math.PI * 2
17
+
18
+ // Marker 数据接口,用于批量绘制
19
+ interface MarkerData {
20
+ x: number
21
+ y: number
22
+ price: number
23
+ text: string
24
+ textWidth: number
25
+ drawLeft: boolean
26
+ lineStartX: number
27
+ lineEndX: number
28
+ endX: number
29
+ alignedY: number
30
+ textX: number
31
+ }
32
+
33
+ function measureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
34
+ // 使用固定字体,缓存更稳定
35
+ const key = MARKER_FONT + '|' + text
36
+ const cached = textWidthCache.get(key)
37
+ if (cached !== undefined) {
38
+ return cached
39
+ }
40
+
41
+ const savedFont = ctx.font
42
+ ctx.font = MARKER_FONT
43
+ const width = ctx.measureText(text).width
44
+ ctx.font = savedFont
45
+
46
+ if (textWidthCache.size >= TEXT_WIDTH_CACHE_LIMIT) {
47
+ textWidthCache.clear()
48
+ }
49
+ textWidthCache.set(key, width)
50
+ return width
51
+ }
52
+
53
+ /**
54
+ * 批量绘制所有 marker
55
+ * 分三个阶段:线条 → 圆点 → 文字,避免 Canvas 状态频繁切换
56
+ */
57
+ function drawAllMarkers(
58
+ ctx: CanvasRenderingContext2D,
59
+ markers: MarkerData[],
60
+ dpr: number,
61
+ colors: ThemeColors
62
+ ) {
63
+ if (markers.length === 0) return
64
+
65
+ ctx.save()
66
+
67
+ // ========== 阶段1:批量绘制所有线条(同一 fillStyle)==========
68
+ ctx.fillStyle = colors.TEXT.WEAK
69
+ for (const m of markers) {
70
+ const lineRect = createHorizontalLineRect(m.lineStartX, m.lineEndX, m.y, dpr)
71
+ if (lineRect) {
72
+ ctx.fillRect(lineRect.x, lineRect.y, lineRect.width, lineRect.height)
73
+ }
74
+ }
75
+
76
+ // ========== 阶段2:批量绘制所有圆点(复用 fillStyle)==========
77
+ ctx.beginPath()
78
+ for (const m of markers) {
79
+ ctx.moveTo(m.endX + DOT_RADIUS, m.alignedY)
80
+ ctx.arc(m.endX, m.alignedY, DOT_RADIUS, 0, TAU)
81
+ }
82
+ ctx.fill()
83
+
84
+ // ========== 阶段3:批量绘制所有文字(同一 font/baseline/fillStyle)==========
85
+ setCanvasFont(ctx, MARKER_FONT)
86
+ ctx.textBaseline = 'middle'
87
+ ctx.fillStyle = colors.PRICE.NEUTRAL
88
+
89
+ for (const m of markers) {
90
+ ctx.textAlign = m.drawLeft ? 'right' : 'left'
91
+ ctx.fillText(m.text, m.textX, m.alignedY)
92
+ }
93
+
94
+ ctx.restore()
95
+ }
96
+
97
+ /**
98
+ * 创建可视区最高/最低价标注渲染器插件
99
+ */
100
+ export function createExtremaMarkersRendererPlugin(): RendererPlugin {
101
+ return {
102
+ name: 'extremaMarkers',
103
+ version: '1.0.0',
104
+ description: '可视区最高/最低价标注渲染器',
105
+ debugName: '极值标记',
106
+ paneId: GLOBAL_PANE_ID,
107
+ priority: RENDERER_PRIORITY.OVERLAY,
108
+
109
+ draw(context: RenderContext) {
110
+ const { ctx, pane, data, range, scrollLeft, dpr, paneWidth, kLineCenters } = context
111
+ const colors = getColors(context.theme)
112
+ const klineData = data as KLineData[]
113
+ if (!klineData.length) return
114
+ if (pane.role !== 'price') return
115
+
116
+ const start = Math.max(0, range.start)
117
+ const end = Math.min(klineData.length, range.end)
118
+ if (end - start <= 0) return
119
+
120
+ let max = -Infinity
121
+ let min = Infinity
122
+ let maxIndex = start
123
+ let minIndex = start
124
+
125
+ for (let i = start; i < end; i++) {
126
+ const e = klineData[i]
127
+ if (!e) continue
128
+ if (e.high >= max) {
129
+ max = e.high
130
+ maxIndex = i
131
+ }
132
+ if (e.low <= min) {
133
+ min = e.low
134
+ minIndex = i
135
+ }
136
+ }
137
+
138
+ if (!Number.isFinite(max) || !Number.isFinite(min)) return
139
+
140
+ const getCenterX = (i: number) => {
141
+ const localIdx = i - range.start
142
+ if (localIdx < 0 || localIdx >= kLineCenters.length) return 0
143
+ return kLineCenters[localIdx]!
144
+ }
145
+
146
+ if (!context.yAxisLabels) context.yAxisLabels = []
147
+
148
+ context.yAxisLabels.push({
149
+ dataIndex: maxIndex,
150
+ price: max,
151
+ y: pane.yAxis.priceToY(max),
152
+ style: {
153
+ bgColor: colors.LAST_PRICE_LABEL.BG,
154
+ borderColor: colors.PRICE.LAST_PRICE,
155
+ textColor: colors.PRICE.LAST_PRICE,
156
+ }
157
+ })
158
+
159
+ context.yAxisLabels.push({
160
+ dataIndex: minIndex,
161
+ price: min,
162
+ y: pane.yAxis.priceToY(min),
163
+ style: {
164
+ bgColor: colors.LAST_PRICE_LABEL.BG,
165
+ borderColor: colors.PRICE.LAST_PRICE,
166
+ textColor: colors.PRICE.LAST_PRICE,
167
+ }
168
+ })
169
+
170
+ // 收集所有 marker 数据
171
+ const markers: MarkerData[] = []
172
+
173
+ const maxMarker = createMarkerData(
174
+ getCenterX(maxIndex),
175
+ pane.yAxis.priceToY(max),
176
+ max,
177
+ dpr,
178
+ paneWidth,
179
+ ctx
180
+ )
181
+ if (maxMarker) markers.push(maxMarker)
182
+
183
+ const minMarker = createMarkerData(
184
+ getCenterX(minIndex),
185
+ pane.yAxis.priceToY(min),
186
+ min,
187
+ dpr,
188
+ paneWidth,
189
+ ctx
190
+ )
191
+ if (minMarker) markers.push(minMarker)
192
+
193
+ // 批量绘制所有 markers
194
+ ctx.save()
195
+ ctx.translate(-scrollLeft, 0)
196
+ drawAllMarkers(ctx, markers, dpr, colors)
197
+ ctx.restore()
198
+ },
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 创建 marker 数据(不绘制,只计算)
204
+ */
205
+ function createMarkerData(
206
+ x: number,
207
+ y: number,
208
+ price: number,
209
+ dpr: number,
210
+ paneWidth: number,
211
+ ctx: CanvasRenderingContext2D
212
+ ): MarkerData | null {
213
+ const text = price.toFixed(2)
214
+ const textWidth = measureTextWidth(ctx, text)
215
+
216
+ const visibleX = x
217
+ const rightEdge = visibleX + LINE_LENGTH + PADDING + textWidth
218
+ const drawLeft = rightEdge > paneWidth
219
+
220
+ let lineStartX = x
221
+ let lineEndX = drawLeft ? x - LINE_LENGTH : x + LINE_LENGTH
222
+ if (lineStartX > lineEndX) {
223
+ ;[lineStartX, lineEndX] = [lineEndX, lineStartX]
224
+ }
225
+
226
+ const endX = roundToPhysicalPixel(lineEndX, dpr)
227
+ const alignedY = alignToPhysicalPixelCenter(y, dpr)
228
+ const textX = roundToPhysicalPixel(
229
+ drawLeft ? x - LINE_LENGTH - PADDING : x + LINE_LENGTH + PADDING,
230
+ dpr
231
+ )
232
+
233
+ return {
234
+ x,
235
+ y,
236
+ price,
237
+ text,
238
+ textWidth,
239
+ drawLeft,
240
+ lineStartX,
241
+ lineEndX,
242
+ endX,
243
+ alignedY,
244
+ textX,
245
+ }
246
+ }
@@ -1,90 +1,90 @@
1
- import type { RendererPlugin, RenderContext } from '../../plugin'
2
- import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../plugin'
3
- import type { KLineData } from '../../types/price'
4
- import { createHorizontalLineRect, createVerticalLineRect } from '../draw/pixelAlign'
5
- import { findMonthBoundaries } from '../../utils/dateFormat'
6
- import { getColors } from '../theme/colors'
7
- import { calculateTickPositions, calculateValueTickPositions, type ScaleType } from '../utils/tickPosition'
8
-
9
- /**
10
- * 创建网格线渲染器插件
11
- * 横向按像素均分铺满整个绘图区高度,纵向按月分割(使用预计算的月边界,网格线对齐到K线实体中部)
12
- * 渲染到所有 pane(使用 GLOBAL_PANE_ID)
13
- */
14
- export function createGridLinesRendererPlugin(): RendererPlugin {
15
- return {
16
- name: 'gridLines',
17
- version: '1.0.0',
18
- description: '网格线渲染器',
19
- debugName: '网格线',
20
- paneId: GLOBAL_PANE_ID,
21
- priority: RENDERER_PRIORITY.GRID,
22
-
23
- draw(context: RenderContext) {
24
- const { ctx, pane, data, range, scrollLeft, kWidth, dpr, kLinePositions, settings } = context
25
- const colors = getColors(context.theme)
26
- const klineData = data as KLineData[]
27
- if (!klineData.length) return
28
- if (settings?.showGridLines === false) return
29
-
30
- ctx.save()
31
- ctx.fillStyle = colors.GRID.HORIZONTAL
32
- ctx.translate(-scrollLeft, 0)
33
-
34
- const plotWidth = ctx.canvas.width / dpr
35
- const startX = scrollLeft
36
- const endX = scrollLeft + plotWidth
37
- const pt = pane.yAxis.getPaddingTop()
38
- const pb = pane.yAxis.getPaddingBottom()
39
-
40
- // 水平网格线:与 Y 轴刻度对齐
41
- const scaleType = pane.yAxis.getScaleType()
42
- let yPositions: number[]
43
-
44
- if (scaleType === 'log' && pane.role === 'price') {
45
- // 对数模式:生成 nice 刻度值,用 priceToY 计算 Y 位置
46
- const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
47
- const tickValues = calculateValueTickPositions({
48
- height: pane.height,
49
- paddingTop: pt,
50
- paddingBottom: pb,
51
- isMain: true,
52
- valueMin: displayRange.minPrice,
53
- valueMax: displayRange.maxPrice,
54
- scaleType: 'log',
55
- })
56
- yPositions = tickValues.map(t => t.y)
57
- } else {
58
- // 线性模式:均匀分布
59
- const tickPositions = calculateTickPositions({
60
- height: pane.height,
61
- paddingTop: pt,
62
- paddingBottom: pb,
63
- isMain: pane.role === 'price',
64
- })
65
- yPositions = tickPositions.map(t => t.y)
66
- }
67
-
68
- for (const y of yPositions) {
69
- const h = createHorizontalLineRect(startX, endX, y, dpr)
70
- if (h) ctx.fillRect(h.x, h.y, h.width, h.height)
71
- }
72
-
73
- const boundaries = findMonthBoundaries(klineData)
74
-
75
- for (const idx of boundaries) {
76
- if (idx < range.start || idx >= range.end || idx >= klineData.length) continue
77
-
78
- // 使用统一的 kLinePositions 计算 K 线中心 X 坐标
79
- const localIdx = idx - range.start
80
- if (localIdx < 0 || localIdx >= kLinePositions.length) continue
81
- const worldX = kLinePositions[localIdx]! + kWidth / 2
82
-
83
- const v = createVerticalLineRect(worldX, 0, pane.height, dpr)
84
- if (v) ctx.fillRect(v.x, v.y, v.width, v.height)
85
- }
86
-
87
- ctx.restore()
88
- },
89
- }
90
- }
1
+ import type { RendererPlugin, RenderContext } from '../../plugin'
2
+ import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../plugin'
3
+ import type { KLineData } from '../../types/price'
4
+ import { createHorizontalLineRect, createVerticalLineRect } from '../draw/pixelAlign'
5
+ import { findMonthBoundaries } from '../../utils/dateFormat'
6
+ import { getColors } from '../theme/colors'
7
+ import { calculateTickPositions, calculateValueTickPositions, type ScaleType } from '../utils/tickPosition'
8
+
9
+ /**
10
+ * 创建网格线渲染器插件
11
+ * 横向按像素均分铺满整个绘图区高度,纵向按月分割(使用预计算的月边界,网格线对齐到K线实体中部)
12
+ * 渲染到所有 pane(使用 GLOBAL_PANE_ID)
13
+ */
14
+ export function createGridLinesRendererPlugin(): RendererPlugin {
15
+ return {
16
+ name: 'gridLines',
17
+ version: '1.0.0',
18
+ description: '网格线渲染器',
19
+ debugName: '网格线',
20
+ paneId: GLOBAL_PANE_ID,
21
+ priority: RENDERER_PRIORITY.GRID,
22
+
23
+ draw(context: RenderContext) {
24
+ const { ctx, pane, data, range, scrollLeft, kWidth, dpr, kLinePositions, settings } = context
25
+ const colors = getColors(context.theme)
26
+ const klineData = data as KLineData[]
27
+ if (!klineData.length) return
28
+ if (settings?.showGridLines === false) return
29
+
30
+ ctx.save()
31
+ ctx.fillStyle = colors.GRID.HORIZONTAL
32
+ ctx.translate(-scrollLeft, 0)
33
+
34
+ const plotWidth = ctx.canvas.width / dpr
35
+ const startX = scrollLeft
36
+ const endX = scrollLeft + plotWidth
37
+ const pt = pane.yAxis.getPaddingTop()
38
+ const pb = pane.yAxis.getPaddingBottom()
39
+
40
+ // 水平网格线:与 Y 轴刻度对齐
41
+ const scaleType = pane.yAxis.getScaleType()
42
+ let yPositions: number[]
43
+
44
+ if (scaleType === 'log' && pane.role === 'price') {
45
+ // 对数模式:生成 nice 刻度值,用 priceToY 计算 Y 位置
46
+ const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
47
+ const tickValues = calculateValueTickPositions({
48
+ height: pane.height,
49
+ paddingTop: pt,
50
+ paddingBottom: pb,
51
+ isMain: true,
52
+ valueMin: displayRange.minPrice,
53
+ valueMax: displayRange.maxPrice,
54
+ scaleType: 'log',
55
+ })
56
+ yPositions = tickValues.map(t => t.y)
57
+ } else {
58
+ // 线性模式:均匀分布
59
+ const tickPositions = calculateTickPositions({
60
+ height: pane.height,
61
+ paddingTop: pt,
62
+ paddingBottom: pb,
63
+ isMain: pane.role === 'price',
64
+ })
65
+ yPositions = tickPositions.map(t => t.y)
66
+ }
67
+
68
+ for (const y of yPositions) {
69
+ const h = createHorizontalLineRect(startX, endX, y, dpr)
70
+ if (h) ctx.fillRect(h.x, h.y, h.width, h.height)
71
+ }
72
+
73
+ const boundaries = findMonthBoundaries(klineData)
74
+
75
+ for (const idx of boundaries) {
76
+ if (idx < range.start || idx >= range.end || idx >= klineData.length) continue
77
+
78
+ // 使用统一的 kLinePositions 计算 K 线中心 X 坐标
79
+ const localIdx = idx - range.start
80
+ if (localIdx < 0 || localIdx >= kLinePositions.length) continue
81
+ const worldX = kLinePositions[localIdx]! + kWidth / 2
82
+
83
+ const v = createVerticalLineRect(worldX, 0, pane.height, dpr)
84
+ if (v) ctx.fillRect(v.x, v.y, v.width, v.height)
85
+ }
86
+
87
+ ctx.restore()
88
+ },
89
+ }
90
+ }