@363045841yyt/klinechart-core 0.9.0-alpha.2 → 0.9.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +9 -8
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/index.d.ts +1 -0
  5. package/dist/controllers/index.d.ts.map +1 -1
  6. package/dist/controllers/index.js.map +1 -1
  7. package/dist/controllers/types.d.ts +5 -0
  8. package/dist/controllers/types.d.ts.map +1 -1
  9. package/dist/data/binance.d.ts +1 -1
  10. package/dist/data/binance.d.ts.map +1 -1
  11. package/dist/data/binance.js +1 -1
  12. package/dist/data/binance.js.map +1 -1
  13. package/dist/engine/chart.d.ts +9 -9
  14. package/dist/engine/chart.d.ts.map +1 -1
  15. package/dist/engine/chart.js +69 -96
  16. package/dist/engine/chart.js.map +1 -1
  17. package/dist/engine/chartTypes.d.ts +5 -0
  18. package/dist/engine/chartTypes.d.ts.map +1 -1
  19. package/dist/engine/controller/interaction.d.ts +4 -2
  20. package/dist/engine/controller/interaction.d.ts.map +1 -1
  21. package/dist/engine/controller/interaction.js +18 -10
  22. package/dist/engine/controller/interaction.js.map +1 -1
  23. package/dist/engine/data/chartDataManager.d.ts +12 -18
  24. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  25. package/dist/engine/data/chartDataManager.js +46 -37
  26. package/dist/engine/data/chartDataManager.js.map +1 -1
  27. package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
  28. package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
  29. package/dist/engine/data/incrementalLoadHint.js +5 -2
  30. package/dist/engine/data/incrementalLoadHint.js.map +1 -1
  31. package/dist/engine/data/scrollCompensator.d.ts +3 -13
  32. package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
  33. package/dist/engine/data/scrollCompensator.js +13 -14
  34. package/dist/engine/data/scrollCompensator.js.map +1 -1
  35. package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
  36. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
  37. package/dist/engine/indicators/chartIndicatorManager.js +37 -37
  38. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
  39. package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
  40. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
  41. package/dist/engine/layout/chartPaneLayout.js +58 -16
  42. package/dist/engine/layout/chartPaneLayout.js.map +1 -1
  43. package/dist/engine/paneRenderer.d.ts +2 -0
  44. package/dist/engine/paneRenderer.d.ts.map +1 -1
  45. package/dist/engine/paneRenderer.js +15 -5
  46. package/dist/engine/paneRenderer.js.map +1 -1
  47. package/dist/engine/render/chartRenderer.d.ts +28 -22
  48. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  49. package/dist/engine/render/chartRenderer.js +117 -69
  50. package/dist/engine/render/chartRenderer.js.map +1 -1
  51. package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
  52. package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
  53. package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
  54. package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
  55. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
  56. package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
  57. package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
  58. package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
  59. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
  60. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
  61. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
  62. package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
  63. package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
  64. package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
  65. package/dist/engine/render/layers/yAxisLayer.js +11 -2
  66. package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
  67. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
  68. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  69. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
  70. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  71. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +72 -0
  72. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
  73. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +177 -0
  74. package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
  75. package/dist/engine/renderers/leftYAxis.d.ts +15 -2
  76. package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
  77. package/dist/engine/renderers/leftYAxis.js +56 -16
  78. package/dist/engine/renderers/leftYAxis.js.map +1 -1
  79. package/dist/engine/renderers/yAxis.d.ts +16 -6
  80. package/dist/engine/renderers/yAxis.d.ts.map +1 -1
  81. package/dist/engine/renderers/yAxis.js +49 -16
  82. package/dist/engine/renderers/yAxis.js.map +1 -1
  83. package/dist/engine/state/viewportState.d.ts +4 -1
  84. package/dist/engine/state/viewportState.d.ts.map +1 -1
  85. package/dist/engine/state/viewportState.js +40 -8
  86. package/dist/engine/state/viewportState.js.map +1 -1
  87. package/dist/engine/subPaneManager.d.ts +5 -3
  88. package/dist/engine/subPaneManager.d.ts.map +1 -1
  89. package/dist/engine/subPaneManager.js +3 -2
  90. package/dist/engine/subPaneManager.js.map +1 -1
  91. package/dist/engine/utils/chartZoomController.d.ts +9 -7
  92. package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
  93. package/dist/engine/utils/chartZoomController.js +13 -12
  94. package/dist/engine/utils/chartZoomController.js.map +1 -1
  95. package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
  96. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
  97. package/dist/engine/viewport/chartViewportManager.js +7 -48
  98. package/dist/engine/viewport/chartViewportManager.js.map +1 -1
  99. package/dist/engine/viewport/viewport.d.ts +14 -0
  100. package/dist/engine/viewport/viewport.d.ts.map +1 -1
  101. package/dist/engine/viewport/viewport.js +10 -0
  102. package/dist/engine/viewport/viewport.js.map +1 -1
  103. package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
  104. package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
  105. package/dist/foundation/plugin/types.d.ts +3 -0
  106. package/dist/foundation/plugin/types.d.ts.map +1 -1
  107. package/dist/foundation/plugin/types.js.map +1 -1
  108. package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
  109. package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
  110. package/dist/foundation/reactivity/frameTransaction.js +17 -2
  111. package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
  112. package/dist/rendering/render/Renderer.d.ts +58 -74
  113. package/dist/rendering/render/Renderer.d.ts.map +1 -1
  114. package/dist/rendering/render/Renderer.js +8 -18
  115. package/dist/rendering/render/Renderer.js.map +1 -1
  116. package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
  117. package/dist/rendering/render/createWebGPURenderer.js +30 -17
  118. package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
  119. package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
  120. package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
  121. package/dist/rendering/render/physicalLine.d.ts +4 -0
  122. package/dist/rendering/render/physicalLine.d.ts.map +1 -0
  123. package/dist/rendering/render/physicalLine.js +37 -0
  124. package/dist/rendering/render/physicalLine.js.map +1 -0
  125. package/dist/rendering/render/physicalRegion.d.ts +14 -0
  126. package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
  127. package/dist/rendering/render/physicalRegion.js +15 -0
  128. package/dist/rendering/render/physicalRegion.js.map +1 -0
  129. package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
  130. package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
  131. package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
  132. package/dist/rendering/scene/createScene.d.ts.map +1 -1
  133. package/dist/rendering/scene/createScene.js +4 -3
  134. package/dist/rendering/scene/createScene.js.map +1 -1
  135. package/dist/version.d.ts +1 -1
  136. package/dist/version.js +1 -1
  137. package/package.json +1 -1
  138. package/src/__tests__/stateKernel.test.ts +1 -0
  139. package/src/controllers/createChartController.ts +9 -8
  140. package/src/controllers/index.ts +11 -0
  141. package/src/controllers/types.ts +7 -0
  142. package/src/data/binance.ts +1 -1
  143. package/src/engine/__tests__/chart.dpr.test.ts +3 -2
  144. package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
  145. package/src/engine/__tests__/subPaneManager.test.ts +5 -3
  146. package/src/engine/chart.ts +76 -109
  147. package/src/engine/chartTypes.ts +5 -0
  148. package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
  149. package/src/engine/controller/interaction.ts +18 -10
  150. package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
  151. package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
  152. package/src/engine/data/chartDataManager.ts +54 -59
  153. package/src/engine/data/incrementalLoadHint.ts +8 -7
  154. package/src/engine/data/scrollCompensator.ts +16 -24
  155. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
  156. package/src/engine/indicators/chartIndicatorManager.ts +62 -62
  157. package/src/engine/layout/chartPaneLayout.ts +73 -21
  158. package/src/engine/paneRenderer.ts +17 -10
  159. package/src/engine/render/chartRenderer.ts +187 -129
  160. package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
  161. package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
  162. package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
  163. package/src/engine/render/layers/yAxisLayer.ts +29 -7
  164. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
  165. package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +282 -0
  166. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
  167. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
  168. package/src/engine/renderers/leftYAxis.ts +60 -18
  169. package/src/engine/renderers/yAxis.ts +57 -22
  170. package/src/engine/state/viewportState.ts +44 -8
  171. package/src/engine/subPaneManager.ts +8 -5
  172. package/src/engine/utils/chartZoomController.ts +22 -19
  173. package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
  174. package/src/engine/viewport/chartViewportManager.ts +9 -60
  175. package/src/engine/viewport/viewport.ts +14 -0
  176. package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
  177. package/src/foundation/plugin/types.ts +3 -0
  178. package/src/foundation/reactivity/frameTransaction.ts +20 -6
  179. package/src/rendering/render/Renderer.ts +69 -85
  180. package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
  181. package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
  182. package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
  183. package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
  184. package/src/rendering/render/createWebGPURenderer.ts +30 -17
  185. package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
  186. package/src/rendering/render/physicalLine.ts +45 -0
  187. package/src/rendering/render/physicalRegion.ts +32 -0
  188. package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
  189. package/src/rendering/scene/createScene.ts +4 -3
  190. package/src/version.ts +1 -1
