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