@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.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +9 -8
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/types.d.ts +5 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data/binance.d.ts +1 -1
- package/dist/data/binance.d.ts.map +1 -1
- package/dist/data/binance.js +1 -1
- package/dist/data/binance.js.map +1 -1
- package/dist/engine/chart.d.ts +9 -9
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +69 -96
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartTypes.d.ts +5 -0
- package/dist/engine/chartTypes.d.ts.map +1 -1
- package/dist/engine/controller/interaction.d.ts +4 -2
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +18 -10
- package/dist/engine/controller/interaction.js.map +1 -1
- package/dist/engine/data/chartDataManager.d.ts +12 -18
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +46 -37
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/data/incrementalLoadHint.d.ts +2 -4
- package/dist/engine/data/incrementalLoadHint.d.ts.map +1 -1
- package/dist/engine/data/incrementalLoadHint.js +5 -2
- package/dist/engine/data/incrementalLoadHint.js.map +1 -1
- package/dist/engine/data/scrollCompensator.d.ts +3 -13
- package/dist/engine/data/scrollCompensator.d.ts.map +1 -1
- package/dist/engine/data/scrollCompensator.js +13 -14
- package/dist/engine/data/scrollCompensator.js.map +1 -1
- package/dist/engine/indicators/chartIndicatorManager.d.ts +16 -17
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -1
- package/dist/engine/indicators/chartIndicatorManager.js +37 -37
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -1
- package/dist/engine/layout/chartPaneLayout.d.ts +11 -7
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -1
- package/dist/engine/layout/chartPaneLayout.js +58 -16
- package/dist/engine/layout/chartPaneLayout.js.map +1 -1
- package/dist/engine/paneRenderer.d.ts +2 -0
- package/dist/engine/paneRenderer.d.ts.map +1 -1
- package/dist/engine/paneRenderer.js +15 -5
- package/dist/engine/paneRenderer.js.map +1 -1
- package/dist/engine/render/chartRenderer.d.ts +28 -22
- package/dist/engine/render/chartRenderer.d.ts.map +1 -1
- package/dist/engine/render/chartRenderer.js +117 -69
- package/dist/engine/render/chartRenderer.js.map +1 -1
- package/dist/engine/render/layers/leftYAxisLayer.d.ts +9 -2
- package/dist/engine/render/layers/leftYAxisLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/leftYAxisLayer.js +11 -2
- package/dist/engine/render/layers/leftYAxisLayer.js.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts +6 -4
- package/dist/engine/render/layers/mainIndicatorLegendLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.js +6 -9
- package/dist/engine/render/layers/mainIndicatorLegendLayer.js.map +1 -1
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts +2 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.d.ts.map +1 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js +19 -0
- package/dist/engine/render/layers/mainIndicatorLegendLayer.test.js.map +1 -0
- package/dist/engine/render/layers/yAxisLayer.d.ts +9 -2
- package/dist/engine/render/layers/yAxisLayer.d.ts.map +1 -1
- package/dist/engine/render/layers/yAxisLayer.js +11 -2
- package/dist/engine/render/layers/yAxisLayer.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts +7 -3
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +234 -330
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts +72 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +177 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js.map +1 -0
- package/dist/engine/renderers/leftYAxis.d.ts +15 -2
- package/dist/engine/renderers/leftYAxis.d.ts.map +1 -1
- package/dist/engine/renderers/leftYAxis.js +56 -16
- package/dist/engine/renderers/leftYAxis.js.map +1 -1
- package/dist/engine/renderers/yAxis.d.ts +16 -6
- package/dist/engine/renderers/yAxis.d.ts.map +1 -1
- package/dist/engine/renderers/yAxis.js +49 -16
- package/dist/engine/renderers/yAxis.js.map +1 -1
- package/dist/engine/state/viewportState.d.ts +4 -1
- package/dist/engine/state/viewportState.d.ts.map +1 -1
- package/dist/engine/state/viewportState.js +40 -8
- package/dist/engine/state/viewportState.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +5 -3
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +3 -2
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +9 -7
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -1
- package/dist/engine/utils/chartZoomController.js +13 -12
- package/dist/engine/utils/chartZoomController.js.map +1 -1
- package/dist/engine/viewport/chartViewportManager.d.ts +5 -16
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -1
- package/dist/engine/viewport/chartViewportManager.js +7 -48
- package/dist/engine/viewport/chartViewportManager.js.map +1 -1
- package/dist/engine/viewport/viewport.d.ts +14 -0
- package/dist/engine/viewport/viewport.d.ts.map +1 -1
- package/dist/engine/viewport/viewport.js +10 -0
- package/dist/engine/viewport/viewport.js.map +1 -1
- package/dist/foundation/plugin/rendererPluginManager.test.js +15 -0
- package/dist/foundation/plugin/rendererPluginManager.test.js.map +1 -1
- package/dist/foundation/plugin/types.d.ts +3 -0
- package/dist/foundation/plugin/types.d.ts.map +1 -1
- package/dist/foundation/plugin/types.js.map +1 -1
- package/dist/foundation/reactivity/frameTransaction.d.ts +11 -0
- package/dist/foundation/reactivity/frameTransaction.d.ts.map +1 -1
- package/dist/foundation/reactivity/frameTransaction.js +17 -2
- package/dist/foundation/reactivity/frameTransaction.js.map +1 -1
- package/dist/rendering/render/Renderer.d.ts +58 -74
- package/dist/rendering/render/Renderer.d.ts.map +1 -1
- package/dist/rendering/render/Renderer.js +8 -18
- package/dist/rendering/render/Renderer.js.map +1 -1
- package/dist/rendering/render/createWebGPURenderer.d.ts.map +1 -1
- package/dist/rendering/render/createWebGPURenderer.js +30 -17
- package/dist/rendering/render/createWebGPURenderer.js.map +1 -1
- package/dist/rendering/render/createWebGPUSurfaceBackend.js +3 -3
- package/dist/rendering/render/createWebGPUSurfaceBackend.js.map +1 -1
- package/dist/rendering/render/physicalLine.d.ts +4 -0
- package/dist/rendering/render/physicalLine.d.ts.map +1 -0
- package/dist/rendering/render/physicalLine.js +37 -0
- package/dist/rendering/render/physicalLine.js.map +1 -0
- package/dist/rendering/render/physicalRegion.d.ts +14 -0
- package/dist/rendering/render/physicalRegion.d.ts.map +1 -0
- package/dist/rendering/render/physicalRegion.js +15 -0
- package/dist/rendering/render/physicalRegion.js.map +1 -0
- package/dist/rendering/scene/createLayerFromPlugin.d.ts.map +1 -1
- package/dist/rendering/scene/createLayerFromPlugin.js +8 -3
- package/dist/rendering/scene/createLayerFromPlugin.js.map +1 -1
- package/dist/rendering/scene/createScene.d.ts.map +1 -1
- package/dist/rendering/scene/createScene.js +4 -3
- package/dist/rendering/scene/createScene.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/stateKernel.test.ts +1 -0
- package/src/controllers/createChartController.ts +9 -8
- package/src/controllers/index.ts +11 -0
- package/src/controllers/types.ts +7 -0
- package/src/data/binance.ts +1 -1
- package/src/engine/__tests__/chart.dpr.test.ts +3 -2
- package/src/engine/__tests__/paneRenderer.resize.test.ts +52 -54
- package/src/engine/__tests__/subPaneManager.test.ts +5 -3
- package/src/engine/chart.ts +76 -109
- package/src/engine/chartTypes.ts +5 -0
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +39 -2
- package/src/engine/controller/interaction.ts +18 -10
- package/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts +37 -17
- package/src/engine/data/__tests__/scrollCompensator.geometry.test.ts +87 -75
- package/src/engine/data/chartDataManager.ts +54 -59
- package/src/engine/data/incrementalLoadHint.ts +8 -7
- package/src/engine/data/scrollCompensator.ts +16 -24
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +18 -26
- package/src/engine/indicators/chartIndicatorManager.ts +62 -62
- package/src/engine/layout/chartPaneLayout.ts +73 -21
- package/src/engine/paneRenderer.ts +17 -10
- package/src/engine/render/chartRenderer.ts +187 -129
- package/src/engine/render/layers/leftYAxisLayer.ts +29 -7
- package/src/engine/render/layers/mainIndicatorLegendLayer.test.ts +26 -0
- package/src/engine/render/layers/mainIndicatorLegendLayer.ts +13 -10
- package/src/engine/render/layers/yAxisLayer.ts +29 -7
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +270 -403
- package/src/engine/renderers/Indicator/mainIndicatorLegendContext.ts +282 -0
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +73 -2
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +10 -6
- package/src/engine/renderers/leftYAxis.ts +60 -18
- package/src/engine/renderers/yAxis.ts +57 -22
- package/src/engine/state/viewportState.ts +44 -8
- package/src/engine/subPaneManager.ts +8 -5
- package/src/engine/utils/chartZoomController.ts +22 -19
- package/src/engine/viewport/__tests__/visibleRange.clamp.test.ts +50 -0
- package/src/engine/viewport/chartViewportManager.ts +9 -60
- package/src/engine/viewport/viewport.ts +14 -0
- package/src/foundation/plugin/rendererPluginManager.test.ts +18 -0
- package/src/foundation/plugin/types.ts +3 -0
- package/src/foundation/reactivity/frameTransaction.ts +20 -6
- package/src/rendering/render/Renderer.ts +69 -85
- package/src/rendering/render/__tests__/physicalLine.test.ts +51 -0
- package/src/rendering/render/__tests__/physicalRegion.test.ts +23 -0
- package/src/rendering/render/__tests__/webgpuRenderer.test.ts +31 -1
- package/src/rendering/render/__tests__/webgpuSurfaceBackend.test.ts +11 -0
- package/src/rendering/render/createWebGPURenderer.ts +30 -17
- package/src/rendering/render/createWebGPUSurfaceBackend.ts +3 -3
- package/src/rendering/render/physicalLine.ts +45 -0
- package/src/rendering/render/physicalRegion.ts +32 -0
- package/src/rendering/scene/createLayerFromPlugin.ts +8 -3
- package/src/rendering/scene/createScene.ts +4 -3
- package/src/version.ts +1 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { PluginHost, RenderContext } from '../../../foundation/plugin/index'
|
|
2
|
+
import { resolveThemeColors } from '../../../foundation/tokens/index'
|
|
3
|
+
import type { KLineData, TimeShareData } from '../../../foundation/types/price'
|
|
4
|
+
import type { TitleInfo, TitleValueItem } from '../../indicators/indicatorMetadata'
|
|
5
|
+
import type { IndicatorScheduler } from '../../indicators/scheduler'
|
|
6
|
+
|
|
7
|
+
/** 图例渲染模式:canvas 默认绘制;external 仅发布上下文,不画 Canvas 文字 */
|
|
8
|
+
export type LegendRenderMode = 'canvas' | 'external'
|
|
9
|
+
|
|
10
|
+
export interface LegendLayout {
|
|
11
|
+
x: number
|
|
12
|
+
y: number
|
|
13
|
+
lineHeight: number
|
|
14
|
+
gap: number
|
|
15
|
+
paneWidth: number
|
|
16
|
+
compact: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 当前 K 线及图例派生的展示字段,保留 KLineData 自定义属性。 */
|
|
20
|
+
export type LegendOhlcRow = Omit<KLineData, 'volume'> & {
|
|
21
|
+
volume: number | null
|
|
22
|
+
volumeText: string | null
|
|
23
|
+
color: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LegendTimeshareRow {
|
|
27
|
+
price: number
|
|
28
|
+
average: number
|
|
29
|
+
changeAmount: number
|
|
30
|
+
changePercent: number
|
|
31
|
+
volume: number
|
|
32
|
+
volumeText: string
|
|
33
|
+
amount: number
|
|
34
|
+
amountText: string
|
|
35
|
+
changeColor: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LegendIndicatorRow {
|
|
39
|
+
name: string
|
|
40
|
+
params?: number[]
|
|
41
|
+
values?: TitleValueItem[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LegendComparisonRow {
|
|
45
|
+
symbol: string
|
|
46
|
+
percent: number
|
|
47
|
+
color: string
|
|
48
|
+
percentColor: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 主图左上角图例完整上下文。
|
|
53
|
+
* Canvas 绘制与 Vue legend slot 共用同一份数据。
|
|
54
|
+
*/
|
|
55
|
+
export interface LegendTemplateContext {
|
|
56
|
+
period: string
|
|
57
|
+
index: number
|
|
58
|
+
hasCrosshair: boolean
|
|
59
|
+
layout: LegendLayout
|
|
60
|
+
colors: {
|
|
61
|
+
textPrimary: string
|
|
62
|
+
textTertiary: string
|
|
63
|
+
up: string
|
|
64
|
+
down: string
|
|
65
|
+
}
|
|
66
|
+
ohlc: LegendOhlcRow | null
|
|
67
|
+
timeshare: LegendTimeshareRow | null
|
|
68
|
+
indicators: ReadonlyArray<LegendIndicatorRow>
|
|
69
|
+
comparisons: ReadonlyArray<LegendComparisonRow>
|
|
70
|
+
/** 当前索引处的 K 线(分时模式下可能无 close) */
|
|
71
|
+
bar: KLineData | TimeShareData | null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BuildLegendTemplateContextInput {
|
|
75
|
+
context: RenderContext
|
|
76
|
+
host: PluginHost | null
|
|
77
|
+
yPaddingPx: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatVolumeShort(v: number): string {
|
|
81
|
+
if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
|
|
82
|
+
if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
|
|
83
|
+
return v.toFixed(2)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatAmountShort(v: number): string {
|
|
87
|
+
if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
|
|
88
|
+
if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
|
|
89
|
+
return v.toFixed(2)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildLegendTemplateContext(
|
|
93
|
+
input: BuildLegendTemplateContextInput,
|
|
94
|
+
): LegendTemplateContext | null {
|
|
95
|
+
const { context, host, yPaddingPx } = input
|
|
96
|
+
const klineData = context.data as KLineData[]
|
|
97
|
+
if (!klineData.length) return null
|
|
98
|
+
|
|
99
|
+
const colors = resolveThemeColors(
|
|
100
|
+
context.theme,
|
|
101
|
+
context.isAsiaMarket,
|
|
102
|
+
context.colorPresetSettings,
|
|
103
|
+
)
|
|
104
|
+
const fontSize = 12
|
|
105
|
+
const lineHeight = fontSize + 6
|
|
106
|
+
const legendX = 12
|
|
107
|
+
const gap = 10
|
|
108
|
+
const legendYOffset = 6
|
|
109
|
+
const compact = context.paneWidth < 400
|
|
110
|
+
const range = context.range
|
|
111
|
+
const crosshairIndex = context.crosshairIndex
|
|
112
|
+
const hasCrosshair = typeof crosshairIndex === 'number'
|
|
113
|
+
const targetIndex = hasCrosshair
|
|
114
|
+
? crosshairIndex
|
|
115
|
+
: Math.min(range.end - 1, klineData.length - 1)
|
|
116
|
+
|
|
117
|
+
const layout: LegendLayout = {
|
|
118
|
+
x: legendX,
|
|
119
|
+
y: yPaddingPx / 2 + legendYOffset,
|
|
120
|
+
lineHeight,
|
|
121
|
+
gap,
|
|
122
|
+
paneWidth: context.paneWidth,
|
|
123
|
+
compact,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let timeshare: LegendTimeshareRow | null = null
|
|
127
|
+
if (context.period === 'timeshare') {
|
|
128
|
+
const tsData = context.data as TimeShareData[]
|
|
129
|
+
const preClose = (context.settings?.preClose as number) ?? tsData[0]?.price ?? 0
|
|
130
|
+
const item = tsData[targetIndex]
|
|
131
|
+
if (item) {
|
|
132
|
+
const changeAmount = item.price - preClose
|
|
133
|
+
const changePercent = preClose !== 0 ? (changeAmount / preClose) * 100 : 0
|
|
134
|
+
timeshare = {
|
|
135
|
+
price: item.price,
|
|
136
|
+
average: item.average,
|
|
137
|
+
changeAmount,
|
|
138
|
+
changePercent,
|
|
139
|
+
volume: item.volume,
|
|
140
|
+
volumeText: formatVolumeShort(item.volume),
|
|
141
|
+
amount: item.amount,
|
|
142
|
+
amountText: formatAmountShort(item.amount),
|
|
143
|
+
changeColor: changeAmount >= 0 ? colors.candleUpBody : colors.candleDownBody,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let ohlc: LegendOhlcRow | null = null
|
|
149
|
+
if (hasCrosshair) {
|
|
150
|
+
const k = klineData[targetIndex]
|
|
151
|
+
if (k && typeof k.close === 'number') {
|
|
152
|
+
const isUp = k.close >= k.open
|
|
153
|
+
ohlc = {
|
|
154
|
+
...k,
|
|
155
|
+
volume: typeof k.volume === 'number' ? k.volume : null,
|
|
156
|
+
volumeText: typeof k.volume === 'number' ? formatVolumeShort(k.volume) : null,
|
|
157
|
+
color: isUp ? colors.candleUpBody : colors.candleDownBody,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const indicators = collectIndicatorRows(host, klineData, targetIndex)
|
|
163
|
+
const comparisons = collectComparisonRows(context, klineData, targetIndex, range, colors)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
period: context.period,
|
|
167
|
+
index: targetIndex,
|
|
168
|
+
hasCrosshair,
|
|
169
|
+
layout,
|
|
170
|
+
colors: {
|
|
171
|
+
textPrimary: colors.text.primary,
|
|
172
|
+
textTertiary: colors.text.tertiary,
|
|
173
|
+
up: colors.candleUpBody,
|
|
174
|
+
down: colors.candleDownBody,
|
|
175
|
+
},
|
|
176
|
+
ohlc,
|
|
177
|
+
timeshare,
|
|
178
|
+
indicators,
|
|
179
|
+
comparisons,
|
|
180
|
+
bar: klineData[targetIndex] ?? null,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function collectIndicatorRows(
|
|
185
|
+
host: PluginHost | null,
|
|
186
|
+
klineData: KLineData[],
|
|
187
|
+
targetIndex: number,
|
|
188
|
+
): LegendIndicatorRow[] {
|
|
189
|
+
if (!host || typeof host.getService !== 'function') return []
|
|
190
|
+
const scheduler = host.getService<IndicatorScheduler>('indicatorScheduler')
|
|
191
|
+
if (!scheduler) return []
|
|
192
|
+
|
|
193
|
+
const rows: LegendIndicatorRow[] = []
|
|
194
|
+
for (const meta of scheduler.getMainIndicators()) {
|
|
195
|
+
if (!meta.getTitleInfo) continue
|
|
196
|
+
if (!scheduler.isMainIndicatorActive(meta.name)) continue
|
|
197
|
+
const params = scheduler.getMainIndicatorParams(meta.name) ?? {}
|
|
198
|
+
const titleInfo: TitleInfo | null = meta.getTitleInfo(
|
|
199
|
+
klineData,
|
|
200
|
+
targetIndex,
|
|
201
|
+
params as Record<string, number | boolean | string>,
|
|
202
|
+
host,
|
|
203
|
+
'main',
|
|
204
|
+
)
|
|
205
|
+
if (!titleInfo) continue
|
|
206
|
+
rows.push({
|
|
207
|
+
name: titleInfo.name,
|
|
208
|
+
params: titleInfo.params,
|
|
209
|
+
values: titleInfo.values,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
return rows
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function collectComparisonRows(
|
|
216
|
+
context: RenderContext,
|
|
217
|
+
klineData: KLineData[],
|
|
218
|
+
targetIndex: number,
|
|
219
|
+
range: { start: number; end: number },
|
|
220
|
+
colors: ReturnType<typeof resolveThemeColors>,
|
|
221
|
+
): LegendComparisonRow[] {
|
|
222
|
+
const comparisonSymbols = context.comparisonSymbols
|
|
223
|
+
const comparisonData = context.comparisonData
|
|
224
|
+
const comparisonColors = context.comparisonColors
|
|
225
|
+
if (!comparisonSymbols?.length || !comparisonData?.size) return []
|
|
226
|
+
|
|
227
|
+
const baseIndex = Math.max(0, range.start)
|
|
228
|
+
const baseItem = klineData[baseIndex]
|
|
229
|
+
if (!baseItem || !Number.isFinite(baseItem.close) || baseItem.close <= 0) return []
|
|
230
|
+
|
|
231
|
+
const baseDate = baseItem.date ?? ''
|
|
232
|
+
const rows: LegendComparisonRow[] = []
|
|
233
|
+
|
|
234
|
+
for (const spec of comparisonSymbols) {
|
|
235
|
+
const data = comparisonData.get(spec.symbol)
|
|
236
|
+
if (!data?.length) continue
|
|
237
|
+
|
|
238
|
+
const baseline = baseDate
|
|
239
|
+
? findBaselineByDate(data, baseDate)
|
|
240
|
+
: findBaselineByTimestamp(data, baseItem.timestamp)
|
|
241
|
+
if (!baseline || baseline.close <= 0) continue
|
|
242
|
+
|
|
243
|
+
const byDate = new Map<string, KLineData>()
|
|
244
|
+
for (const item of data) {
|
|
245
|
+
byDate.set(item.date ?? String(item.timestamp), item)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const mainItem = klineData[targetIndex]
|
|
249
|
+
if (!mainItem) continue
|
|
250
|
+
const key = mainItem.date ?? String(mainItem.timestamp)
|
|
251
|
+
const currentItem = byDate.get(key)
|
|
252
|
+
if (!currentItem || !Number.isFinite(currentItem.close)) continue
|
|
253
|
+
|
|
254
|
+
const percent = ((currentItem.close - baseline.close) / baseline.close) * 100
|
|
255
|
+
const color = comparisonColors?.get(spec.symbol) ?? '#f59e0b'
|
|
256
|
+
rows.push({
|
|
257
|
+
symbol: spec.symbol,
|
|
258
|
+
percent,
|
|
259
|
+
color,
|
|
260
|
+
percentColor:
|
|
261
|
+
percent > 0 ? colors.candleUpBody : percent < 0 ? colors.candleDownBody : colors.text.primary,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
return rows
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findBaselineByDate(data: ReadonlyArray<KLineData>, date: string): KLineData | null {
|
|
268
|
+
for (const item of data) {
|
|
269
|
+
if (item.date && item.date >= date) return item
|
|
270
|
+
}
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findBaselineByTimestamp(
|
|
275
|
+
data: ReadonlyArray<KLineData>,
|
|
276
|
+
timestamp: number,
|
|
277
|
+
): KLineData | null {
|
|
278
|
+
for (const item of data) {
|
|
279
|
+
if (item.timestamp >= timestamp) return item
|
|
280
|
+
}
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
@@ -20,7 +20,7 @@ import type { KLineData } from '@/types/price'
|
|
|
20
20
|
// Type helper for tests - we know these methods exist on the implementation
|
|
21
21
|
interface TestableLegendRenderer extends RendererPluginWithHost {
|
|
22
22
|
draw: (context: RenderContext) => void
|
|
23
|
-
getConfig: () => { yPaddingPx: number }
|
|
23
|
+
getConfig: () => { yPaddingPx: number; renderMode: 'canvas' | 'external' }
|
|
24
24
|
setConfig: (config: Record<string, unknown>) => void
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -210,7 +210,7 @@ describe('createMainIndicatorLegendRendererPlugin', () => {
|
|
|
210
210
|
const plugin = createMainIndicatorLegendRendererPlugin({ yPaddingPx: 20 })
|
|
211
211
|
|
|
212
212
|
expect(plugin.name).toBe('mainIndicatorLegend')
|
|
213
|
-
expect(plugin.version).toBe('2.
|
|
213
|
+
expect(plugin.version).toBe('2.2.0')
|
|
214
214
|
expect(plugin.description).toBe('主图指标图例渲染器(MA 数据来自 StateStore)')
|
|
215
215
|
expect(plugin.debugName).toBe('主图指标图例')
|
|
216
216
|
expect(plugin.paneId).toBe('main')
|
|
@@ -552,3 +552,74 @@ describe('MainIndicatorLegend with other indicators', () => {
|
|
|
552
552
|
expect(wmaLabelCalls.length).toBeGreaterThan(0)
|
|
553
553
|
})
|
|
554
554
|
})
|
|
555
|
+
|
|
556
|
+
describe('MainIndicatorLegend external mode & context callback', () => {
|
|
557
|
+
it('publishes legend context via onContext while still painting in canvas mode', () => {
|
|
558
|
+
const onContext = vi.fn()
|
|
559
|
+
const scheduler = createMockScheduler({ ma: { getTitleInfo: createMAGetTitleInfo() } }, ['ma'])
|
|
560
|
+
const state = createTestMARenderState()
|
|
561
|
+
const mockHost = createMockPluginHost(state, scheduler)
|
|
562
|
+
const plugin = createMainIndicatorLegendRendererPlugin({
|
|
563
|
+
yPaddingPx: 20,
|
|
564
|
+
onContext,
|
|
565
|
+
}) as TestableLegendRenderer
|
|
566
|
+
plugin.onInstall(mockHost)
|
|
567
|
+
|
|
568
|
+
const ctx = createMockCanvasContext()
|
|
569
|
+
const context = createMockRenderContext(ctx, { crosshairIndex: 50 })
|
|
570
|
+
plugin.draw(context)
|
|
571
|
+
|
|
572
|
+
expect(onContext).toHaveBeenCalledTimes(1)
|
|
573
|
+
const legend = onContext.mock.calls[0]![0]
|
|
574
|
+
expect(legend).not.toBeNull()
|
|
575
|
+
expect(legend.index).toBe(50)
|
|
576
|
+
expect(legend.hasCrosshair).toBe(true)
|
|
577
|
+
expect(legend.ohlc).not.toBeNull()
|
|
578
|
+
expect(legend.indicators.some((row: { name: string }) => row.name === 'MA')).toBe(true)
|
|
579
|
+
expect(vi.mocked(ctx.fillText).mock.calls.length).toBeGreaterThan(0)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('does not paint canvas text when renderMode is external but still publishes context', () => {
|
|
583
|
+
const onContext = vi.fn()
|
|
584
|
+
const scheduler = createMockScheduler({ ma: { getTitleInfo: createMAGetTitleInfo() } }, ['ma'])
|
|
585
|
+
const state = createTestMARenderState()
|
|
586
|
+
const mockHost = createMockPluginHost(state, scheduler)
|
|
587
|
+
const plugin = createMainIndicatorLegendRendererPlugin({
|
|
588
|
+
yPaddingPx: 20,
|
|
589
|
+
onContext,
|
|
590
|
+
}) as TestableLegendRenderer
|
|
591
|
+
plugin.onInstall(mockHost)
|
|
592
|
+
plugin.setConfig({ renderMode: 'external' })
|
|
593
|
+
expect(plugin.getConfig().renderMode).toBe('external')
|
|
594
|
+
|
|
595
|
+
const ctx = createMockCanvasContext()
|
|
596
|
+
const context = createMockRenderContext(ctx, { crosshairIndex: 10 })
|
|
597
|
+
plugin.draw(context)
|
|
598
|
+
|
|
599
|
+
expect(onContext).toHaveBeenCalledTimes(1)
|
|
600
|
+
expect(onContext.mock.calls[0]![0]).not.toBeNull()
|
|
601
|
+
expect(vi.mocked(ctx.fillText)).not.toHaveBeenCalled()
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('retains custom KLineData fields in the ohlc slot context', () => {
|
|
605
|
+
const onContext = vi.fn()
|
|
606
|
+
const plugin = createMainIndicatorLegendRendererPlugin({
|
|
607
|
+
yPaddingPx: 20,
|
|
608
|
+
onContext,
|
|
609
|
+
}) as TestableLegendRenderer
|
|
610
|
+
plugin.onInstall(createMockPluginHost())
|
|
611
|
+
plugin.setConfig({ renderMode: 'external' })
|
|
612
|
+
|
|
613
|
+
const ctx = createMockCanvasContext()
|
|
614
|
+
const context = createMockRenderContext(ctx, { crosshairIndex: 10 })
|
|
615
|
+
Object.assign((context.data as KLineData[])[10]!, {
|
|
616
|
+
turnoverRate: 3.14,
|
|
617
|
+
customLabel: 'featured',
|
|
618
|
+
})
|
|
619
|
+
plugin.draw(context)
|
|
620
|
+
|
|
621
|
+
const legend = onContext.mock.calls[0]![0]
|
|
622
|
+
expect(legend.ohlc.turnoverRate).toBe(3.14)
|
|
623
|
+
expect(legend.ohlc.customLabel).toBe('featured')
|
|
624
|
+
})
|
|
625
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { createYAxisRendererPlugin } from '@/core/renderers/yAxis'
|
|
3
|
+
import { createYAxisRendererPlugin, createYAxisOverlayRendererPlugin } from '@/core/renderers/yAxis'
|
|
4
4
|
import type { RenderContext, PaneInfo, YAxisTick } from '@/plugin'
|
|
5
5
|
|
|
6
6
|
vi.mock('@/utils/kLineDraw/axis', () => ({
|
|
@@ -135,9 +135,10 @@ describe('yAxis renderer', () => {
|
|
|
135
135
|
})
|
|
136
136
|
|
|
137
137
|
it('draws last price label via drawAxisPriceLabel for main pane when yAxisLabels contains lastPrice', () => {
|
|
138
|
-
const plugin =
|
|
138
|
+
const plugin = createYAxisOverlayRendererPlugin({ axisWidth: 80, yPaddingPx: 0 })
|
|
139
139
|
const context = createContext({
|
|
140
140
|
pane: createPane({ id: 'main' }),
|
|
141
|
+
yAxisOverlayCtx: createCtx(),
|
|
141
142
|
yAxisLabels: [
|
|
142
143
|
{
|
|
143
144
|
type: 'lastPrice',
|
|
@@ -165,12 +166,15 @@ describe('yAxis renderer', () => {
|
|
|
165
166
|
})
|
|
166
167
|
|
|
167
168
|
it('draws crosshair price label exactly once for active pane', () => {
|
|
168
|
-
const plugin =
|
|
169
|
+
const plugin = createYAxisOverlayRendererPlugin({
|
|
169
170
|
axisWidth: 80,
|
|
170
171
|
yPaddingPx: 0,
|
|
171
172
|
getCrosshair: () => ({ y: 55, price: 95, activePaneId: 'main' }),
|
|
172
173
|
})
|
|
173
|
-
const context = createContext({
|
|
174
|
+
const context = createContext({
|
|
175
|
+
pane: createPane({ id: 'main' }),
|
|
176
|
+
yAxisOverlayCtx: createCtx(),
|
|
177
|
+
})
|
|
174
178
|
|
|
175
179
|
plugin.draw(context)
|
|
176
180
|
|
|
@@ -178,12 +182,12 @@ describe('yAxis renderer', () => {
|
|
|
178
182
|
})
|
|
179
183
|
|
|
180
184
|
it('does not draw crosshair price label when getCrosshair returns null', () => {
|
|
181
|
-
const plugin =
|
|
185
|
+
const plugin = createYAxisOverlayRendererPlugin({
|
|
182
186
|
axisWidth: 80,
|
|
183
187
|
yPaddingPx: 0,
|
|
184
188
|
getCrosshair: () => null,
|
|
185
189
|
})
|
|
186
|
-
const context = createContext()
|
|
190
|
+
const context = createContext({ yAxisOverlayCtx: createCtx() })
|
|
187
191
|
|
|
188
192
|
plugin.draw(context)
|
|
189
193
|
|
|
@@ -6,49 +6,52 @@ import { roundToPhysicalPixel } from '../../foundation/utils/pixelAlign'
|
|
|
6
6
|
import { getFont, setCanvasFont } from '../../foundation/tokens/fonts'
|
|
7
7
|
import { priceAtYForScaleType, type ScaleType } from '../utils/tickPosition'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type LeftYAxisOptions = {
|
|
10
10
|
axisWidth: number
|
|
11
11
|
yPaddingPx: number
|
|
12
12
|
getCrosshair?: () => { y: number; price: number; activePaneId: string | null } | null
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function shouldShowLeftAxis(period: string, settings: RenderContext['settings']): boolean {
|
|
16
|
+
if (period === 'timeshare') return true
|
|
17
|
+
const leftType = settings?.leftAxisType as string | undefined
|
|
18
|
+
return Boolean(leftType && leftType !== 'none')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 左 Y 轴静态层:刻度,画到 leftAxisCtx(main 级刷新)
|
|
23
|
+
*/
|
|
24
|
+
export function createLeftYAxisStaticRendererPlugin(options: LeftYAxisOptions): RendererPlugin {
|
|
14
25
|
return {
|
|
15
26
|
name: 'leftYAxis',
|
|
16
|
-
version: '
|
|
17
|
-
description: '左侧Y
|
|
18
|
-
debugName: '左侧Y
|
|
27
|
+
version: '2.0.0',
|
|
28
|
+
description: '左侧Y轴价格刻度渲染器(静态)',
|
|
29
|
+
debugName: '左侧Y轴刻度',
|
|
19
30
|
paneId: GLOBAL_PANE_ID,
|
|
20
31
|
priority: RENDERER_PRIORITY.SYSTEM_YAXIS,
|
|
21
|
-
layer: 'overlay',
|
|
22
32
|
|
|
23
33
|
draw(context: RenderContext) {
|
|
24
34
|
const { leftAxisCtx, pane, dpr, period } = context
|
|
25
35
|
if (!leftAxisCtx) return
|
|
36
|
+
if (!shouldShowLeftAxis(period, context.settings)) return
|
|
37
|
+
if (!pane.capabilities.showPriceAxisTicks) return
|
|
38
|
+
if (!context.yAxisTicks) return
|
|
26
39
|
|
|
27
40
|
const axisWidth = leftAxisCtx.canvas ? leftAxisCtx.canvas.width / dpr : 0
|
|
28
41
|
if (axisWidth <= 0) return
|
|
29
42
|
|
|
30
|
-
// 分时模式始终显示左轴(线性),不受设置约束
|
|
31
|
-
if (period !== 'timeshare') {
|
|
32
|
-
const leftType = context.settings?.leftAxisType as string | undefined
|
|
33
|
-
if (!leftType || leftType === 'none') return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
43
|
const tokenColors = resolveThemeColors(
|
|
37
44
|
context.theme,
|
|
38
45
|
context.isAsiaMarket,
|
|
39
46
|
context.colorPresetSettings,
|
|
40
47
|
)
|
|
41
48
|
|
|
42
|
-
if (!pane.capabilities.showPriceAxisTicks) return
|
|
43
|
-
|
|
44
49
|
const scaleType: ScaleType =
|
|
45
50
|
period === 'timeshare'
|
|
46
51
|
? 'linear'
|
|
47
52
|
: ((context.settings?.leftAxisType as ScaleType) ?? 'linear')
|
|
48
53
|
const paneScaleType = pane.yAxis.getScaleType()
|
|
49
54
|
|
|
50
|
-
if (!context.yAxisTicks) return
|
|
51
|
-
|
|
52
55
|
leftAxisCtx.clearRect(0, 0, axisWidth, pane.height)
|
|
53
56
|
|
|
54
57
|
const font = getFont(12)
|
|
@@ -58,7 +61,6 @@ export function createLeftYAxisRendererPlugin(options: {
|
|
|
58
61
|
leftAxisCtx.fillStyle = tokenColors.text.secondary
|
|
59
62
|
|
|
60
63
|
const textX = roundToPhysicalPixel(axisWidth / 2, dpr)
|
|
61
|
-
|
|
62
64
|
const needsOwnValues = scaleType !== paneScaleType
|
|
63
65
|
const crosshairPriceRange = pane.yAxis.getDisplayRange()
|
|
64
66
|
|
|
@@ -92,14 +94,47 @@ export function createLeftYAxisRendererPlugin(options: {
|
|
|
92
94
|
}
|
|
93
95
|
leftAxisCtx.fillText(formatTick(displayValue), textX, tick.y)
|
|
94
96
|
}
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 左 Y 轴动态层:十字线价签,画到 leftAxisOverlayCtx(overlay 级刷新)
|
|
103
|
+
*/
|
|
104
|
+
export function createLeftYAxisOverlayRendererPlugin(options: LeftYAxisOptions): RendererPlugin {
|
|
105
|
+
return {
|
|
106
|
+
name: 'leftYAxisOverlay',
|
|
107
|
+
version: '2.0.0',
|
|
108
|
+
description: '左侧Y轴动态标签渲染器',
|
|
109
|
+
debugName: '左侧Y轴标签',
|
|
110
|
+
paneId: GLOBAL_PANE_ID,
|
|
111
|
+
priority: RENDERER_PRIORITY.SYSTEM_YAXIS + 1,
|
|
112
|
+
layer: 'overlay',
|
|
113
|
+
|
|
114
|
+
draw(context: RenderContext) {
|
|
115
|
+
const { leftAxisOverlayCtx, leftAxisCtx, pane, dpr, period } = context
|
|
116
|
+
if (!shouldShowLeftAxis(period, context.settings)) return
|
|
117
|
+
|
|
118
|
+
const targetCtx = leftAxisOverlayCtx ?? leftAxisCtx
|
|
119
|
+
if (!targetCtx) return
|
|
120
|
+
|
|
121
|
+
const axisWidth = targetCtx.canvas ? targetCtx.canvas.width / dpr : 0
|
|
122
|
+
if (axisWidth <= 0) return
|
|
123
|
+
|
|
124
|
+
targetCtx.clearRect(0, 0, axisWidth, pane.height)
|
|
95
125
|
|
|
96
126
|
const crosshair = options.getCrosshair?.()
|
|
97
127
|
if (!crosshair || crosshair.activePaneId !== pane.id || crosshair.price === null) return
|
|
98
128
|
|
|
129
|
+
const scaleType: ScaleType =
|
|
130
|
+
period === 'timeshare'
|
|
131
|
+
? 'linear'
|
|
132
|
+
: ((context.settings?.leftAxisType as ScaleType) ?? 'linear')
|
|
99
133
|
const isCrosshairPercent = scaleType === 'percent'
|
|
100
134
|
const crosshairPrice = isCrosshairPercent
|
|
101
135
|
? pane.yAxis.toPercent(crosshair.price)
|
|
102
136
|
: crosshair.price
|
|
137
|
+
const crosshairPriceRange = pane.yAxis.getDisplayRange()
|
|
103
138
|
const crosshairLabelRange: { minPrice: number; maxPrice: number } = isCrosshairPercent
|
|
104
139
|
? (() => {
|
|
105
140
|
const p = pane.yAxis.getDisplayPercentRange()
|
|
@@ -114,7 +149,7 @@ export function createLeftYAxisRendererPlugin(options: {
|
|
|
114
149
|
: undefined
|
|
115
150
|
|
|
116
151
|
drawCrosshairPriceLabel(
|
|
117
|
-
|
|
152
|
+
targetCtx,
|
|
118
153
|
{
|
|
119
154
|
x: 0,
|
|
120
155
|
y: pane.top,
|
|
@@ -136,3 +171,10 @@ export function createLeftYAxisRendererPlugin(options: {
|
|
|
136
171
|
},
|
|
137
172
|
}
|
|
138
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @deprecated 使用 createLeftYAxisStaticRendererPlugin + createLeftYAxisOverlayRendererPlugin
|
|
177
|
+
*/
|
|
178
|
+
export function createLeftYAxisRendererPlugin(options: LeftYAxisOptions): RendererPlugin {
|
|
179
|
+
return createLeftYAxisStaticRendererPlugin(options)
|
|
180
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
import type { RendererPlugin, RenderContext } from '../../foundation/plugin/index'
|
|
2
2
|
import { RENDERER_PRIORITY, GLOBAL_PANE_ID } from '../../foundation/plugin/index'
|
|
3
3
|
import { resolveThemeColors } from '../../foundation/tokens/index'
|
|
4
|
-
import type { KLineData } from '../../foundation/types/price'
|
|
5
4
|
import { drawCrosshairPriceLabel, drawAxisPriceLabel } from '../../foundation/utils/kLineDraw/axis'
|
|
6
5
|
import { roundToPhysicalPixel } from '../../foundation/utils/pixelAlign'
|
|
7
6
|
import { getFont, setCanvasFont } from '../../foundation/tokens/fonts'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
* 创建 Y 轴渲染器插件
|
|
11
|
-
* 按 pane capability 决定是否绘制刻度与价格标签
|
|
12
|
-
*/
|
|
13
|
-
export function createYAxisRendererPlugin(options: {
|
|
8
|
+
type YAxisOptions = {
|
|
14
9
|
axisWidth: number
|
|
15
10
|
yPaddingPx: number
|
|
16
11
|
getCrosshair?: () => { y: number; price: number; activePaneId: string | null } | null
|
|
17
|
-
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function shouldShowRightAxis(period: string, settings: RenderContext['settings']): boolean {
|
|
15
|
+
if (period === 'timeshare') return true
|
|
16
|
+
const rightType = settings?.rightAxisType as string | undefined
|
|
17
|
+
return rightType !== 'none'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Y 轴静态层:刻度 + 价格范围带,画到 yAxisCtx(main 级刷新)
|
|
22
|
+
*/
|
|
23
|
+
export function createYAxisStaticRendererPlugin(options: YAxisOptions): RendererPlugin {
|
|
18
24
|
return {
|
|
19
25
|
name: 'yAxis',
|
|
20
|
-
version: '
|
|
21
|
-
description: 'Y
|
|
22
|
-
debugName: 'Y
|
|
26
|
+
version: '2.0.0',
|
|
27
|
+
description: 'Y轴价格刻度渲染器(静态)',
|
|
28
|
+
debugName: 'Y轴刻度',
|
|
23
29
|
paneId: GLOBAL_PANE_ID,
|
|
24
30
|
priority: RENDERER_PRIORITY.SYSTEM_YAXIS,
|
|
25
|
-
layer: 'overlay',
|
|
26
31
|
|
|
27
32
|
draw(context: RenderContext) {
|
|
28
|
-
const { ctx, pane, dpr, yAxisCtx,
|
|
29
|
-
|
|
30
|
-
// 分时模式始终显示右轴,不受设置约束
|
|
31
|
-
if (period !== 'timeshare') {
|
|
32
|
-
const rightType = context.settings?.rightAxisType as string | undefined
|
|
33
|
-
if (rightType === 'none') return
|
|
34
|
-
}
|
|
33
|
+
const { ctx, pane, dpr, yAxisCtx, period } = context
|
|
34
|
+
if (!shouldShowRightAxis(period, context.settings)) return
|
|
35
35
|
|
|
36
36
|
const tokenColors = resolveThemeColors(
|
|
37
37
|
context.theme,
|
|
@@ -39,11 +39,8 @@ export function createYAxisRendererPlugin(options: {
|
|
|
39
39
|
context.colorPresetSettings,
|
|
40
40
|
)
|
|
41
41
|
const scaleType = pane.yAxis.getScaleType()
|
|
42
|
-
|
|
43
42
|
const targetCtx = yAxisCtx || ctx
|
|
44
43
|
const axisWidth = yAxisCtx?.canvas ? yAxisCtx.canvas.width / dpr : options.axisWidth
|
|
45
|
-
const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
|
|
46
|
-
|
|
47
44
|
const isPercent = scaleType === 'percent' && pane.role === 'price'
|
|
48
45
|
|
|
49
46
|
if (pane.capabilities.showPriceAxisTicks && context.yAxisTicks) {
|
|
@@ -70,7 +67,7 @@ export function createYAxisRendererPlugin(options: {
|
|
|
70
67
|
}
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
//
|
|
70
|
+
// 价格范围带(先于标签,使标签覆盖在范围带之上)
|
|
74
71
|
if (context.yAxisRanges && pane.role === 'price') {
|
|
75
72
|
for (const range of context.yAxisRanges) {
|
|
76
73
|
const topY = range.topY + pane.top
|
|
@@ -83,6 +80,36 @@ export function createYAxisRendererPlugin(options: {
|
|
|
83
80
|
targetCtx.restore()
|
|
84
81
|
}
|
|
85
82
|
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Y 轴动态层:yAxisLabels + 十字线价签,画到 yAxisOverlayCtx(overlay 级刷新)
|
|
89
|
+
*/
|
|
90
|
+
export function createYAxisOverlayRendererPlugin(options: YAxisOptions): RendererPlugin {
|
|
91
|
+
return {
|
|
92
|
+
name: 'yAxisOverlay',
|
|
93
|
+
version: '2.0.0',
|
|
94
|
+
description: 'Y轴动态标签渲染器',
|
|
95
|
+
debugName: 'Y轴标签',
|
|
96
|
+
paneId: GLOBAL_PANE_ID,
|
|
97
|
+
priority: RENDERER_PRIORITY.SYSTEM_YAXIS + 1,
|
|
98
|
+
layer: 'overlay',
|
|
99
|
+
|
|
100
|
+
draw(context: RenderContext) {
|
|
101
|
+
const { pane, dpr, yAxisOverlayCtx, yAxisCtx, period } = context
|
|
102
|
+
if (!shouldShowRightAxis(period, context.settings)) return
|
|
103
|
+
|
|
104
|
+
const targetCtx = yAxisOverlayCtx ?? yAxisCtx
|
|
105
|
+
if (!targetCtx) return
|
|
106
|
+
|
|
107
|
+
const axisWidth = targetCtx.canvas ? targetCtx.canvas.width / dpr : options.axisWidth
|
|
108
|
+
targetCtx.clearRect(0, 0, axisWidth, pane.height)
|
|
109
|
+
|
|
110
|
+
const scaleType = pane.yAxis.getScaleType()
|
|
111
|
+
const displayRange = pane.yAxis.getDisplayRange(pane.priceRange)
|
|
112
|
+
const isPercent = scaleType === 'percent' && pane.role === 'price'
|
|
86
113
|
|
|
87
114
|
// 绘制来自 yAxisLabels 的标签(最新价格、极值点、绘图锚点等)
|
|
88
115
|
if (context.yAxisLabels && pane.role === 'price') {
|
|
@@ -151,3 +178,11 @@ export function createYAxisRendererPlugin(options: {
|
|
|
151
178
|
},
|
|
152
179
|
}
|
|
153
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @deprecated 使用 createYAxisStaticRendererPlugin + createYAxisOverlayRendererPlugin
|
|
184
|
+
* 保留兼容:静态+动态合画到 yAxisCtx
|
|
185
|
+
*/
|
|
186
|
+
export function createYAxisRendererPlugin(options: YAxisOptions): RendererPlugin {
|
|
187
|
+
return createYAxisStaticRendererPlugin(options)
|
|
188
|
+
}
|