@@ -4,11 +4,14 @@ import type {
4
4
  RenderContext,
5
5
  } from '../../../foundation/plugin/index'
6
6
  import { RENDERER_PRIORITY } from '../../../foundation/plugin/index'
7
- import { resolveThemeColors } from '../../../foundation/tokens/index'
8
- import type { KLineData, TimeShareData } from '../../../foundation/types/price'
9
- import type { IndicatorScheduler } from '../../indicators/scheduler'
10
7
  import { getFont, setCanvasFont } from '../../../foundation/tokens/fonts'
11
8
 
9
+ import {
10
+ buildLegendTemplateContext,
11
+ type LegendRenderMode,
12
+ type LegendTemplateContext,
13
+ } from './mainIndicatorLegendContext'
14
+
12
15
  const textWidthCache = new Map<string, number>()
13
16
  const TEXT_WIDTH_CACHE_LIMIT = 512
14
17
 
@@ -30,6 +33,14 @@ function measureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
30
33
  /** 渲染器配置 */
31
34
  interface MainIndicatorLegendConfig {
32
35
  yPaddingPx: number
36
+ /** canvas 默认绘制;external 仅发布上下文 */
37
+ renderMode: LegendRenderMode
38
+ }
39
+
40
+ export type MainIndicatorLegendOptions = {
41
+ yPaddingPx: number
42
+ /** 每帧构建后的图例上下文回调(canvas / external 均触发) */
43
+ onContext?: (ctx: LegendTemplateContext | null) => void
33
44
  }
34
45
 
35
46
  /**
@@ -38,18 +49,20 @@ interface MainIndicatorLegendConfig {
38
49
  * 统一管理 MA、BOLL 等主图指标的图例显示,支持多行排列
39
50
  * MA 数据从 StateStore 读取(与 MA 线渲染器共享同一数据源)
40
51
  */
