react-native-livechart 4.11.0 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/CrosshairLine.d.ts +3 -1
  2. package/dist/components/CrosshairLine.d.ts.map +1 -1
  3. package/dist/components/CustomReferenceLineOverlay.d.ts +15 -10
  4. package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
  5. package/dist/components/DotOverlay.d.ts +3 -5
  6. package/dist/components/DotOverlay.d.ts.map +1 -1
  7. package/dist/components/LiveChart.d.ts.map +1 -1
  8. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  9. package/dist/components/PerSeriesTooltipOverlay.d.ts +19 -0
  10. package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -0
  11. package/dist/components/ReferenceLineOverlay.d.ts +20 -6
  12. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  13. package/dist/components/YAxisOverlay.d.ts +3 -1
  14. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  15. package/dist/core/liveIndicatorVisibility.d.ts +9 -0
  16. package/dist/core/liveIndicatorVisibility.d.ts.map +1 -0
  17. package/dist/core/resolveConfig.d.ts +29 -0
  18. package/dist/core/resolveConfig.d.ts.map +1 -1
  19. package/dist/hooks/crosshairSeries.d.ts +10 -2
  20. package/dist/hooks/crosshairSeries.d.ts.map +1 -1
  21. package/dist/hooks/crosshairShared.d.ts +33 -2
  22. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  23. package/dist/hooks/delayedPanGuard.d.ts +66 -8
  24. package/dist/hooks/delayedPanGuard.d.ts.map +1 -1
  25. package/dist/hooks/useChartPaths.d.ts +5 -9
  26. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  27. package/dist/hooks/useCrosshair.d.ts +7 -1
  28. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  29. package/dist/hooks/useCrosshairSeries.d.ts +22 -3
  30. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  31. package/dist/hooks/useMomentum.d.ts +1 -1
  32. package/dist/hooks/useMomentum.d.ts.map +1 -1
  33. package/dist/hooks/usePanScroll.d.ts +15 -1
  34. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/math/lerp.d.ts.map +1 -1
  38. package/dist/math/momentum.d.ts +1 -1
  39. package/dist/math/momentum.d.ts.map +1 -1
  40. package/dist/math/referenceLines.d.ts +7 -0
  41. package/dist/math/referenceLines.d.ts.map +1 -1
  42. package/dist/types.d.ts +160 -18
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/CrosshairLine.tsx +4 -1
  46. package/src/components/CustomReferenceLineOverlay.tsx +42 -13
  47. package/src/components/DotOverlay.tsx +24 -8
  48. package/src/components/LiveChart.tsx +157 -37
  49. package/src/components/LiveChartSeries.tsx +128 -16
  50. package/src/components/PerSeriesTooltipOverlay.tsx +287 -0
  51. package/src/components/ReferenceLineOverlay.tsx +274 -141
  52. package/src/components/YAxisOverlay.tsx +5 -1
  53. package/src/core/liveIndicatorVisibility.ts +25 -0
  54. package/src/core/resolveConfig.ts +86 -0
  55. package/src/hooks/crosshairSeries.ts +262 -33
  56. package/src/hooks/crosshairShared.ts +36 -2
  57. package/src/hooks/delayedPanGuard.ts +111 -6
  58. package/src/hooks/useChartPaths.ts +25 -15
  59. package/src/hooks/useCrosshair.ts +57 -4
  60. package/src/hooks/useCrosshairSeries.ts +95 -7
  61. package/src/hooks/useMomentum.ts +8 -1
  62. package/src/hooks/usePanScroll.ts +46 -5
  63. package/src/index.ts +1 -0
  64. package/src/math/lerp.ts +7 -0
  65. package/src/math/momentum.ts +22 -5
  66. package/src/math/referenceLines.ts +38 -0
  67. package/src/types.ts +168 -18
@@ -12,7 +12,10 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
12
12
  import type { ChartPadding } from "../draw/line";
13
13
  import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
14
14
  import { usePathBuilder } from "../hooks/usePathBuilder";
