@@ -32,7 +32,143 @@ interface Offsets {
32
32
belowBar : number ;
33
33
}
34
34
35
+ // Helper function to calculate marker size more precisely
36
+ function calculateMarkerSize ( barSpacing : number , sizeMultiplier : number ) : number {
37
+ // Base size from the standard calculation
38
+ const baseSize = calculateShapeHeight ( barSpacing ) ;
39
+
40
+ // If size multiplier is 1, return the standard size
41
+ if ( sizeMultiplier === 1 ) {
42
+ return baseSize ;
43
+ }
44
+
45
+ // For other size multipliers, scale more aggressively
46
+ // This allows for more noticeable size differences
47
+ if ( sizeMultiplier < 1 ) {
48
+ // For smaller sizes, scale down more aggressively
49
+ return Math . max ( baseSize * ( 0.5 + 0.5 * sizeMultiplier ) , 6 ) ;
50
+ } else {
51
+ // For larger sizes, scale up more aggressively
52
+ return baseSize * sizeMultiplier ;
53
+ }
54
+ }
55
+
56
+ // Helper function to handle price-based positioning
57
+ /* eslint-disable */
58
+ function positionMarkerAtPrice < HorzScaleItem > (
59
+ rendererItem : SeriesMarkerRendererDataItem ,
60
+ marker : InternalSeriesMarker < TimePointIndex > ,
61
+ y : number | null ,
62
+ series : ISeriesApi < SeriesType , HorzScaleItem > ,
63
+ textHeight : number ,
64
+ shapeMargin : number ,
65
+ chart ?: IChartApiBase < HorzScaleItem >
66
+ ) : boolean {
67
+ // If we don't have a price, we can't position properly
68
+ if ( marker . price === undefined ) {
69
+ return false ;
70
+ }
71
+
72
+ // ALWAYS get fresh coordinate for the marker price
73
+ // This ensures we use the latest price scale, matching how price lines work
74
+ const latestYCoordinate = series . priceToCoordinate ( marker . price ) ;
75
+ if ( latestYCoordinate === null ) {
76
+ return false ;
77
+ }
78
+
79
+ // Use the latest coordinate instead of the cached value
80
+ y = latestYCoordinate ;
81
+
82
+ // Get the timeScale from chart to ensure we use the exact same barSpacing
83
+ const timeScale = chart ?. timeScale ( ) ;
84
+ if ( ! timeScale ) {
85
+ return false ;
86
+ }
87
+
88
+ // Use EXACTLY the same size calculation as in fillSizeAndY
89
+ const sizeMultiplier = isNumber ( marker . size ) ? Math . max ( marker . size , 0 ) : 1 ;
90
+ const barSpacing = timeScale . options ( ) . barSpacing ;
91
+ // Use our new more precise size calculation
92
+ const shapeSize = calculateMarkerSize ( barSpacing , sizeMultiplier ) ;
93
+
94
+ const halfSize = shapeSize / 2 ;
95
+ rendererItem . size = shapeSize ;
96
+
97
+ // Position the marker based on its position property and price
98
+ switch ( marker . position ) {
99
+ case 'atPriceTop' :
100
+ // Position marker so its bottom edge is exactly at the price level
101
+ rendererItem . y = ( y - halfSize ) as Coordinate ;
102
+ if ( rendererItem . text !== undefined ) {
103
+ // Position text above marker using original logic
104
+ rendererItem . text . y = ( rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
105
+ }
106
+ break ;
107
+ case 'atPriceBottom' :
108
+ // Position marker so its top edge is exactly at the price level
109
+ rendererItem . y = ( y + halfSize ) as Coordinate ;
110
+ if ( rendererItem . text !== undefined ) {
111
+ // Position text below marker using original logic
112
+ rendererItem . text . y = ( rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
113
+ }
114
+ break ;
115
+ case 'atPriceMiddle' :
116
+ // Position marker centered at the price level
117
+ rendererItem . y = y as Coordinate ;
118
+ if ( rendererItem . text !== undefined ) {
119
+ // Position text above marker using original logic
120
+ rendererItem . text . y = ( rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
121
+ }
122
+ break ;
123
+ default :
124
+ // For other positions, we'll handle them in the original way
125
+ return false ;
126
+ }
127
+
128
+ return true ;
129
+ }
130
+ /* eslint-enable */
131
+
132
+
133
+
134
+ // Helper function to position marker above bar
135
+ function positionAboveBar < HorzScaleItem > (
136
+ series : ISeriesApi < SeriesType , HorzScaleItem > ,
137
+ highPrice : number ,
138
+ halfSize : number ,
139
+ offsets : Offsets ,
140
+ shapeSize : number ,
141
+ shapeMargin : number
142
+ ) : Coordinate | null {
143
+ const topCoordinate = series . priceToCoordinate ( highPrice ) ;
144
+ if ( topCoordinate === null ) {
145
+ return null ;
146
+ }
147
+ const y = topCoordinate - halfSize - offsets . aboveBar as Coordinate ;
148
+ offsets . aboveBar += shapeSize + shapeMargin ;
149
+ return y ;
150
+ }
151
+
152
+ // Helper function to position marker below bar
153
+ function positionBelowBar < HorzScaleItem > (
154
+ series : ISeriesApi < SeriesType , HorzScaleItem > ,
155
+ lowPrice : number ,
156
+ halfSize : number ,
157
+ offsets : Offsets ,
158
+ shapeSize : number ,
159
+ shapeMargin : number
160
+ ) : Coordinate | null {
161
+ const bottomCoordinate = series . priceToCoordinate ( lowPrice ) ;
162
+ if ( bottomCoordinate === null ) {
163
+ return null ;
164
+ }
165
+ const y = bottomCoordinate + halfSize + offsets . belowBar as Coordinate ;
166
+ offsets . belowBar += shapeSize + shapeMargin ;
167
+ return y ;
168
+ }
169
+
35
170
// eslint-disable-next-line max-params
171
+ /* eslint-disable */
36
172
function fillSizeAndY < HorzScaleItem > (
37
173
rendererItem : SeriesMarkerRendererDataItem ,
38
174
marker : InternalSeriesMarker < TimePointIndex > ,
@@ -43,6 +179,21 @@ function fillSizeAndY<HorzScaleItem>(
43
179
series : ISeriesApi < SeriesType , HorzScaleItem > ,
44
180
chart : IChartApiBase < HorzScaleItem >
45
181
) : void {
182
+ // If marker has a price property AND is using one of the atPrice* positions,
183
+ // prioritize exact price positioning
184
+ if ( marker . price !== undefined &&
185
+ ( marker . position === 'atPriceTop' ||
186
+ marker . position === 'atPriceBottom' ||
187
+ marker . position === 'atPriceMiddle' ) ) {
188
+ // We don't need to pass the y coordinate - positionMarkerAtPrice will get it fresh
189
+ if ( positionMarkerAtPrice ( rendererItem , marker , null , series , textHeight , shapeMargin , chart ) ) {
190
+ // Successfully positioned using price, we're done
191
+ return ;
192
+ }
193
+ // Only if price-to-coordinate conversion fails, fall back to bar positioning
194
+ }
195
+
196
+ // Original positioning logic
46
197
const timeScale = chart . timeScale ( ) ;
47
198
let inBarPrice : number ;
48
199
let highPrice : number ;
@@ -61,39 +212,100 @@ function fillSizeAndY<HorzScaleItem>(
61
212
}
62
213
63
214
const sizeMultiplier = isNumber ( marker . size ) ? Math . max ( marker . size , 0 ) : 1 ;
64
- const shapeSize = calculateShapeHeight ( timeScale . options ( ) . barSpacing ) * sizeMultiplier ;
215
+ const barSpacing = timeScale . options ( ) . barSpacing ;
216
+
217
+
218
+ // Use our new more precise size calculation
219
+ const shapeSize = calculateMarkerSize ( barSpacing , sizeMultiplier ) ;
220
+
65
221
const halfSize = shapeSize / 2 ;
66
222
rendererItem . size = shapeSize ;
223
+
224
+ let y : Coordinate | null = null ;
225
+
67
226
switch ( marker . position ) {
68
227
case 'inBar' : {
69
228
rendererItem . y = ensureNotNull ( series . priceToCoordinate ( inBarPrice ) ) ;
70
229
if ( rendererItem . text !== undefined ) {
71
- rendererItem . text . y = rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) as Coordinate ;
230
+ rendererItem . text . y = ( rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
72
231
}
73
232
return ;
74
233
}
75
234
case 'aboveBar' : {
76
- rendererItem . y = ( ensureNotNull ( series . priceToCoordinate ( highPrice ) ) - halfSize - offsets . aboveBar ) as Coordinate ;
235
+ y = positionAboveBar ( series , highPrice , halfSize , offsets , shapeSize , shapeMargin ) ;
236
+ if ( y === null ) return ;
237
+ rendererItem . y = y ;
238
+
77
239
if ( rendererItem . text !== undefined ) {
78
- rendererItem . text . y = rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) as Coordinate ;
240
+ rendererItem . text . y = ( rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
79
241
offsets . aboveBar += textHeight * ( 1 + 2 * Constants . TextMargin ) ;
80
242
}
81
- offsets . aboveBar += shapeSize + shapeMargin ;
82
243
return ;
83
244
}
84
245
case 'belowBar' : {
85
- rendererItem . y = ( ensureNotNull ( series . priceToCoordinate ( lowPrice ) ) + halfSize + offsets . belowBar ) as Coordinate ;
246
+ y = positionBelowBar ( series , lowPrice , halfSize , offsets , shapeSize , shapeMargin ) ;
247
+ if ( y === null ) return ;
248
+ rendererItem . y = y ;
249
+
250
+ if ( rendererItem . text !== undefined ) {
251
+ rendererItem . text . y = ( rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
252
+ offsets . belowBar += textHeight * ( 1 + 2 * Constants . TextMargin ) ;
253
+ }
254
+ return ;
255
+ }
256
+ // Handle the price-based positions as a fallback if they didn't have a price attribute
257
+ case 'atPriceTop' : {
258
+ if ( marker . price !== undefined ) {
259
+ // This should have been handled by positionMarkerAtPrice
260
+ // If we're here, it means the price coordinate conversion failed
261
+ return ;
262
+ }
263
+ // Fallback to aboveBar behavior
264
+ y = positionAboveBar ( series , highPrice , halfSize , offsets , shapeSize , shapeMargin ) ;
265
+ if ( y === null ) return ;
266
+ rendererItem . y = y ;
267
+
86
268
if ( rendererItem . text !== undefined ) {
87
- rendererItem . text . y = rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) as Coordinate ;
269
+ rendererItem . text . y = ( rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
270
+ offsets . aboveBar += textHeight * ( 1 + 2 * Constants . TextMargin ) ;
271
+ }
272
+ return ;
273
+ }
274
+ case 'atPriceBottom' : {
275
+ if ( marker . price !== undefined ) {
276
+ // This should have been handled by positionMarkerAtPrice
277
+ // If we're here, it means the price coordinate conversion failed
278
+ return ;
279
+ }
280
+ // Fallback to belowBar behavior
281
+ y = positionBelowBar ( series , lowPrice , halfSize , offsets , shapeSize , shapeMargin ) ;
282
+ if ( y === null ) return ;
283
+ rendererItem . y = y ;
284
+
285
+ if ( rendererItem . text !== undefined ) {
286
+ rendererItem . text . y = ( rendererItem . y + halfSize + shapeMargin + textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
88
287
offsets . belowBar += textHeight * ( 1 + 2 * Constants . TextMargin ) ;
89
288
}
90
- offsets . belowBar += shapeSize + shapeMargin ;
289
+ return ;
290
+ }
291
+ case 'atPriceMiddle' : {
292
+ if ( marker . price !== undefined ) {
293
+ // This should have been handled by positionMarkerAtPrice
294
+ // If we're here, it means the price coordinate conversion failed
295
+ return ;
296
+ }
297
+ // Fallback to inBar behavior
298
+ rendererItem . y = ensureNotNull ( series . priceToCoordinate ( inBarPrice ) ) ;
299
+ if ( rendererItem . text !== undefined ) {
300
+ rendererItem . text . y = ( rendererItem . y - halfSize - textHeight * ( 0.5 + Constants . TextMargin ) ) as Coordinate ;
301
+ }
91
302
return ;
92
303
}
93
304
}
94
305
95
306
ensureNever ( marker . position ) ;
96
307
}
308
+ /* eslint-enable */
97
309
98
310
function isValueData < HorzScaleItem > (
99
311
data : SeriesDataItemTypeMap < HorzScaleItem > [ SeriesType ]
0 commit comments