@363045841yyt/klinechart-core 0.9.0-alpha.2 → 0.9.0-alpha.4
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/data/timeShareBuffer.d.ts +3 -0
- package/dist/data/timeShareBuffer.d.ts.map +1 -1
- package/dist/data/timeShareBuffer.js +13 -0
- package/dist/data/timeShareBuffer.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 +87 -101
- 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 +14 -18
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +55 -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/modes/timeShareMath.d.ts +56 -0
- package/dist/engine/modes/timeShareMath.d.ts.map +1 -0
- package/dist/engine/modes/timeShareMath.js +90 -0
- package/dist/engine/modes/timeShareMath.js.map +1 -0
- package/dist/engine/modes/timeShareMode.d.ts +5 -0
- package/dist/engine/modes/timeShareMode.d.ts.map +1 -1
- package/dist/engine/modes/timeShareMode.js +27 -27
- package/dist/engine/modes/timeShareMode.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 +143 -85
- 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 +73 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.d.ts.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegendContext.js +182 -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/timeShare.d.ts.map +1 -1
- package/dist/engine/renderers/timeShare.js +20 -9
- package/dist/engine/renderers/timeShare.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/config/chartSettings.d.ts +0 -5
- package/dist/foundation/config/chartSettings.d.ts.map +1 -1
- package/dist/foundation/config/chartSettings.js +14 -0
- package/dist/foundation/config/chartSettings.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/foundation/utils/kLineDraw/axis.d.ts +3 -0
- package/dist/foundation/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/foundation/utils/kLineDraw/axis.js +40 -9
- package/dist/foundation/utils/kLineDraw/axis.js.map +1 -1
- package/dist/foundation/utils/sessionTimeLabels.d.ts +59 -0
- package/dist/foundation/utils/sessionTimeLabels.d.ts.map +1 -0
- package/dist/foundation/utils/sessionTimeLabels.js +170 -0
- package/dist/foundation/utils/sessionTimeLabels.js.map +1 -0
- package/dist/foundation/utils/timeShareAxisLabels.d.ts +27 -0
- package/dist/foundation/utils/timeShareAxisLabels.d.ts.map +1 -0
- package/dist/foundation/utils/timeShareAxisLabels.js +45 -0
- package/dist/foundation/utils/timeShareAxisLabels.js.map +1 -0
- 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/__tests__/timeShareBuffer.test.ts +45 -0
- package/src/data/binance.ts +1 -1
- package/src/data/timeShareBuffer.ts +15 -0
- package/src/engine/__tests__/chart.dpr.test.ts +31 -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 +105 -115
- 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 +64 -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/modes/__tests__/timeShareMath.test.ts +185 -0
- package/src/engine/modes/__tests__/timeShareMode.test.ts +81 -0
- package/src/engine/modes/timeShareMath.ts +150 -0
- package/src/engine/modes/timeShareMode.ts +35 -28
- package/src/engine/paneRenderer.ts +17 -10
- package/src/engine/render/chartRenderer.ts +214 -145
- 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 +290 -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/timeShare.ts +25 -9
- package/src/engine/renderers/yAxis.ts +57 -22
- package/src/engine/state/__tests__/settingsState.test.ts +9 -0
- 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/config/chartSettings.ts +13 -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/foundation/utils/__tests__/sessionTimeLabels.test.ts +91 -0
- package/src/foundation/utils/kLineDraw/axis.ts +50 -9
- package/src/foundation/utils/sessionTimeLabels.ts +237 -0
- package/src/foundation/utils/timeShareAxisLabels.ts +94 -0
- 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,290 @@
|
|
|
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 LegendCurrentBar = Omit<KLineData, 'volume'> & {
|
|
21
|
+
volume: number | null
|
|
22
|
+
// 成交量+单位格式化文本(eg. 1.23亿)
|
|
23
|
+
volumeText: string | null
|
|
24
|
+
color: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LegendTimeshareRow {
|
|
28
|
+
price: number
|
|
29
|
+
average: number
|
|
30
|
+
changeAmount: number
|
|
31
|
+
changePercent: number
|
|
32
|
+
volume: number
|
|
33
|
+
volumeText: string
|
|
34
|
+
amount: number
|
|
35
|
+
amountText: string
|
|
36
|
+
changeColor: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LegendIndicatorRow {
|
|
40
|
+
name: string
|
|
41
|
+
params?: number[]
|
|
42
|
+
values?: TitleValueItem[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LegendComparisonRow {
|
|
46
|
+
symbol: string
|
|
47
|
+
percent: number
|
|
48
|
+
color: string
|
|
49
|
+
percentColor: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 主图左上角图例完整上下文。
|
|
54
|
+
* Canvas 绘制与 Vue legend slot 共用同一份数据。
|
|
55
|
+
*/
|
|
56
|
+
export interface LegendTemplateContext {
|
|
57
|
+
period: string
|
|
58
|
+
index: number
|
|
59
|
+
hasCrosshair: boolean
|
|
60
|
+
layout: LegendLayout
|
|
61
|
+
colors: {
|
|
62
|
+
textPrimary: string
|
|
63
|
+
textTertiary: string
|
|
64
|
+
up: string
|
|
65
|
+
down: string
|
|
66
|
+
}
|
|
67
|
+
/** 十字线指向的当前 K 线展示行(含 volumeText / color 与自定义字段) */
|
|
68
|
+
currentBar: LegendCurrentBar | null
|
|
69
|
+
timeshare: LegendTimeshareRow | null
|
|
70
|
+
indicators: ReadonlyArray<LegendIndicatorRow>
|
|
71
|
+
comparisons: ReadonlyArray<LegendComparisonRow>
|
|
72
|
+
/** 当前索引处的原始 K 线(分时模式下可能无 close) */
|
|
73
|
+
bar: KLineData | TimeShareData | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BuildLegendTemplateContextInput {
|
|
77
|
+
context: RenderContext
|
|
78
|
+
host: PluginHost | null
|
|
79
|
+
yPaddingPx: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatVolumeShort(v: number): string {
|
|
83
|
+
if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
|
|
84
|
+
if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
|
|
85
|
+
return v.toFixed(2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatAmountShort(v: number): string {
|
|
89
|
+
if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
|
|
90
|
+
if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
|
|
91
|
+
return v.toFixed(2)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildLegendTemplateContext(
|
|
95
|
+
input: BuildLegendTemplateContextInput,
|
|
96
|
+
): LegendTemplateContext | null {
|
|
97
|
+
const { context, host, yPaddingPx } = input
|
|
98
|
+
const klineData = context.data as KLineData[]
|
|
99
|
+
if (!klineData.length) return null
|
|
100
|
+
|
|
101
|
+
const colors = resolveThemeColors(
|
|
102
|
+
context.theme,
|
|
103
|
+
context.isAsiaMarket,
|
|
104
|
+
context.colorPresetSettings,
|
|
105
|
+
)
|
|
106
|
+
const fontSize = 12
|
|
107
|
+
const lineHeight = fontSize + 6
|
|
108
|
+
const legendX = 12
|
|
109
|
+
const gap = 10
|
|
110
|
+
const legendYOffset = 6
|
|
111
|
+
const compact = context.paneWidth < 400
|
|
112
|
+
const range = context.range
|
|
113
|
+
const crosshairIndex = context.crosshairIndex
|
|
114
|
+
const hasCrosshair = typeof crosshairIndex === 'number'
|
|
115
|
+
const targetIndex = hasCrosshair ? crosshairIndex : 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 rawPreClose = context.settings?.preClose as number | undefined
|
|
130
|
+
const preClose =
|
|
131
|
+
typeof rawPreClose === 'number' && Number.isFinite(rawPreClose) && rawPreClose !== 0
|
|
132
|
+
? rawPreClose
|
|
133
|
+
: (tsData[0]?.price ?? 0)
|
|
134
|
+
const item = tsData[targetIndex]
|
|
135
|
+
if (item) {
|
|
136
|
+
const changeAmount = item.price - preClose
|
|
137
|
+
const changePercent = preClose !== 0 ? (changeAmount / preClose) * 100 : 0
|
|
138
|
+
timeshare = {
|
|
139
|
+
price: item.price,
|
|
140
|
+
average: item.average,
|
|
141
|
+
changeAmount,
|
|
142
|
+
changePercent,
|
|
143
|
+
volume: item.volume,
|
|
144
|
+
volumeText: formatVolumeShort(item.volume),
|
|
145
|
+
amount: item.amount,
|
|
146
|
+
amountText: formatAmountShort(item.amount),
|
|
147
|
+
changeColor: changeAmount >= 0 ? colors.candleUpBody : colors.candleDownBody,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let currentBar: LegendCurrentBar | null = null
|
|
153
|
+
if (hasCrosshair) {
|
|
154
|
+
const k = klineData[targetIndex]
|
|
155
|
+
if (k && typeof k.close === 'number') {
|
|
156
|
+
const isUp = k.close >= k.open
|
|
157
|
+
currentBar = {
|
|
158
|
+
...k,
|
|
159
|
+
volume: typeof k.volume === 'number' ? k.volume : null,
|
|
160
|
+
volumeText: typeof k.volume === 'number' ? formatVolumeShort(k.volume) : null,
|
|
161
|
+
color: isUp ? colors.candleUpBody : colors.candleDownBody,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const indicators = collectIndicatorRows(host, klineData, targetIndex)
|
|
167
|
+
const comparisons = collectComparisonRows(context, klineData, targetIndex, range, colors)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
period: context.period,
|
|
171
|
+
index: targetIndex,
|
|
172
|
+
hasCrosshair,
|
|
173
|
+
layout,
|
|
174
|
+
colors: {
|
|
175
|
+
textPrimary: colors.text.primary,
|
|
176
|
+
textTertiary: colors.text.tertiary,
|
|
177
|
+
up: colors.candleUpBody,
|
|
178
|
+
down: colors.candleDownBody,
|
|
179
|
+
},
|
|
180
|
+
currentBar,
|
|
181
|
+
timeshare,
|
|
182
|
+
indicators,
|
|
183
|
+
comparisons,
|
|
184
|
+
bar: klineData[targetIndex] ?? null,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function collectIndicatorRows(
|
|
189
|
+
host: PluginHost | null,
|
|
190
|
+
klineData: KLineData[],
|
|
191
|
+
targetIndex: number,
|
|
192
|
+
): LegendIndicatorRow[] {
|
|
193
|
+
if (!host || typeof host.getService !== 'function') return []
|
|
194
|
+
const scheduler = host.getService<IndicatorScheduler>('indicatorScheduler')
|
|
195
|
+
if (!scheduler) return []
|
|
196
|
+
|
|
197
|
+
const rows: LegendIndicatorRow[] = []
|
|
198
|
+
for (const meta of scheduler.getMainIndicators()) {
|
|
199
|
+
if (!meta.getTitleInfo) continue
|
|
200
|
+
if (!scheduler.isMainIndicatorActive(meta.name)) continue
|
|
201
|
+
const params = scheduler.getMainIndicatorParams(meta.name) ?? {}
|
|
202
|
+
const titleInfo: TitleInfo | null = meta.getTitleInfo(
|
|
203
|
+
klineData,
|
|
204
|
+
targetIndex,
|
|
205
|
+
params as Record<string, number | boolean | string>,
|
|
206
|
+
host,
|
|
207
|
+
'main',
|
|
208
|
+
)
|
|
209
|
+
if (!titleInfo) continue
|
|
210
|
+
rows.push({
|
|
211
|
+
name: titleInfo.name,
|
|
212
|
+
params: titleInfo.params,
|
|
213
|
+
values: titleInfo.values,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
return rows
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectComparisonRows(
|
|
220
|
+
context: RenderContext,
|
|
221
|
+
klineData: KLineData[],
|
|
222
|
+
targetIndex: number,
|
|
223
|
+
range: { start: number; end: number },
|
|
224
|
+
colors: ReturnType<typeof resolveThemeColors>,
|
|
225
|
+
): LegendComparisonRow[] {
|
|
226
|
+
const comparisonSymbols = context.comparisonSymbols
|
|
227
|
+
const comparisonData = context.comparisonData
|
|
228
|
+
const comparisonColors = context.comparisonColors
|
|
229
|
+
if (!comparisonSymbols?.length || !comparisonData?.size) return []
|
|
230
|
+
|
|
231
|
+
const baseIndex = Math.max(0, range.start)
|
|
232
|
+
const baseItem = klineData[baseIndex]
|
|
233
|
+
if (!baseItem || !Number.isFinite(baseItem.close) || baseItem.close <= 0) return []
|
|
234
|
+
|
|
235
|
+
const baseDate = baseItem.date ?? ''
|
|
236
|
+
const rows: LegendComparisonRow[] = []
|
|
237
|
+
|
|
238
|
+
for (const spec of comparisonSymbols) {
|
|
239
|
+
const data = comparisonData.get(spec.symbol)
|
|
240
|
+
if (!data?.length) continue
|
|
241
|
+
|
|
242
|
+
const baseline = baseDate
|
|
243
|
+
? findBaselineByDate(data, baseDate)
|
|
244
|
+
: findBaselineByTimestamp(data, baseItem.timestamp)
|
|
245
|
+
if (!baseline || baseline.close <= 0) continue
|
|
246
|
+
|
|
247
|
+
const byDate = new Map<string, KLineData>()
|
|
248
|
+
for (const item of data) {
|
|
249
|
+
byDate.set(item.date ?? String(item.timestamp), item)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const mainItem = klineData[targetIndex]
|
|
253
|
+
if (!mainItem) continue
|
|
254
|
+
const key = mainItem.date ?? String(mainItem.timestamp)
|
|
255
|
+
const currentItem = byDate.get(key)
|
|
256
|
+
if (!currentItem || !Number.isFinite(currentItem.close)) continue
|
|
257
|
+
|
|
258
|
+
const percent = ((currentItem.close - baseline.close) / baseline.close) * 100
|
|
259
|
+
const color = comparisonColors?.get(spec.symbol) ?? '#f59e0b'
|
|
260
|
+
rows.push({
|
|
261
|
+
symbol: spec.symbol,
|
|
262
|
+
percent,
|
|
263
|
+
color,
|
|
264
|
+
percentColor:
|
|
265
|
+
percent > 0
|
|
266
|
+
? colors.candleUpBody
|
|
267
|
+
: percent < 0
|
|
268
|
+
? colors.candleDownBody
|
|
269
|
+
: colors.text.primary,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
return rows
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findBaselineByDate(data: ReadonlyArray<KLineData>, date: string): KLineData | null {
|
|
276
|
+
for (const item of data) {
|
|
277
|
+
if (item.date && item.date >= date) return item
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findBaselineByTimestamp(
|
|
283
|
+
data: ReadonlyArray<KLineData>,
|
|
284
|
+
timestamp: number,
|
|
285
|
+
): KLineData | null {
|
|
286
|
+
for (const item of data) {
|
|
287
|
+
if (item.timestamp >= timestamp) return item
|
|
288
|
+
}
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
@@ -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.currentBar).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 currentBar 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.currentBar.turnoverRate).toBe(3.14)
|
|
623
|
+
expect(legend.currentBar.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
|
+
}
|
|
@@ -8,6 +8,10 @@ import { RENDERER_PRIORITY } from '../../foundation/plugin/index'
|
|
|
8
8
|
import { resolveThemeColors } from '../../foundation/tokens/index'
|
|
9
9
|
import type { TimeShareData } from '../../foundation/types/price'
|
|
10
10
|
import { Indicator } from '../indicators/indicatorDefinitionRegistry'
|
|
11
|
+
import {
|
|
12
|
+
computeTimeSharePaneLayout,
|
|
13
|
+
resolveTimeShareBaseline,
|
|
14
|
+
} from '../modes/timeShareMath'
|
|
11
15
|
|
|
12
16
|
/** 成交量区域占 pane 高度的比例(底部) */
|
|
13
17
|
const VOLUME_RATIO = 0.25
|
|
@@ -34,15 +38,17 @@ export function createTimeShareRendererPlugin(): RendererPluginWithHost {
|
|
|
34
38
|
context.isAsiaMarket,
|
|
35
39
|
context.colorPresetSettings,
|
|
36
40
|
)
|
|
37
|
-
const preClose = (
|
|
38
|
-
|
|
41
|
+
const preClose = resolveTimeShareBaseline({
|
|
42
|
+
preClose: settings?.preClose as number | undefined,
|
|
43
|
+
firstPrice: tsData[0]?.price,
|
|
44
|
+
})
|
|
45
|
+
if (preClose === null) return
|
|
39
46
|
|
|
40
47
|
const paneHeight = pane.height
|
|
41
|
-
const
|
|
42
|
-
const priceAreaHeight =
|
|
48
|
+
const layout = computeTimeSharePaneLayout(paneHeight, VOLUME_RATIO)
|
|
49
|
+
const { volumeAreaHeight, priceAreaHeight } = layout
|
|
43
50
|
|
|
44
51
|
const { start, end } = range
|
|
45
|
-
const visibleCount = Math.min(end - start, tsData.length - start)
|
|
46
52
|
const itemCount = Math.min(end, tsData.length) - start
|
|
47
53
|
|
|
48
54
|
const xPositions: number[] = []
|
|
@@ -51,16 +57,19 @@ export function createTimeShareRendererPlugin(): RendererPluginWithHost {
|
|
|
51
57
|
const volumes: number[] = []
|
|
52
58
|
let maxVolume = 0
|
|
53
59
|
|
|
60
|
+
// 价格 Y 映射到 price 子区域:pane.yAxis 仍按全高,再线性压到 priceAreaHeight
|
|
61
|
+
const scaleYToPriceArea = (y: number) => (y / Math.max(paneHeight, 1)) * priceAreaHeight
|
|
62
|
+
|
|
54
63
|
for (let i = start; i < start + itemCount; i++) {
|
|
55
64
|
const item = tsData[i]
|
|
56
65
|
if (!item) continue
|
|
57
66
|
const x = kLineCenters[i - start]
|
|
58
67
|
if (x === undefined) continue
|
|
59
68
|
xPositions.push(x)
|
|
60
|
-
yPrices.push(pane.yAxis.priceToY(item.price))
|
|
61
|
-
yAvgs.push(pane.yAxis.priceToY(item.average))
|
|
69
|
+
yPrices.push(scaleYToPriceArea(pane.yAxis.priceToY(item.price)))
|
|
70
|
+
yAvgs.push(scaleYToPriceArea(pane.yAxis.priceToY(item.average)))
|
|
62
71
|
volumes.push(item.volume ?? 0)
|
|
63
|
-
maxVolume = Math.max(maxVolume, item.volume)
|
|
72
|
+
maxVolume = Math.max(maxVolume, item.volume ?? 0)
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
if (xPositions.length < 2) return
|
|
@@ -68,7 +77,13 @@ export function createTimeShareRendererPlugin(): RendererPluginWithHost {
|
|
|
68
77
|
ctx.save()
|
|
69
78
|
ctx.translate(-scrollLeft, 0)
|
|
70
79
|
|
|
71
|
-
const preCloseY = pane.yAxis.priceToY(preClose)
|
|
80
|
+
const preCloseY = scaleYToPriceArea(pane.yAxis.priceToY(preClose))
|
|
81
|
+
|
|
82
|
+
// 价格区裁剪,避免线/填充画进成交量区(translate 后世界 scrollLeft 映射到 0)
|
|
83
|
+
ctx.save()
|
|
84
|
+
ctx.beginPath()
|
|
85
|
+
ctx.rect(scrollLeft, 0, context.paneWidth, priceAreaHeight)
|
|
86
|
+
ctx.clip()
|
|
72
87
|
|
|
73
88
|
drawPreCloseLine(ctx, xPositions, preCloseY, dpr, colors.timeSharePreClose)
|
|
74
89
|
|
|
@@ -85,6 +100,7 @@ export function createTimeShareRendererPlugin(): RendererPluginWithHost {
|
|
|
85
100
|
drawSegmentLine(ctx, xPositions, yPrices, dpr, colors.timeSharePriceLine, 2)
|
|
86
101
|
|
|
87
102
|
drawSegmentLine(ctx, xPositions, yAvgs, dpr, colors.timeShareAvgLine, 1.5)
|
|
103
|
+
ctx.restore()
|
|
88
104
|
|
|
89
105
|
drawVolumeBars(
|
|
90
106
|
ctx,
|