15
- import { useReferenceLine } from "../hooks/useReferenceLine";
15
+ import {
16
+ useReferenceLine,
17
+ type ReferenceLineLayout,
18
+ } from "../hooks/useReferenceLine";
16
19
  import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
17
20
  import { referenceLineForm, resolveReferenceBadge } from "../math/referenceLines";
18
21
  import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types";
@@ -23,6 +26,10 @@ const BAND_FILL_OPACITY = 0.16;
23
26
  /** Vertical padding inside the badge pill, in px (kept in sync with the layout). */
24
27
  const BADGE_PILL_PAD_Y = 3;
25
28
  const BADGE_PILL_RADIUS = 5;
29
+ /** Gap between a badge's outer edge and its dashed connector, in px. */
30
+ const CONNECTOR_GAP = 4;
31
+ /** Badge inset from its plot edge, in px (matches useReferenceLine). */
32
+ const BADGE_EDGE_INSET = 2;
26
33
 
27
34
  /**
28
35
  * Renders one reference line or band into the chart canvas. Handles all three
@@ -30,20 +37,7 @@ const BADGE_PILL_RADIUS = 5;
30
37
  * band) plus the Form-A pill badge (in-range tag + off-screen chevron pin).
31
38
  * Self-contained so callers can `.map()` over a variable-length array.
32
39
  */
