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
@@ -58,7 +58,11 @@ import {
58
58
  resolveYAxis,
59
59
  } from "../core/resolveConfig";
60
60
  import type { ResolvedThresholdConfig } from "../core/resolveConfig";
61
- import { resolveSegment } from "../core/resolveSegment";
61
+ import {
62
+ liveIndicatorScrollOpacity,
63
+ resolveHideLiveOnScrollBack,
64
+ } from "../core/liveIndicatorVisibility";
65
+ import { resolveSegment, type ResolvedSegment } from "../core/resolveSegment";
62
66
  import { useLiveChartEngine } from "../core/useLiveChartEngine";
63
67
  import { pulseRadialOutset } from "../draw/line";
64
68
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
@@ -78,7 +82,10 @@ import { useMarkers } from "../hooks/useMarkers";
78
82
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
79
83
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
80
84
  import { useModeBlend } from "../hooks/useModeBlend";
81
- import { useMomentum } from "../hooks/useMomentum";
85
+ import {
86
+ resolveMomentumProp,
87
+ useMomentum,
88
+ } from "../hooks/useMomentum";
82
89
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
83
90
  import { usePinchZoom } from "../hooks/usePinchZoom";
84
91
  import { useVisibleRange } from "../hooks/useVisibleRange";
@@ -105,6 +112,7 @@ import {
105
112
  import {
106
113
  collectReferenceValues,
107
114
  referenceLineForm,
115
+ referenceLineReactKeys,
108
116
  resolveReferenceGroupBadge,
109
117
  } from "../math/referenceLines";
110
118
  import {
@@ -311,6 +319,7 @@ function useLiveChartController({
311
319
  renderTooltip,
312
320
  renderOverlay,
313
321
  renderReferenceLine,
322
+ renderOffAxisReferenceLine,
314
323
  referenceLineGrouping,
315
324
  leftEdgeFade = true,
316
325
 
@@ -387,18 +396,18 @@ function useLiveChartController({
387
396
  const seededRef = useRef<(number | undefined)[]>([]);
388
397
  const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
389
398
  useEffect(() => {
390
- const active = dragActive.value;
391
- const cur = dragValues.value;
399
+ const active = dragActive.get();
400
+ const cur = dragValues.get();
392
401
  const seeded = seededRef.current;
393
- dragValues.value = allRefLines.map((l, i) => {
402
+ dragValues.set(allRefLines.map((l, i) => {
394
403
  const prop = l.value ?? 0;
395
404
  if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
396
405
  if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
397
406
  return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
398
- });
407
+ }));
399
408
  seededRef.current = allRefLines.map((l) => l.value);
400
- if (dragActive.value.length !== allRefLines.length) {
401
- dragActive.value = allRefLines.map((_, i) => active[i] ?? false);
409
+ if (dragActive.get().length !== allRefLines.length) {
410
+ dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
402
411
  }
403
412
  // allRefLines is rebuilt every render; key off the value signature + length.
404
413
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -410,6 +419,17 @@ function useLiveChartController({
410
419
  allRefLines,
411
420
  renderReferenceLine,
412
421
  );
422
+ // An off-axis renderer replaces only the edge-pinned tag. A full custom tag
423
+ // takes precedence for the same line to avoid mounting two native tags.
424
+ const refLineOffAxisCustom = customReferenceLineFlags(
425
+ allRefLines,
426
+ renderOffAxisReferenceLine,
427
+ "off-axis",
428
+ ).map((custom, index) => custom && !refLineCustom[index]);
429
+ const refLineKeys = referenceLineReactKeys(allRefLines);
430
+ // RN custom tags report their measured widths here so the Skia connector can
431
+ // start after the native badge instead of the hidden built-in pill.
432
+ const refLineCustomTagWidths = useSharedValue<number[]>([]);
413
433
 
414
434
  // Live Y values of the *draggable* Form-A lines, folded into the engine's
415
435
  // axis-range fit so dragging a line toward / past the visible edge expands the
@@ -421,13 +441,17 @@ function useLiveChartController({
421
441
  // contributes its live value; a series contributes its window min/max
422
442
  // (respecting `extendToNow`), so an off-range break-even expands the axis and
423
443
  // stays on-plot like a reference line would.
424
- const draggableRefIdx = allRefLines
425
- .map((l, i) =>
426
- l.draggable && !l.excludeFromRange && referenceLineForm(l) === "line"
427
- ? i
428
- : -1,
429
- )
430
- .filter((i) => i >= 0);
444
+ const draggableRefIdx: number[] = [];
445
+ for (let i = 0; i < allRefLines.length; i++) {
446
+ const line = allRefLines[i];
447
+ if (
448
+ line.draggable &&
449
+ !line.excludeFromRange &&
450
+ referenceLineForm(line) === "line"
451
+ ) {
452
+ draggableRefIdx.push(i);
453
+ }
454
+ }
431
455
  const thresholdInRange = thresholdCfg?.includeInRange === true;
432
456
  // Constant benchmark → its live value rides this channel. A series threshold
433
457
  // is folded inside the engine tick instead (`thresholdRangePoints`), which
@@ -572,6 +596,7 @@ function useLiveChartController({
572
596
  // react-doctor-disable-next-line react-doctor/no-derived-state-effect -- Reanimated: must read the SharedValue off the render path
573
597
  useLayoutEffect(() => {
574
598
  // react-doctor-disable-next-line react-hooks-js/set-state-in-effect -- Reanimated: seeding from a SharedValue off render is the warning-free path
599
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Reanimated: seed from the SharedValue outside render to avoid strict-mode access warnings
575
600
  setValueLayoutSample(value.get());
576
601
  }, [value]);
577
602
 
@@ -721,7 +746,7 @@ function useLiveChartController({
721
746
  if (
722
747
  referenceLineForm(l) !== "line" ||
723
748
  l.value === undefined ||
724
- refLineCustom[i]
749
+ refLineCustom[i] || refLineOffAxisCustom[i]
725
750
  ) {
726
751
  ys.push(-1);
727
752
  continue;
@@ -866,11 +891,8 @@ function useLiveChartController({
866
891
  // polyline passed as the last arg below).
867
892
  thresholdCfg?.fill && !thresholdIsSeries ? thresholdGeom.lineY : undefined,
868
893
  lineIsLinear,
869
- // Tip the line at the view-edge price (not the live value) while scrolled,
870
- // matching the live dot so the right edge doesn't drop to the off-screen
871
- // live value when `followViewEdge` tracks the scrolled-back window.
872
- engine.edgeValue,
873
- badgeCfg?.followViewEdge ?? false,
894
+ // The plotted line independently selects the visible edge while historical;
895
+ // badge/live-indicator options affect overlays only.
874
896
  // Match the standalone loading squiggle's wave during the reveal morph.
875
897
  loadingCfg?.amplitude,
876
898
  loadingCfg?.speed,
@@ -906,6 +928,15 @@ function useLiveChartController({
906
928
  );
907
929
 
908
930
  const momentumSV = useMomentum(engine, momentum);
931
+ // A follow-edge badge must derive its color from the same historical point
932
+ // as its value and position. While following live, reuse the normal momentum
933
+ // result; while scrolled back, detect against the data prefix ending at the
934
+ // visible right-edge timestamp so incoming live ticks cannot change the pill.
935
+ const badgeMomentumSV = useDerivedValue(() =>
936
+ badgeCfg?.followViewEdge && engine.viewEnd.value !== null
937
+ ? resolveMomentumProp(momentum, engine.data.value, engine.timestamp.value)
938
+ : momentumSV.value,
939
+ );
909
940
 
910
941
  // ── Overlay hooks ─────────────────────────────────────────────────────
911
942
  // Scrub/crosshair must see the same stash-backed candles as the engine.
@@ -997,6 +1028,14 @@ function useLiveChartController({
997
1028
  ? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
998
1029
  : (scrubCfg?.panGestureDelay ?? 0);
999
1030
 
1031
+ // Cross-gesture arbitration for the one-finger touch. `Gesture.Race` below is
1032
+ // NOT arbitration — RNGH's Race adds no relation between its children, so both
1033
+ // pans recognize independently and each can activate while the other already
1034
+ // owns the touch. This latch (written by the scroll pan, read by the scrub's
1035
+ // long-press guard) makes "the scroll already won" a hard fact; `scrubActive`
1036
+ // (written by the crosshair, read by the scroll pan) is the mirror image.
1037
+ const scrollActive = useSharedValue(false);
1038
+
1000
1039
  const crosshair = useCrosshair(
1001
1040
  engine,
1002
1041
  effectivePadding,
@@ -1026,6 +1065,7 @@ function useLiveChartController({
1026
1065
  timeScrollEnabled && scrollGestureMode === "axisDrag"
1027
1066
  ? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
1028
1067
  : 0,
1068
+ scrollActive,
1029
1069
  );
1030
1070
 
1031
1071
  // Capture only the shared value in the worklets below. Referencing
@@ -1048,6 +1088,10 @@ function useLiveChartController({
1048
1088
  minTime: scrollMinTime,
1049
1089
  enabled: timeScrollEnabled,
1050
1090
  mode: scrollGestureMode,
1091
+ scrollActive,
1092
+ // Once a scrub is engaged the chart is locked: scrolling goes inert so the
1093
+ // finger only moves the price indicator across a fixed window.
1094
+ scrubActive: crosshairScrubActive,
1051
1095
  // Clear any live crosshair when a scroll drag takes over.
1052
1096
  onScrollStart: () => {
1053
1097
  "worklet";
@@ -1172,10 +1216,40 @@ function useLiveChartController({
1172
1216
  // point instead — otherwise both dots show at once. Applies on static charts
1173
1217
  // too, now that they're scrubbable.
1174
1218
  const selectionDotDuringScrub = scrubCfg !== null && selectionDotCfg !== null;
1219
+ // While scrolled back, the badge, dot, and value line still point at the live
1220
+ // price even though the window is showing history. Hide that live-priced group
1221
+ // together. `badge.followViewEdge` opts back in because all three then track
1222
+ // the visible edge price; `hideLiveOnScrollBack: false` keeps the legacy group.
1223
+ const hideLiveOnScrollBack = resolveHideLiveOnScrollBack(
1224
+ timeScroll,
1225
+ badgeCfg?.followViewEdge ?? false,
1226
+ );
1175
1227
  const liveDotOpacity = useDerivedValue(
1176
1228
  () =>
1177
1229
  reveal.dotOpacity.value *
1178
- (selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1),
1230
+ (selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1) *
1231
+ liveIndicatorScrollOpacity(
1232
+ hideLiveOnScrollBack,
1233
+ engine.viewEnd.value,
1234
+ ),
1235
+ );
1236
+ // Same scrolled-back gating for the value line: it would draw a dashed line
1237
+ // at the live value's Y — a price that isn't in the scrolled-back view.
1238
+ const valueLineOpacity = useDerivedValue(
1239
+ () =>
1240
+ reveal.lineOpacity.value *
1241
+ liveIndicatorScrollOpacity(
1242
+ hideLiveOnScrollBack,
1243
+ engine.viewEnd.value,
1244
+ ),
1245
+ );
1246
+ const liveBadgeOpacity = useDerivedValue(
1247
+ () =>
1248
+ reveal.badgeOpacity.value *
1249
+ liveIndicatorScrollOpacity(
1250
+ hideLiveOnScrollBack,
1251
+ engine.viewEnd.value,
1252
+ ),
1179
1253
  );
1180
1254
 
1181
1255
  // Fade the annotation overlays (markers + reference lines) out while scrubbing
@@ -1235,10 +1309,14 @@ function useLiveChartController({
1235
1309
  leftEdgeFadeCfg,
1236
1310
  metricsCfg,
1237
1311
  allRefLines,
1312
+ refLineKeys,
1238
1313
  refLineCustom,
1314
+ refLineOffAxisCustom,
1315
+ refLineCustomTagWidths,
1239
1316
  dragValues,
1240
1317
  dragActive,
1241
1318
  renderReferenceLine,
1319
+ renderOffAxisReferenceLine,
1242
1320
  refGroupingActive: refGroupingRadius != null,
1243
1321
  refGroupResult,
1244
1322
  groupHidden,
@@ -1311,9 +1389,12 @@ function useLiveChartController({
1311
1389
  dotX,
1312
1390
  dotY,
1313
1391
  liveDotOpacity,
1392
+ valueLineOpacity,
1393
+ liveBadgeOpacity,
1314
1394
  overlayScrubFade,
1315
1395
  markerGroupOpacity,
1316
1396
  momentumSV,
1397
+ badgeMomentumSV,
1317
1398
  onDegenShake,
1318
1399
  crosshair,
1319
1400
  rootGesture,
@@ -1414,6 +1495,7 @@ function ChartYAxisLayer({
1414
1495
  metricsCfg,
1415
1496
  gridStyleCfg,
1416
1497
  yAxisFloat,
1498
+ liveBadgeOpacity,
1417
1499
  } = model;
1418
1500
  return (
1419
1501
  <Group opacity={reveal.yAxisOpacity}>
@@ -1431,6 +1513,7 @@ function ChartYAxisLayer({
1431
1513
  badgeCenterY={badgeUsesRightGutter ? dotY : undefined}
1432
1514
  badgeFontSize={badgeUsesRightGutter ? badgeFont.getSize() : undefined}
1433
1515
  badgeOffsetY={badgeCfg?.offsetY ?? 0}
1516
+ badgeOpacity={badgeUsesRightGutter ? liveBadgeOpacity : undefined}
1434
1517
  gridStyle={gridStyleCfg}
1435
1518
  />
1436
1519
  </Group>
@@ -1670,8 +1753,10 @@ function ChartStack({
1670
1753
  fontProp,
1671
1754
  badgeCfg,
1672
1755
  valueLineCfg,
1756
+ valueLineOpacity,
1673
1757
  dotY,
1674
1758
  allRefLines,
1759
+ refLineKeys,
1675
1760
  dragValues,
1676
1761
  resolvedSegments,
1677
1762
  hasRecolorSegments,
@@ -1722,9 +1807,9 @@ function ChartStack({
1722
1807
  <Group transform={degen?.shakeTransform}>
1723
1808
  {/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
1724
1809
  painted on the line stroke itself, below — this overlay draws no fill. */}
1725
- {resolvedSegments.map((seg, i) => (
1810
+ {resolvedSegments.map((seg) => (
1726
1811
  <SegmentDividerOverlay
1727
- key={`seg-${seg.from ?? "start"}-${seg.to ?? "end"}-${i}`}
1812
+ key={segmentReactKey(seg)}
1728
1813
  engine={engine}
1729
1814
  padding={effectivePadding}
1730
1815
  segment={seg}
@@ -1734,7 +1819,7 @@ function ChartStack({
1734
1819
 
1735
1820
  {/* Value line + reference line (behind chart line) */}
1736
1821
  {valueLineCfg && (
1737
- <Group opacity={reveal.lineOpacity}>
1822
+ <Group opacity={valueLineOpacity}>
1738
1823
  <ValueLineOverlay
1739
1824
  dotY={dotY}
1740
1825
  engine={engine}
@@ -1746,14 +1831,12 @@ function ChartStack({
1746
1831
  </Group>
1747
1832
  )}
1748
1833
 
1749
- {/* Index keys: reference lines are a positional array and two may share
1750
- value + label (e.g. duplicate working orders at the same price), which a
1751
- content-derived key would collapse to one. Wrapped in a fade group so
1752
- `scrub.hideOverlaysOnScrub` can ease the lines out while scrubbing. */}
1834
+ {/* Wrapped in a fade group so `scrub.hideOverlaysOnScrub` can ease lines
1835
+ out while scrubbing. Explicit ids keep lines stable when reordered. */}
1753
1836
  <Group opacity={overlayScrubFade}>
1754
1837
  {allRefLines.map((rl, i) => (
1755
1838
  <ReferenceLineOverlay
1756
- key={i}
1839
+ key={refLineKeys[i]}
1757
1840
  engine={engine}
1758
1841
  padding={effectivePadding}
1759
1842
  line={rl}
@@ -1857,7 +1940,6 @@ function ChartStack({
1857
1940
  dotX={dotX}
1858
1941
  dotY={dotY}
1859
1942
  palette={palette}
1860
- engine={engine}
1861
1943
  pulse={pulseCfg}
1862
1944
  radius={dotCfg.radius}
1863
1945
  ring={dotCfg.ring}
@@ -2064,6 +2146,19 @@ function ChartScrubLayer({
2064
2146
  );
2065
2147
  }
2066
2148
 
2149
+ /** A segment's time range and presentation uniquely identify its divider view. */
2150
+ function segmentReactKey(segment: ResolvedSegment): string {
2151
+ return [
2152
+ "seg",
2153
+ segment.from ?? "start",
2154
+ segment.to ?? "end",
2155
+ segment.divider,
2156
+ segment.dividerColor,
2157
+ segment.label ?? "",
2158
+ segment.labelPosition,
2159
+ ].join(":");
2160
+ }
2161
+
2067
2162
  /** Live-value text drawn as its own canvas layer, above both the area gradient
2068
2163
  * and the left-edge fade, so the large number stays crisp at the left edge
2069
2164
  * instead of being washed out by the fade's `dstOut` blend. */
@@ -2115,14 +2210,14 @@ function ChartBadgeLayer({
2115
2210
  }) {
2116
2211
  const {
2117
2212
  badgeFont,
2118
- reveal,
2119
2213
  engine,
2120
2214
  effectivePadding,
2121
2215
  palette,
2122
2216
  formatValue,
2123
- momentumSV,
2217
+ badgeMomentumSV,
2124
2218
  metricsCfg,
2125
2219
  yAxisFloat,
2220
+ liveBadgeOpacity,
2126
2221
  } = model;
2127
2222
  const badgeCfg = model.badgeCfg!;
2128
2223
  const badgeData = useBadge(
@@ -2133,7 +2228,7 @@ function ChartBadgeLayer({
2133
2228
  badgeFont,
2134
2229
  badgeCfg.variant,
2135
2230
  badgeCfg.tail,
2136
- momentumSV,
2231
+ badgeMomentumSV,
2137
2232
  badgeCfg.position,
2138
2233
  badgeCfg.background,
2139
2234
  metricsCfg.badge,
@@ -2146,7 +2241,7 @@ function ChartBadgeLayer({
2146
2241
  );
2147
2242
  return (
2148
2243
  <Group transform={degen?.shakeTransform}>
2149
- <Group opacity={reveal.badgeOpacity}>
2244
+ <Group opacity={liveBadgeOpacity}>
2150
2245
  <BadgeOverlay
2151
2246
  badge={badgeData}
2152
2247
  font={badgeFont}
@@ -2173,7 +2268,10 @@ function ChartRefBadgeLayer({
2173
2268
  }) {
2174
2269
  const {
2175
2270
  allRefLines,
2271
+ refLineKeys,
2176
2272
  refLineCustom,
2273
+ refLineOffAxisCustom,
2274
+ refLineCustomTagWidths,
2177
2275
  dragValues,
2178
2276
  groupHidden,
2179
2277
  refGroupResult,
@@ -2194,7 +2292,7 @@ function ChartRefBadgeLayer({
2194
2292
  <Group transform={degen?.shakeTransform} opacity={overlayScrubFade}>
2195
2293
  {allRefLines.map((rl, i) => (
2196
2294
  <ReferenceLineOverlay
2197
- key={i}
2295
+ key={refLineKeys[i]}
2198
2296
  engine={engine}
2199
2297
  padding={effectivePadding}
2200
2298
  line={rl}
@@ -2206,6 +2304,8 @@ function ChartRefBadgeLayer({
2206
2304
  dragValues={dragValues}
2207
2305
  index={i}
2208
2306
  suppressTag={refLineCustom[i]}
2307
+ suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
2308
+ customTagWidths={refLineCustomTagWidths}
2209
2309
  groupHidden={refGroupingActive ? groupHidden : undefined}
2210
2310
  />
2211
2311
  ))}
@@ -2276,8 +2376,11 @@ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2276
2376
  markerClusterCfg,
2277
2377
  renderMarker,
2278
2378
  renderReferenceLine,
2379
+ renderOffAxisReferenceLine,
2279
2380
  allRefLines,
2280
2381
  refLineCustom,
2382
+ refLineOffAxisCustom,
2383
+ refLineCustomTagWidths,
2281
2384
  dragValues,
2282
2385
  dragActive,
2283
2386
  engine,
@@ -2316,6 +2419,21 @@ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2316
2419
  formatValue={formatValue}
2317
2420
  dragValues={dragValues}
2318
2421
  dragActive={dragActive}
2422
+ tagWidths={refLineCustomTagWidths}
2423
+ />
2424
+ )}
2425
+ {renderOffAxisReferenceLine && allRefLines.length > 0 && (
2426
+ <CustomReferenceLineOverlay
2427
+ lines={allRefLines}
2428
+ renderReferenceLine={renderOffAxisReferenceLine}
2429
+ custom={refLineOffAxisCustom}
2430
+ engine={engine}
2431
+ padding={effectivePadding}
2432
+ formatValue={formatValue}
2433
+ dragValues={dragValues}
2434
+ dragActive={dragActive}
2435
+ tagWidths={refLineCustomTagWidths}
2436
+ offAxisOnly
2319
2437
  />
2320
2438
  )}
2321
2439
  </Animated.View>
@@ -2357,6 +2475,7 @@ function ChartView({
2357
2475
  renderTooltip,
2358
2476
  renderOverlay,
2359
2477
  renderReferenceLine,
2478
+ renderOffAxisReferenceLine,
2360
2479
  allRefLines,
2361
2480
  scrubCfg,
2362
2481
  crosshair,
@@ -2466,7 +2585,8 @@ function ChartView({
2466
2585
  Skia markers (the wrapper is full-bleed; children keep their own
2467
2586
  absolute positions). */}
2468
2587
  {((markersActive && renderMarker) ||
2469
- (renderReferenceLine && allRefLines.length > 0)) && (
2588
+ ((renderReferenceLine || renderOffAxisReferenceLine) &&
2589
+ allRefLines.length > 0)) && (
2470
2590
  <ChartCustomAnnotations model={model} />
2471
2591
  )}
2472
2592