41
- export function createMainIndicatorLegendRendererPlugin(options: {
42
- yPaddingPx: number
43
- }): RendererPluginWithHost {
52
+ export function createMainIndicatorLegendRendererPlugin(
53
+ options: MainIndicatorLegendOptions,
54
+ ): RendererPluginWithHost {
44
55
  const config: MainIndicatorLegendConfig = {
45
56
  yPaddingPx: options.yPaddingPx,
57
+ renderMode: 'canvas',
46
58
  }
59
+ const onContext = options.onContext
47
60
 
48
61
  let pluginHost: PluginHost | null = null
49
62
 
50
63
  return {
51
64
  name: 'mainIndicatorLegend',
52
- version: '2.1.0',
65
+ version: '2.2.0',
53
66
  description: '主图指标图例渲染器(MA 数据来自 StateStore)',
54
67
  debugName: '主图指标图例',
55
68
  paneId: 'main',
@@ -66,316 +79,23 @@ export function createMainIndicatorLegendRendererPlugin(options: {
66
79
  },
67
80
 
68
81
  draw(context: RenderContext) {
69
- const { overlayCtx, data, range, crosshairIndex } = context
70
- const klineData = data as KLineData[]
71
- const colors = resolveThemeColors(
72
- context.theme,
73
- context.isAsiaMarket,
74
- context.colorPresetSettings,
75
- )
76
- if (!klineData.length || !overlayCtx) return
77
-
78
- const fontSize = 12
79
- const lineHeight = fontSize + 6
80
- const legendX = 12
81
- const gap = 10
82
- const legendYOffset = 6
83
-
84
- overlayCtx.save()
85
- setCanvasFont(overlayCtx, getFont(fontSize))
86
- overlayCtx.textAlign = 'left'
87
- overlayCtx.textBaseline = 'top'
88
-
89
- const targetIndex = crosshairIndex ?? Math.min(range.end - 1, klineData.length - 1)
90
- const rows: Array<{ draw: (rowIndex: number) => void }> = []
91
-
92
- // ── Timeshare legend ──
93
- if (context.period === 'timeshare') {
94
- const tsData = data as TimeShareData[]
95
- const preClose = (context.settings?.preClose as number) ?? tsData[0]?.price ?? 0
96
- const item = tsData[targetIndex]
97
- if (item) {
98
- const changeAmount = item.price - preClose
99
- const changePercent = preClose !== 0 ? (changeAmount / preClose) * 100 : 0
100
- const changeColor = changeAmount >= 0 ? colors.candleUpBody : colors.candleDownBody
101
-
102
- if (context.paneWidth >= 400) {
103
- rows.push({
104
- draw: (rowIndex: number) => {
105
- let x = legendX
106
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
107
-
108
- overlayCtx.fillStyle = colors.text.primary
109
- overlayCtx.fillText('现价 ', x, y)
110
- x += measureTextWidth(overlayCtx, '现价 ')
111
- overlayCtx.fillStyle = changeColor
112
- overlayCtx.fillText(item.price.toFixed(2), x, y)
113
- x += measureTextWidth(overlayCtx, item.price.toFixed(2)) + gap
114
-
115
- overlayCtx.fillStyle = colors.text.primary
116
- overlayCtx.fillText('均价 ', x, y)
117
- x += measureTextWidth(overlayCtx, '均价 ')
118
- overlayCtx.fillText(item.average.toFixed(2), x, y)
119
- x += measureTextWidth(overlayCtx, item.average.toFixed(2)) + gap
120
-
121
- overlayCtx.fillStyle = colors.text.primary
122
- overlayCtx.fillText('涨跌 ', x, y)
123
- x += measureTextWidth(overlayCtx, '涨跌 ')
124
- overlayCtx.fillStyle = changeColor
125
- const sign = changeAmount > 0 ? '+' : ''
126
- overlayCtx.fillText(`${sign}${changeAmount.toFixed(2)}`, x, y)
127
- x += measureTextWidth(overlayCtx, `${sign}${changeAmount.toFixed(2)}`) + gap
128
-
129
- overlayCtx.fillStyle = colors.text.primary
130
- overlayCtx.fillText('涨幅 ', x, y)
131
- x += measureTextWidth(overlayCtx, '涨幅 ')
132
- overlayCtx.fillStyle = changeColor
133
- const pctSign = changePercent > 0 ? '+' : ''
134
- overlayCtx.fillText(`${pctSign}${changePercent.toFixed(2)}%`, x, y)
135
- x += measureTextWidth(overlayCtx, `${pctSign}${changePercent.toFixed(2)}%`) + gap
136
-
137
- const volText = formatVolumeShort(item.volume)
138
- overlayCtx.fillStyle = colors.text.tertiary
139
- overlayCtx.fillText('成交量 ', x, y)
140
- x += measureTextWidth(overlayCtx, '成交量 ')
141
- overlayCtx.fillStyle = colors.text.primary
142
- overlayCtx.fillText(volText, x, y)
143
- x += measureTextWidth(overlayCtx, volText) + gap
144
-
145
- const amtText = formatAmountShort(item.amount)
146
- overlayCtx.fillStyle = colors.text.tertiary
147
- overlayCtx.fillText('成交额 ', x, y)
148
- x += measureTextWidth(overlayCtx, '成交额 ')
149
- overlayCtx.fillStyle = colors.text.primary
150
- overlayCtx.fillText(amtText, x, y)
151
- },
152
- })
153
- } else {
154
- rows.push({
155
- draw: (rowIndex: number) => {
156
- let x = legendX
157
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
158
-
159
- overlayCtx.fillStyle = colors.text.primary
160
- overlayCtx.fillText('现价 ', x, y)
161
- x += measureTextWidth(overlayCtx, '现价 ')
162
- overlayCtx.fillStyle = changeColor
163
- overlayCtx.fillText(item.price.toFixed(2), x, y)
164
- x += measureTextWidth(overlayCtx, item.price.toFixed(2)) + gap
165
-
166
- overlayCtx.fillStyle = colors.text.primary
167
- overlayCtx.fillText('均价 ', x, y)
168
- x += measureTextWidth(overlayCtx, '均价 ')
169
- overlayCtx.fillText(item.average.toFixed(2), x, y)
170
- x += measureTextWidth(overlayCtx, item.average.toFixed(2)) + gap
171
-
172
- overlayCtx.fillStyle = colors.text.tertiary
173
- overlayCtx.fillText('成交量 ', x, y)
174
- x += measureTextWidth(overlayCtx, '成交量 ')
175
- overlayCtx.fillStyle = colors.text.primary
176
- overlayCtx.fillText(formatVolumeShort(item.volume), x, y)
177
- },
178
- })
179
- rows.push({
180
- draw: (rowIndex: number) => {
181
- let x = legendX
182
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
183
-
184
- overlayCtx.fillStyle = colors.text.primary
185
- overlayCtx.fillText('涨跌 ', x, y)
186
- x += measureTextWidth(overlayCtx, '涨跌 ')
187
- overlayCtx.fillStyle = changeColor
188
- const sign = changeAmount > 0 ? '+' : ''
189
- overlayCtx.fillText(`${sign}${changeAmount.toFixed(2)}`, x, y)
190
- x += measureTextWidth(overlayCtx, `${sign}${changeAmount.toFixed(2)}`) + gap
191
-
192
- overlayCtx.fillStyle = colors.text.primary
193
- overlayCtx.fillText('涨幅 ', x, y)
194
- x += measureTextWidth(overlayCtx, '涨幅 ')
195
- overlayCtx.fillStyle = changeColor
196
- const pctSign = changePercent > 0 ? '+' : ''
197
- overlayCtx.fillText(`${pctSign}${changePercent.toFixed(2)}%`, x, y)
198
- x += measureTextWidth(overlayCtx, `${pctSign}${changePercent.toFixed(2)}%`) + gap
199
-
200
- overlayCtx.fillStyle = colors.text.tertiary
201
- overlayCtx.fillText('成交额 ', x, y)
202
- x += measureTextWidth(overlayCtx, '成交额 ')
203
- overlayCtx.fillStyle = colors.text.primary
204
- overlayCtx.fillText(formatAmountShort(item.amount), x, y)
205
- },
206
- })
207
- }
208
- }
209
- }
210
-
211
- if (typeof crosshairIndex === 'number') {
212
- const k = klineData[targetIndex]
213
- if (k && typeof k.close === 'number') {
214
- const isUp = k.close >= k.open
215
- const volText = typeof k.volume === 'number' ? formatVolumeShort(k.volume) : null
216
- const upColor = isUp ? colors.candleUpBody : colors.candleDownBody
217
-
218
- if (context.paneWidth >= 400) {
219
- rows.push({
220
- draw: (rowIndex: number) => {
221
- let x = legendX
222
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
223
-
224
- overlayCtx.fillStyle = colors.text.primary
225
- overlayCtx.fillText('O ', x, y)
226
- x += measureTextWidth(overlayCtx, 'O ')
227
- overlayCtx.fillStyle = upColor
228
- overlayCtx.fillText(k.open.toFixed(2), x, y)
229
- x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
230
-
231
- overlayCtx.fillStyle = colors.text.primary
232
- overlayCtx.fillText('H ', x, y)
233
- x += measureTextWidth(overlayCtx, 'H ')
234
- overlayCtx.fillText(k.high.toFixed(2), x, y)
235
- x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
236
-
237
- overlayCtx.fillText('L ', x, y)
238
- x += measureTextWidth(overlayCtx, 'L ')
239
- overlayCtx.fillText(k.low.toFixed(2), x, y)
240
- x += measureTextWidth(overlayCtx, k.low.toFixed(2)) + gap
241
-
242
- overlayCtx.fillStyle = colors.text.primary
243
- overlayCtx.fillText('C ', x, y)
244
- x += measureTextWidth(overlayCtx, 'C ')
245
- overlayCtx.fillStyle = upColor
246
- overlayCtx.fillText(k.close.toFixed(2), x, y)
247
- x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
248
-
249
- if (volText) {
250
- overlayCtx.fillStyle = colors.text.tertiary
251
- overlayCtx.fillText('Vol ', x, y)
252
- x += measureTextWidth(overlayCtx, 'Vol ')
253
- overlayCtx.fillStyle = colors.text.primary
254
- overlayCtx.fillText(volText, x, y)
255
- }
256
- },
257
- })
258
- } else {
259
- rows.push({
260
- draw: (rowIndex: number) => {
261
- let x = legendX
262
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
263
-
264
- overlayCtx.fillStyle = colors.text.primary
265
- overlayCtx.fillText('O ', x, y)
266
- x += measureTextWidth(overlayCtx, 'O ')
267
- overlayCtx.fillStyle = upColor
268
- overlayCtx.fillText(k.open.toFixed(2), x, y)
269
- x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
270
-
271
- overlayCtx.fillStyle = colors.text.primary
272
- overlayCtx.fillText('H ', x, y)
273
- x += measureTextWidth(overlayCtx, 'H ')
274
- overlayCtx.fillText(k.high.toFixed(2), x, y)
275
- x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
276
-
277
- overlayCtx.fillText('L ', x, y)
278
- x += measureTextWidth(overlayCtx, 'L ')
279
- overlayCtx.fillText(k.low.toFixed(2), x, y)
280
- },
281
- })
282
- rows.push({
283
- draw: (rowIndex: number) => {
284
- let x = legendX
285
- const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
286
-
287
- overlayCtx.fillStyle = colors.text.primary
288
- overlayCtx.fillText('C ', x, y)
289
- x += measureTextWidth(overlayCtx, 'C ')
290
- overlayCtx.fillStyle = upColor
291
- overlayCtx.fillText(k.close.toFixed(2), x, y)
292
- x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
293
-
294
- if (volText) {
295
- overlayCtx.fillStyle = colors.text.tertiary
296
- overlayCtx.fillText('Vol ', x, y)
297
- x += measureTextWidth(overlayCtx, 'Vol ')
298
- overlayCtx.fillStyle = colors.text.primary
299
- overlayCtx.fillText(volText, x, y)
300
- }
301
- },
302
- })
303
- }
304
- }
305
- }
82
+ const legend = buildLegendTemplateContext({
83
+ context,
84
+ host: pluginHost,
85
+ yPaddingPx: config.yPaddingPx,
86
+ })
87
+ onContext?.(legend)
306
88
 
307
- const scheduler =
308
- pluginHost && typeof pluginHost.getService === 'function'
309
- ? pluginHost.getService<IndicatorScheduler>('indicatorScheduler')
310
- : undefined
311
-
312
- const mainIndicators = scheduler?.getMainIndicators() ?? []
313
- for (const meta of mainIndicators) {
314
- if (!meta.getTitleInfo) continue
315
- if (!scheduler?.isMainIndicatorActive(meta.name)) continue
316
- const params = scheduler?.getMainIndicatorParams(meta.name) ?? {}
317
-
318
- const titleInfo = meta.getTitleInfo(
319
- klineData,
320
- targetIndex,
321
- params as Record<string, number | boolean | string>,
322
- pluginHost!,
323
- 'main',
324
- )
325
- if (!titleInfo) continue
326
-
327
- rows.push({
328
- draw: (rowIndex: number) => {
329
- let x = legendX
330
- let y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
331
- overlayCtx.fillStyle = colors.text.primary
332
- overlayCtx.fillText(titleInfo.name, x, y)
333
- x += measureTextWidth(overlayCtx, titleInfo.name)
334
-
335
- if (titleInfo.params && titleInfo.params.length > 0) {
336
- const paramText = `(${titleInfo.params.join(',')})`
337
- overlayCtx.fillStyle = colors.text.tertiary
338
- overlayCtx.fillText(paramText, x, y)
339
- x += measureTextWidth(overlayCtx, paramText) + gap
340
- } else {
341
- x += gap
342
- }
343
-
344
- if (titleInfo.values) {
345
- y += 1
346
- for (const item of titleInfo.values) {
347
- const valText = `${item.label} ${item.value.toFixed(3)}`
348
- overlayCtx.fillStyle = item.color
349
- overlayCtx.fillText(valText, x, y)
350
- x += measureTextWidth(overlayCtx, valText) + gap
351
- }
352
- }
353
- },
354
- })
355
- }
89
+ if (config.renderMode === 'external') return
90
+ if (!legend || !context.overlayCtx) return
356
91
 
357
- pushComparisonLegendRows(
358
- context,
359
- klineData,
360
- targetIndex,
361
- range,
362
- rows,
363
- config.yPaddingPx,
364
- overlayCtx,
365
- legendX,
366
- legendYOffset,
367
- lineHeight,
368
- gap,
369
- colors,
370
- )
371
-
372
- rows.forEach((row, index) => row.draw(index))
373
- overlayCtx.restore()
92
+ paintLegendOnCanvas(context.overlayCtx, legend)
374
93
  },
375
94
 
376
95
  getConfig() {
377
96
  return {
378
97
  yPaddingPx: config.yPaddingPx,
98
+ renderMode: config.renderMode,
379
99
  }
380
100
  },
381
101
 
@@ -383,110 +103,257 @@ export function createMainIndicatorLegendRendererPlugin(options: {
383
103
  if (typeof newConfig.yPaddingPx === 'number') {
384
104
  config.yPaddingPx = newConfig.yPaddingPx
385
105
  }
106
+ if (newConfig.renderMode === 'canvas' || newConfig.renderMode === 'external') {
107
+ config.renderMode = newConfig.renderMode
108
+ }
386
109
  },
387
110
  }
388
111
  }
389
112
 
390
- function pushComparisonLegendRows(
391
- context: RenderContext,
392
- klineData: KLineData[],
393
- targetIndex: number,
394
- range: { start: number; end: number },
395
- rows: Array<{ draw: (rowIndex: number) => void }>,
396
- yPaddingPx: number,
397
- overlayCtx: CanvasRenderingContext2D,
398
- legendX: number,
399
- legendYOffset: number,
400
- lineHeight: number,
401
- gap: number,
402
- colors: ReturnType<typeof resolveThemeColors>,
403
- ): void {
404
- const comparisonSymbols = context.comparisonSymbols
405
- const comparisonData = context.comparisonData
406
- const comparisonColors = context.comparisonColors
407
- if (!comparisonSymbols?.length || !comparisonData?.size) return
408
-
409
- const baseIndex = Math.max(0, range.start)
410
- const baseItem = klineData[baseIndex]
411
- if (!baseItem || !Number.isFinite(baseItem.close) || baseItem.close <= 0) return
412
-
413
- const baseDate = baseItem.date ?? ''
414
-
415
- for (const spec of comparisonSymbols) {
416
- const data = comparisonData.get(spec.symbol)
417
- if (!data?.length) continue
418
-
419
- const baseline = baseDate
420
- ? findBaselineByDate(data, baseDate)
421
- : findBaselineByTimestamp(data, baseItem.timestamp)
422
- if (!baseline || baseline.close <= 0) continue
423
-
424
- const byDate = new Map<string, KLineData>()
425
- for (const item of data) {
426
- byDate.set(item.date ?? String(item.timestamp), item)
113
+ function paintLegendOnCanvas(overlayCtx: CanvasRenderingContext2D, legend: LegendTemplateContext) {
114
+ const { layout, colors } = legend
115
+ const fontSize = 12
116
+ const { x: legendX, y: baseY, lineHeight, gap, compact } = layout
117
+
118
+ overlayCtx.save()
119
+ setCanvasFont(overlayCtx, getFont(fontSize))
120
+ overlayCtx.textAlign = 'left'
121
+ overlayCtx.textBaseline = 'top'
122
+
123
+ let rowIndex = 0
124
+
125
+ const rowY = () => baseY + rowIndex * lineHeight
126
+
127
+ if (legend.timeshare) {
128
+ const ts = legend.timeshare
129
+ if (!compact) {
130
+ let x = legendX
131
+ const y = rowY()
132
+ overlayCtx.fillStyle = colors.textPrimary
133
+ overlayCtx.fillText('现价 ', x, y)
134
+ x += measureTextWidth(overlayCtx, '现价 ')
135
+ overlayCtx.fillStyle = ts.changeColor
136
+ overlayCtx.fillText(ts.price.toFixed(2), x, y)
137
+ x += measureTextWidth(overlayCtx, ts.price.toFixed(2)) + gap
138
+
139
+ overlayCtx.fillStyle = colors.textPrimary
140
+ overlayCtx.fillText('均价 ', x, y)
141
+ x += measureTextWidth(overlayCtx, '均价 ')
142
+ overlayCtx.fillText(ts.average.toFixed(2), x, y)
143
+ x += measureTextWidth(overlayCtx, ts.average.toFixed(2)) + gap
144
+
145
+ overlayCtx.fillStyle = colors.textPrimary
146
+ overlayCtx.fillText('涨跌 ', x, y)
147
+ x += measureTextWidth(overlayCtx, '涨跌 ')
148
+ overlayCtx.fillStyle = ts.changeColor
149
+ const sign = ts.changeAmount > 0 ? '+' : ''
150
+ overlayCtx.fillText(`${sign}${ts.changeAmount.toFixed(2)}`, x, y)
151
+ x += measureTextWidth(overlayCtx, `${sign}${ts.changeAmount.toFixed(2)}`) + gap
152
+
153
+ overlayCtx.fillStyle = colors.textPrimary
154
+ overlayCtx.fillText('涨幅 ', x, y)
155
+ x += measureTextWidth(overlayCtx, '涨幅 ')
156
+ overlayCtx.fillStyle = ts.changeColor
157
+ const pctSign = ts.changePercent > 0 ? '+' : ''
158
+ overlayCtx.fillText(`${pctSign}${ts.changePercent.toFixed(2)}%`, x, y)
159
+ x += measureTextWidth(overlayCtx, `${pctSign}${ts.changePercent.toFixed(2)}%`) + gap
160
+
161
+ overlayCtx.fillStyle = colors.textTertiary
162
+ overlayCtx.fillText('成交量 ', x, y)
163
+ x += measureTextWidth(overlayCtx, '成交量 ')
164
+ overlayCtx.fillStyle = colors.textPrimary
165
+ overlayCtx.fillText(ts.volumeText, x, y)
166
+ x += measureTextWidth(overlayCtx, ts.volumeText) + gap
167
+
168
+ overlayCtx.fillStyle = colors.textTertiary
169
+ overlayCtx.fillText('成交额 ', x, y)
170
+ x += measureTextWidth(overlayCtx, '成交额 ')
171
+ overlayCtx.fillStyle = colors.textPrimary
172
+ overlayCtx.fillText(ts.amountText, x, y)
173
+ rowIndex++
174
+ } else {
175
+ {
176
+ let x = legendX
177
+ const y = rowY()
178
+ overlayCtx.fillStyle = colors.textPrimary
179
+ overlayCtx.fillText('现价 ', x, y)
180
+ x += measureTextWidth(overlayCtx, '现价 ')
181
+ overlayCtx.fillStyle = ts.changeColor
182
+ overlayCtx.fillText(ts.price.toFixed(2), x, y)
183
+ x += measureTextWidth(overlayCtx, ts.price.toFixed(2)) + gap
184
+
185
+ overlayCtx.fillStyle = colors.textPrimary
186
+ overlayCtx.fillText('均价 ', x, y)
187
+ x += measureTextWidth(overlayCtx, '均价 ')
188
+ overlayCtx.fillText(ts.average.toFixed(2), x, y)
189
+ x += measureTextWidth(overlayCtx, ts.average.toFixed(2)) + gap
190
+
191
+ overlayCtx.fillStyle = colors.textTertiary
192
+ overlayCtx.fillText('成交量 ', x, y)
193
+ x += measureTextWidth(overlayCtx, '成交量 ')
194
+ overlayCtx.fillStyle = colors.textPrimary
195
+ overlayCtx.fillText(ts.volumeText, x, y)
196
+ rowIndex++
197
+ }
198
+ {
199
+ let x = legendX
200
+ const y = rowY()
201
+ overlayCtx.fillStyle = colors.textPrimary
202
+ overlayCtx.fillText('涨跌 ', x, y)
203
+ x += measureTextWidth(overlayCtx, '涨跌 ')
204
+ overlayCtx.fillStyle = ts.changeColor
205
+ const sign = ts.changeAmount > 0 ? '+' : ''
206
+ overlayCtx.fillText(`${sign}${ts.changeAmount.toFixed(2)}`, x, y)
207
+ x += measureTextWidth(overlayCtx, `${sign}${ts.changeAmount.toFixed(2)}`) + gap
208
+
209
+ overlayCtx.fillStyle = colors.textPrimary
210
+ overlayCtx.fillText('涨幅 ', x, y)
211
+ x += measureTextWidth(overlayCtx, '涨幅 ')
212
+ overlayCtx.fillStyle = ts.changeColor
213
+ const pctSign = ts.changePercent > 0 ? '+' : ''
214
+ overlayCtx.fillText(`${pctSign}${ts.changePercent.toFixed(2)}%`, x, y)
215
+ x += measureTextWidth(overlayCtx, `${pctSign}${ts.changePercent.toFixed(2)}%`) + gap
216
+
217
+ overlayCtx.fillStyle = colors.textTertiary
218
+ overlayCtx.fillText('成交额 ', x, y)
219
+ x += measureTextWidth(overlayCtx, '成交额 ')
220
+ overlayCtx.fillStyle = colors.textPrimary
221
+ overlayCtx.fillText(ts.amountText, x, y)
222
+ rowIndex++
223
+ }
427
224
  }
225
+ }
428
226
 
429
- const mainItem = klineData[targetIndex]
430
- if (!mainItem) continue
431
- const key = mainItem.date ?? String(mainItem.timestamp)
432
- const currentItem = byDate.get(key)
433
- if (!currentItem || !Number.isFinite(currentItem.close)) continue
434
-
435
- const pct = ((currentItem.close - baseline.close) / baseline.close) * 100
436
- const color = comparisonColors?.get(spec.symbol) ?? '#f59e0b'
437
-
438
- rows.push({
439
- draw: (rowIndex: number) => {
227
+ if (legend.ohlc) {
228
+ const k = legend.ohlc
229
+ if (!compact) {
230
+ let x = legendX
231
+ const y = rowY()
232
+ overlayCtx.fillStyle = colors.textPrimary
233
+ overlayCtx.fillText('O ', x, y)
234
+ x += measureTextWidth(overlayCtx, 'O ')
235
+ overlayCtx.fillStyle = k.color
236
+ overlayCtx.fillText(k.open.toFixed(2), x, y)
237
+ x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
238
+
239
+ overlayCtx.fillStyle = colors.textPrimary
240
+ overlayCtx.fillText('H ', x, y)
241
+ x += measureTextWidth(overlayCtx, 'H ')
242
+ overlayCtx.fillText(k.high.toFixed(2), x, y)
243
+ x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
244
+
245
+ overlayCtx.fillText('L ', x, y)
246
+ x += measureTextWidth(overlayCtx, 'L ')
247
+ overlayCtx.fillText(k.low.toFixed(2), x, y)
248
+ x += measureTextWidth(overlayCtx, k.low.toFixed(2)) + gap
249
+
250
+ overlayCtx.fillStyle = colors.textPrimary
251
+ overlayCtx.fillText('C ', x, y)
252
+ x += measureTextWidth(overlayCtx, 'C ')
253
+ overlayCtx.fillStyle = k.color
254
+ overlayCtx.fillText(k.close.toFixed(2), x, y)
255
+ x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
256
+
257
+ if (k.volumeText) {
258
+ overlayCtx.fillStyle = colors.textTertiary
259
+ overlayCtx.fillText('Vol ', x, y)
260
+ x += measureTextWidth(overlayCtx, 'Vol ')
261
+ overlayCtx.fillStyle = colors.textPrimary
262
+ overlayCtx.fillText(k.volumeText, x, y)
263
+ }
264
+ rowIndex++
265
+ } else {
266
+ {
440
267
  let x = legendX
441
- const y = yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
442
-
443
- const dotRadius = 4
444
- overlayCtx.fillStyle = color
445
- overlayCtx.beginPath()
446
- const fontSize = lineHeight - 6
447
- overlayCtx.arc(x + dotRadius, y + fontSize / 2 - 1, dotRadius, 0, Math.PI * 2)
448
- overlayCtx.fill()
449
- x += dotRadius * 2 + 4
450
-
451
- overlayCtx.fillStyle = colors.text.primary
452
- overlayCtx.fillText(spec.symbol, x, y)
453
- x += measureTextWidth(overlayCtx, spec.symbol) + gap
454
-
455
- const sign = pct > 0 ? '+' : ''
456
- const pctText = `${sign}${pct.toFixed(2)}%`
457
- overlayCtx.fillStyle =
458
- pct > 0 ? colors.candleUpBody : pct < 0 ? colors.candleDownBody : colors.text.primary
459
- overlayCtx.fillText(pctText, x, y)
460
- },
461
- })
268
+ const y = rowY()
269
+ overlayCtx.fillStyle = colors.textPrimary
270
+ overlayCtx.fillText('O ', x, y)
271
+ x += measureTextWidth(overlayCtx, 'O ')
272
+ overlayCtx.fillStyle = k.color
273
+ overlayCtx.fillText(k.open.toFixed(2), x, y)
274
+ x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
275
+
276
+ overlayCtx.fillStyle = colors.textPrimary
277
+ overlayCtx.fillText('H ', x, y)
278
+ x += measureTextWidth(overlayCtx, 'H ')
279
+ overlayCtx.fillText(k.high.toFixed(2), x, y)
280
+ x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
281
+
282
+ overlayCtx.fillText('L ', x, y)
283
+ x += measureTextWidth(overlayCtx, 'L ')
284
+ overlayCtx.fillText(k.low.toFixed(2), x, y)
285
+ rowIndex++
286
+ }
287
+ {
288
+ let x = legendX
289
+ const y = rowY()
290
+ overlayCtx.fillStyle = colors.textPrimary
291
+ overlayCtx.fillText('C ', x, y)
292
+ x += measureTextWidth(overlayCtx, 'C ')
293
+ overlayCtx.fillStyle = k.color
294
+ overlayCtx.fillText(k.close.toFixed(2), x, y)
295
+ x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
296
+
297
+ if (k.volumeText) {
298
+ overlayCtx.fillStyle = colors.textTertiary
299
+ overlayCtx.fillText('Vol ', x, y)
300
+ x += measureTextWidth(overlayCtx, 'Vol ')
301
+ overlayCtx.fillStyle = colors.textPrimary
302
+ overlayCtx.fillText(k.volumeText, x, y)
303
+ }
304
+ rowIndex++
305
+ }
306
+ }
462
307
  }
463
- }
464
308
 
465
- function findBaselineByDate(data: ReadonlyArray<KLineData>, date: string): KLineData | null {
466
- for (const item of data) {
467
- if (item.date && item.date >= date) return item
468
- }
469
- return null
470
- }
309
+ for (const titleInfo of legend.indicators) {
310
+ let x = legendX
311
+ let y = rowY()
312
+ overlayCtx.fillStyle = colors.textPrimary
313
+ overlayCtx.fillText(titleInfo.name, x, y)
314
+ x += measureTextWidth(overlayCtx, titleInfo.name)
315
+
316
+ if (titleInfo.params && titleInfo.params.length > 0) {
317
+ const paramText = `(${titleInfo.params.join(',')})`
318
+ overlayCtx.fillStyle = colors.textTertiary
319
+ overlayCtx.fillText(paramText, x, y)
320
+ x += measureTextWidth(overlayCtx, paramText) + gap
321
+ } else {
322
+ x += gap
323
+ }
471
324
 
472
- function findBaselineByTimestamp(
473
- data: ReadonlyArray<KLineData>,
474
- timestamp: number,
475
- ): KLineData | null {
476
- for (const item of data) {
477
- if (item.timestamp >= timestamp) return item
325
+ if (titleInfo.values) {
326
+ y += 1
327
+ for (const item of titleInfo.values) {
328
+ const valText = `${item.label} ${item.value.toFixed(3)}`
329
+ overlayCtx.fillStyle = item.color
330
+ overlayCtx.fillText(valText, x, y)
331
+ x += measureTextWidth(overlayCtx, valText) + gap
332
+ }
333
+ }
334
+ rowIndex++
478
335
  }
479
- return null
480
- }
481
336
 
482
- function formatVolumeShort(v: number): string {
483
- if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
484
- if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
485
- return v.toFixed(2)
486
- }
337
+ for (const cmp of legend.comparisons) {
338
+ let x = legendX
339
+ const y = rowY()
340
+ const dotRadius = 4
341
+ overlayCtx.fillStyle = cmp.color
342
+ overlayCtx.beginPath()
343
+ overlayCtx.arc(x + dotRadius, y + fontSize / 2 - 1, dotRadius, 0, Math.PI * 2)
344
+ overlayCtx.fill()
345
+ x += dotRadius * 2 + 4
346
+
347
+ overlayCtx.fillStyle = colors.textPrimary
348
+ overlayCtx.fillText(cmp.symbol, x, y)
349
+ x += measureTextWidth(overlayCtx, cmp.symbol) + gap
350
+
351
+ const sign = cmp.percent > 0 ? '+' : ''
352
+ const pctText = `${sign}${cmp.percent.toFixed(2)}%`
353
+ overlayCtx.fillStyle = cmp.percentColor
354
+ overlayCtx.fillText(pctText, x, y)
355
+ rowIndex++
356
+ }
487
357
 
488
- function formatAmountShort(v: number): string {
489
- if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
490
- if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
491
- return v.toFixed(2)
358
+ overlayCtx.restore()
492
359
  }