33
- export function ReferenceLineOverlay({
34
- engine,
35
- padding,
36
- line,
37
- palette,
38
- formatValue,
39
- font,
40
- fontProp,
41
- badgeLayer = false,
42
- suppressTag = false,
43
- groupHidden,
44
- dragValues,
45
- index = 0,
46
- }: {
40
+ type ReferenceLineOverlayProps = {
47
41
  engine: ChartEngineLayout;
48
42
  padding: ChartPadding;
49
43
  line: ReferenceLine;
@@ -64,11 +58,23 @@ export function ReferenceLineOverlay({
64
58
  */
65
59
  badgeLayer?: boolean;
66
60
  /**
67
- * Suppress the built-in tag (badge pill + connector + chevron + icon + gutter
68
- * label) for this line — used when a custom `renderReferenceLine` element owns
69
- * the tag. The line / band stroke still draws. No effect on the base pass.
70
- */
61
+ * Suppress the built-in tag (badge pill + chevron + icon + gutter label) for
62
+ * this line — used when a custom `renderReferenceLine` element owns the tag.
63
+ * The line / band stroke and a badged line's dashed connector still draw. No
64
+ * effect on the base pass.
65
+ */
71
66
  suppressTag?: boolean;
67
+ /**
68
+ * Suppress the built-in tag only while the line is off-axis. Used by
69
+ * `renderOffAxisReferenceLine`, so the normal in-range tag remains intact.
70
+ */
71
+ suppressTagWhenOffAxis?: boolean;
72
+ /**
73
+ * Measured widths of custom reference-line tags, index-aligned with their
74
+ * `referenceLines`. When present for a suppressed badged tag, the built-in
75
+ * connector begins after the custom element instead of the hidden Skia pill.
76
+ */
77
+ customTagWidths?: SharedValue<number[]>;
72
78
  /**
73
79
  * Per-frame grouping flags (index-aligned): when this line's slot is `true` it's
74
80
  * collapsed into a group count handle, so its tag is suppressed (the line / band
@@ -79,7 +85,24 @@ export function ReferenceLineOverlay({
79
85
  dragValues?: SharedValue<number[]>;
80
86
  /** This line's index into {@link dragValues}. */
81
87
  index?: number;
82
- }) {
88
+ };
89
+
90
+ export function ReferenceLineOverlay({
91
+ engine,
92
+ padding,
93
+ line,
94
+ palette,
95
+ formatValue,
96
+ font,
97
+ fontProp,
98
+ badgeLayer = false,
99
+ suppressTag = false,
100
+ suppressTagWhenOffAxis = false,
101
+ customTagWidths,
102
+ groupHidden,
103
+ dragValues,
104
+ index = 0,
105
+ }: ReferenceLineOverlayProps) {
83
106
  const form = referenceLineForm(line);
84
107
  const isBand = form === "value-band" || form === "time-band";
85
108
 
@@ -141,24 +164,78 @@ export function ReferenceLineOverlay({
141
164
  ? [{ translateX: badgeOffsetX }, { translateY: badgeOffsetY }]
142
165
  : undefined;
143
166
 
167
+ if (!badgeLayer) {
168
+ return (
169
+ <ReferenceLineBasePass
170
+ layout={layout}
171
+ color={color}
172
+ strokeWidth={strokeWidth}
173
+ intervals={intervals}
174
+ isBand={isBand}
175
+ isTimeBand={form === "time-band"}
176
+ bandFillOpacity={bandFillOpacity}
177
+ hasBandBorder={hasBandBorder}
178
+ />
179
+ );
180
+ }
181
+
182
+ return (
183
+ <ReferenceLineBadgePass
184
+ layout={layout}
185
+ color={color}
186
+ strokeWidth={strokeWidth}
187
+ intervals={intervals}
188
+ badgeFont={badgeFont}
189
+ badgePosition={badge?.position}
190
+ labelColor={labelColor}
191
+ badgeBackground={badgeBackground}
192
+ badgeBorderColor={badgeBorderColor}
193
+ badgeBorderWidth={badgeBorderWidth}
194
+ badgeRadius={badgeRadius}
195
+ badgeOffsetX={badgeOffsetX}
196
+ badgeOffsetY={badgeOffsetY}
197
+ suppressTag={suppressTag}
198
+ suppressTagWhenOffAxis={suppressTagWhenOffAxis}
199
+ customTagWidths={customTagWidths}
200
+ groupHidden={groupHidden}
201
+ index={index}
202
+ />
203
+ );
204
+ }
205
+
206
+ /** Base stroke / band pass, kept behind the chart content. */
207
+ function ReferenceLineBasePass({
208
+ layout,
209
+ color,
210
+ strokeWidth,
211
+ intervals,
212
+ isBand,
213
+ isTimeBand,
214
+ bandFillOpacity,
215
+ hasBandBorder,
216
+ }: {
217
+ layout: SharedValue<ReferenceLineLayout>;
218
+ color: string;
219
+ strokeWidth: number;
220
+ intervals: [number, number];
221
+ isBand: boolean;
222
+ isTimeBand: boolean;
223
+ bandFillOpacity: number;
224
+ hasBandBorder: boolean;
225
+ }) {
144
226
  const lineBuilder = usePathBuilder();
145
227
  const bandBuilder = usePathBuilder();
146
228
  const borderBuilder = usePathBuilder();
147
- const connBuilder = usePathBuilder();
148
- const chevBuilder = usePathBuilder();
149
229
 
150
230
  const linePath = useDerivedValue(() => {
151
231
  const b = lineBuilder.value;
152
232
  const l = layout.get();
153
- // A plain horizontal line, drawn at the line extent (full-width when opted in).
154
- // A non-full-width badge instead draws a pill + connector to the edge, below.
155
233
  if (l.visible && l.drawLine && !isBand) {
156
234
  b.moveTo(l.lineX1, l.y);
157
235
  b.lineTo(l.lineX2, l.y);
158
236
  }
159
237
  return b.detach();
160
238
  });
161
-
162
239
  const bandPath = useDerivedValue(() => {
163
240
  const b = bandBuilder.value;
164
241
  const l = layout.get();
@@ -171,19 +248,16 @@ export function ReferenceLineOverlay({
171
248
  }
172
249
  return b.detach();
173
250
  });
174
-
175
251
  const bandBorderPath = useDerivedValue(() => {
176
252
  const b = borderBuilder.value;
177
253
  const l = layout.get();
178
254
  if (l.visible && hasBandBorder) {
179
- if (form === "time-band") {
180
- // Vertical edges at the band's left / right.
255
+ if (isTimeBand) {
181
256
  b.moveTo(l.lineX1, l.y);
182
257
  b.lineTo(l.lineX1, l.yBottom);
183
258
  b.moveTo(l.lineX2, l.y);
184
259
  b.lineTo(l.lineX2, l.yBottom);
185
260
  } else {
186
- // Horizontal edges at the band's top / bottom.
187
261
  b.moveTo(l.lineX1, l.y);
188
262
  b.lineTo(l.lineX2, l.y);
189
263
  b.moveTo(l.lineX1, l.yBottom);
@@ -192,18 +266,125 @@ export function ReferenceLineOverlay({
192
266
  }
193
267
  return b.detach();
194
268
  });
269
+ const lineOpacity = useDerivedValue(() => {
270
+ const l = layout.get();
271
+ return l.visible && l.drawLine && !isBand ? 1 : 0;
272
+ });
273
+ const bandOpacity = useDerivedValue(() =>
274
+ layout.get().visible && isBand ? bandFillOpacity : 0,
275
+ );
276
+ const bandBorderOpacity = useDerivedValue(() =>
277
+ layout.get().visible && hasBandBorder ? 1 : 0,
278
+ );
279
+
280
+ return (
281
+ <Group>
282
+ {isBand && (
283
+ <Group opacity={bandOpacity}>
284
+ <Path path={bandPath} style="fill" color={color} />
285
+ </Group>
286
+ )}
287
+ {hasBandBorder && (
288
+ <Group opacity={bandBorderOpacity}>
289
+ <Path
290
+ path={bandBorderPath}
291
+ style="stroke"
292
+ strokeWidth={strokeWidth}
293
+ color={color}
294
+ >
295
+ <DashPathEffect intervals={intervals} />
296
+ </Path>
297
+ </Group>
298
+ )}
299
+ {!isBand && (
300
+ <Group opacity={lineOpacity}>
301
+ <Path
302
+ path={linePath}
303
+ style="stroke"
304
+ strokeWidth={strokeWidth}
305
+ color={color}
306
+ >
307
+ <DashPathEffect intervals={intervals} />
308
+ </Path>
309
+ </Group>
310
+ )}
311
+ </Group>
312
+ );
313
+ }
195
314
 
196
- // Badge connector the dashed line from the pill out to the opposite edge.
315
+ /** Badge, connector, and label pass, painted above the chart's left-edge fade. */
316
+ function ReferenceLineBadgePass({
317
+ layout,
318
+ color,
319
+ strokeWidth,
320
+ intervals,
321
+ badgeFont,
322
+ badgePosition,
323
+ labelColor,
324
+ badgeBackground,
325
+ badgeBorderColor,
326
+ badgeBorderWidth,
327
+ badgeRadius,
328
+ badgeOffsetX,
329
+ badgeOffsetY,
330
+ suppressTag,
331
+ suppressTagWhenOffAxis,
332
+ customTagWidths,
333
+ groupHidden,
334
+ index,
335
+ }: {
336
+ layout: SharedValue<ReferenceLineLayout>;
337
+ color: string;
338
+ strokeWidth: number;
339
+ intervals: [number, number];
340
+ badgeFont: SkFont;
341
+ badgePosition: "left" | "center" | "right" | undefined;
342
+ labelColor: string;
343
+ badgeBackground: string;
344
+ badgeBorderColor: string;
345
+ badgeBorderWidth: number;
346
+ badgeRadius: number;
347
+ badgeOffsetX: number;
348
+ badgeOffsetY: number;
349
+ suppressTag: boolean;
350
+ suppressTagWhenOffAxis: boolean;
351
+ customTagWidths?: SharedValue<number[]>;
352
+ groupHidden?: SharedValue<boolean[]>;
353
+ index: number;
354
+ }) {
355
+ const connBuilder = usePathBuilder();
356
+ const chevBuilder = usePathBuilder();
357
+ const badgeTransform =
358
+ badgeOffsetX !== 0 || badgeOffsetY !== 0
359
+ ? [{ translateX: badgeOffsetX }, { translateY: badgeOffsetY }]
360
+ : undefined;
197
361
  const connPath = useDerivedValue(() => {
198
362
  const b = connBuilder.value;
199
363
  const l = layout.get();
200
364
  if (l.visible && l.badge && l.connStart >= 0) {
201
- b.moveTo(l.connStart, l.y);
202
- b.lineTo(l.connEnd, l.y);
365
+ let start = l.connStart;
366
+ let end = l.connEnd;
367
+ const customTagActive =
368
+ suppressTag || (suppressTagWhenOffAxis && l.offAxis);
369
+ const customWidth = customTagActive
370
+ ? customTagWidths?.get()[index] ?? 0
371
+ : 0;
372
+ if (customWidth > 0) {
373
+ if (badgePosition === "left") {
374
+ start = l.x1 + BADGE_EDGE_INSET + customWidth + CONNECTOR_GAP;
375
+ end = l.x2;
376
+ } else if (badgePosition === "right") {
377
+ start = l.x1;
378
+ end = l.x2 - BADGE_EDGE_INSET - customWidth - CONNECTOR_GAP;
379
+ }
380
+ }
381
+ if (end > start) {
382
+ b.moveTo(start, l.y);
383
+ b.lineTo(end, l.y);
384
+ }
203
385
  }
204
386
  return b.detach();
205
387
  });
206
-
207
388
  const chevronPath = useDerivedValue(() => {
208
389
  const b = chevBuilder.value;
209
390
  const l = layout.get();
@@ -223,41 +404,34 @@ export function ReferenceLineOverlay({
223
404
  }
224
405
  return b.detach();
225
406
  });
226
-
227
- const lineOpacity = useDerivedValue(() => {
407
+ const badgeOpacity = useDerivedValue(() => {
228
408
  const l = layout.get();
229
- return l.visible && l.drawLine && !isBand ? 1 : 0;
409
+ const grouped = groupHidden ? groupHidden.get()[index] === true : false;
410
+ const customTagActive =
411
+ suppressTag || (suppressTagWhenOffAxis && l.offAxis);
412
+ return !customTagActive && !grouped && l.visible && l.badge ? 1 : 0;
230
413
  });
231
- const bandOpacity = useDerivedValue(() =>
232
- layout.get().visible && isBand ? bandFillOpacity : 0,
233
- );
234
- const bandBorderOpacity = useDerivedValue(() =>
235
- layout.get().visible && hasBandBorder ? 1 : 0,
236
- );
237
- const badgeOpacity = useDerivedValue(() => {
414
+ const connectorOpacity = useDerivedValue(() => {
238
415
  const l = layout.get();
239
416
  const grouped = groupHidden ? groupHidden.get()[index] === true : false;
240
- return !suppressTag && !grouped && l.visible && l.badge ? 1 : 0;
417
+ return !grouped && l.visible && l.badge && l.connStart >= 0 ? 1 : 0;
241
418
  });
242
- // Text + icon ride in the badge pass too, so they stay crisp above the fade.
243
419
  const labelOpacity = useDerivedValue(() => {
244
420
  const l = layout.get();
245
421
  const grouped = groupHidden ? groupHidden.get()[index] === true : false;
246
- return !suppressTag && !grouped && l.visible && l.label.length > 0 ? 1 : 0;
422
+ const customTagActive =
423
+ suppressTag || (suppressTagWhenOffAxis && l.offAxis);
424
+ return !customTagActive && !grouped && l.visible && l.label.length > 0 ? 1 : 0;
247
425
  });
248
426
  const iconOpacity = useDerivedValue(() => {
249
427
  const l = layout.get();
250
428
  return l.visible && l.icon.length > 0 ? 1 : 0;
251
429
  });
252
-
253
430
  const labelX = useDerivedValue(() => layout.get().labelX);
254
431
  const labelY = useDerivedValue(() => layout.get().labelY);
255
432
  const labelText = useDerivedValue(() => layout.get().label);
256
433
  const iconX = useDerivedValue(() => layout.get().iconX);
257
434
  const iconText = useDerivedValue(() => layout.get().icon);
258
-
259
- // Font metrics depend only on the (stable) font, so read them once instead of
260
- // on every frame inside the pill worklet (`getMetrics` allocates + crosses JSI).
261
435
  const { ascent: fontAscent, height: pillH } = (() => {
262
436
  const fm = badgeFont.getMetrics();
263
437
  return {
@@ -265,8 +439,6 @@ export function ReferenceLineOverlay({
265
439
  height: fm.descent - fm.ascent + BADGE_PILL_PAD_Y * 2,
266
440
  };
267
441
  })();
268
-
269
- // Pill rect — position/size from the layout; vertically centered on the line.
270
442
  const pillX = useDerivedValue(() => layout.get().pillX);
271
443
  const pillW = useDerivedValue(() => layout.get().pillW);
272
444
  const pillY = useDerivedValue(
@@ -275,101 +447,62 @@ export function ReferenceLineOverlay({
275
447
 
276
448
  return (
277
449
  <Group>
278
- {/* Base pass — bands + lines, drawn behind the chart content (and faded at
279
- the left edge like the rest of the content). */}
280
- {!badgeLayer && isBand && (
281
- <Group opacity={bandOpacity}>
282
- <Path path={bandPath} style="fill" color={color} />
283
- </Group>
284
- )}
285
-
286
- {!badgeLayer && hasBandBorder && (
287
- <Group opacity={bandBorderOpacity}>
288
- <Path
289
- path={bandBorderPath}
290
- style="stroke"
291
- strokeWidth={strokeWidth}
292
- color={color}
293
- >
294
- <DashPathEffect intervals={intervals} />
295
- </Path>
296
- </Group>
297
- )}
298
-
299
- {!badgeLayer && !isBand && (
300
- <Group opacity={lineOpacity}>
301
- <Path
302
- path={linePath}
303
- style="stroke"
304
- strokeWidth={strokeWidth}
305
- color={color}
306
- >
307
- <DashPathEffect intervals={intervals} />
308
- </Path>
309
- </Group>
310
- )}
311
-
312
- {/* Badge pass — connector + pill + chevron + icon, above the left-edge fade. */}
313
- {badgeLayer && (
314
- <Group opacity={badgeOpacity} transform={badgeTransform}>
315
- <Path
316
- path={connPath}
317
- style="stroke"
318
- strokeWidth={strokeWidth}
319
- color={color}
320
- >
321
- <DashPathEffect intervals={intervals} />
322
- </Path>
323
- <RoundedRect
324
- x={pillX}
325
- y={pillY}
326
- width={pillW}
327
- height={pillH}
328
- r={badgeRadius}
329
- color={badgeBackground}
330
- />
331
- <RoundedRect
332
- x={pillX}
333
- y={pillY}
334
- width={pillW}
335
- height={pillH}
336
- r={badgeRadius}
337
- color={badgeBorderColor}
338
- style="stroke"
339
- strokeWidth={badgeBorderWidth}
340
- />
341
- <Path
342
- path={chevronPath}
343
- style="stroke"
344
- strokeWidth={1.5}
345
- color={color}
346
- strokeCap="round"
347
- strokeJoin="round"
348
- />
349
- <Group opacity={iconOpacity}>
350
- <SkiaText
351
- x={iconX}
352
- y={labelY}
353
- text={iconText}
354
- font={badgeFont}
355
- color={labelColor}
356
- />
357
- </Group>
358
- </Group>
359
- )}
360
-
361
- {/* Labels ride in the badge pass too, so they stay crisp above the fade. */}
362
- {badgeLayer && (
363
- <Group opacity={labelOpacity} transform={badgeTransform}>
450
+ <Group opacity={connectorOpacity} transform={badgeTransform}>
451
+ <Path
452
+ path={connPath}
453
+ style="stroke"
454
+ strokeWidth={strokeWidth}
455
+ color={color}
456
+ >
457
+ <DashPathEffect intervals={intervals} />
458
+ </Path>
459
+ </Group>
460
+ <Group opacity={badgeOpacity} transform={badgeTransform}>
461
+ <RoundedRect
462
+ x={pillX}
463
+ y={pillY}
464
+ width={pillW}
465
+ height={pillH}
466
+ r={badgeRadius}
467
+ color={badgeBackground}
468
+ />
469
+ <RoundedRect
470
+ x={pillX}
471
+ y={pillY}
472
+ width={pillW}
473
+ height={pillH}
474
+ r={badgeRadius}
475
+ color={badgeBorderColor}
476
+ style="stroke"
477
+ strokeWidth={badgeBorderWidth}
478
+ />
479
+ <Path
480
+ path={chevronPath}
481
+ style="stroke"
482
+ strokeWidth={1.5}
483
+ color={color}
484
+ strokeCap="round"
485
+ strokeJoin="round"
486
+ />
487
+ <Group opacity={iconOpacity}>
364
488
  <SkiaText
365
- x={labelX}
489
+ x={iconX}
366
490
  y={labelY}
367
- text={labelText}
491
+ text={iconText}
368
492
  font={badgeFont}
369
493
  color={labelColor}
370
494
  />
371
495
  </Group>
372
- )}
496
+ </Group>
497
+ <Group opacity={labelOpacity} transform={badgeTransform}>
498
+ <SkiaText
499
+ x={labelX}
500
+ y={labelY}
501
+ text={labelText}
502
+ font={badgeFont}
503
+ color={labelColor}
504
+ />
505
+ </Group>
373
506
  </Group>
374
507
  );
375
508
  }
@@ -52,6 +52,7 @@ export function YAxisOverlay({
52
52
  badgeCenterY,
53
53
  badgeFontSize,
54
54
  badgeOffsetY = 0,
55
+ badgeOpacity,
55
56
  seriesLabelInset = 0,
56
57
  gridStyle,
57
58
  variant = "all",
@@ -74,6 +75,8 @@ export function YAxisOverlay({
74
75
  badgeFontSize?: number;
75
76
  /** Configured live badge Y offset. */
76
77
  badgeOffsetY?: number;
78
+ /** Live badge opacity. A fully hidden badge does not suppress an axis label. */
79
+ badgeOpacity?: SharedValue<number>;
77
80
  /** When > 0, series labels occupy the left portion of the gutter; Y-axis labels right-align. */
78
81
  seriesLabelInset?: number;
79
82
  /** Grid-line styling overrides. Omit for the legacy solid 1px line. */
@@ -118,7 +121,8 @@ export function YAxisOverlay({
118
121
  const fm = font.getMetrics();
119
122
  const baselineOffset = (fm.ascent + fm.descent) / 2;
120
123
  const labelHeight = fm.descent - fm.ascent;
121
- const resolvedBadgeCenterY = badgeCenterY
124
+ const badgeIsVisible = badgeOpacity === undefined || badgeOpacity.get() > 0;
125
+ const resolvedBadgeCenterY = badgeCenterY && badgeIsVisible
122
126
  ? badgeCenterY.get() + badgeOffsetY
123
127
  : null;
124
128
  const badgeHeight =
@@ -0,0 +1,25 @@
1
+ import type { TimeScrollConfig } from "../types";
2
+
3
+ /**
4
+ * Resolve whether historical time-scroll views should suppress indicators that
5
+ * still point at the live price.
6
+ */
7
+ export function resolveHideLiveOnScrollBack(
8
+ timeScroll: boolean | TimeScrollConfig | undefined,
9
+ followViewEdge: boolean,
10
+ ): boolean {
11
+ const hide =
12
+ typeof timeScroll === "object"
13
+ ? (timeScroll.hideLiveOnScrollBack ?? true)
14
+ : true;
15
+ return hide && !followViewEdge;
16
+ }
17
+
18
+ /** Worklet-safe opacity gate shared by the live badge, dot, and dashed value line. */
19
+ export function liveIndicatorScrollOpacity(
20
+ hideLiveOnScrollBack: boolean,
21
+ viewEnd: number | null,
22
+ ): 0 | 1 {
23
+ "worklet";
24
+ return hideLiveOnScrollBack && viewEnd != null ? 0 : 1;
25
+ }