react-native-livechart 4.10.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 (114) hide show
  1. package/README.md +24 -0
  2. package/dist/components/CrosshairLine.d.ts +5 -1
  3. package/dist/components/CrosshairLine.d.ts.map +1 -1
  4. package/dist/components/CrosshairOverlay.d.ts +3 -1
  5. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  6. package/dist/components/CustomReferenceLineOverlay.d.ts +19 -12
  7. package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.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/LeftEdgeFade.d.ts +3 -1
  12. package/dist/components/LeftEdgeFade.d.ts.map +1 -1
  13. package/dist/components/LiveChart.d.ts.map +1 -1
  14. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  15. package/dist/components/LoadingOverlay.d.ts +3 -1
  16. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  17. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  18. package/dist/components/MultiSeriesDots.d.ts +3 -1
  19. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  20. package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
  21. package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
  22. package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
  23. package/dist/components/MultiSeriesValueLines.d.ts +3 -1
  24. package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
  25. package/dist/components/PerSeriesTooltipOverlay.d.ts +19 -0
  26. package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -0
  27. package/dist/components/ReferenceLineOverlay.d.ts +20 -6
  28. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  29. package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
  30. package/dist/components/YAxisOverlay.d.ts +3 -1
  31. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  32. package/dist/components/thresholdSplitShaderSource.d.ts +13 -0
  33. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
  34. package/dist/core/liveIndicatorVisibility.d.ts +9 -0
  35. package/dist/core/liveIndicatorVisibility.d.ts.map +1 -0
  36. package/dist/core/resolveConfig.d.ts +29 -0
  37. package/dist/core/resolveConfig.d.ts.map +1 -1
  38. package/dist/core/useLiveChartEngine.d.ts +9 -1
  39. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  40. package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
  41. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  42. package/dist/draw/particleAtlas.d.ts +3 -2
  43. package/dist/draw/particleAtlas.d.ts.map +1 -1
  44. package/dist/hooks/crosshairSeries.d.ts +10 -2
  45. package/dist/hooks/crosshairSeries.d.ts.map +1 -1
  46. package/dist/hooks/crosshairShared.d.ts +37 -2
  47. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  48. package/dist/hooks/delayedPanGuard.d.ts +66 -8
  49. package/dist/hooks/delayedPanGuard.d.ts.map +1 -1
  50. package/dist/hooks/useChartPaths.d.ts +5 -9
  51. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  52. package/dist/hooks/useCrosshair.d.ts +7 -1
  53. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  54. package/dist/hooks/useCrosshairSeries.d.ts +22 -3
  55. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  56. package/dist/hooks/useMomentum.d.ts +1 -1
  57. package/dist/hooks/useMomentum.d.ts.map +1 -1
  58. package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
  59. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  60. package/dist/hooks/usePanScroll.d.ts +15 -1
  61. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/math/lerp.d.ts.map +1 -1
  65. package/dist/math/momentum.d.ts +1 -1
  66. package/dist/math/momentum.d.ts.map +1 -1
  67. package/dist/math/referenceLines.d.ts +7 -0
  68. package/dist/math/referenceLines.d.ts.map +1 -1
  69. package/dist/math/squiggly.d.ts +6 -3
  70. package/dist/math/squiggly.d.ts.map +1 -1
  71. package/dist/math/threshold.d.ts +2 -2
  72. package/dist/types.d.ts +184 -16
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/src/components/CrosshairLine.tsx +21 -1
  76. package/src/components/CrosshairOverlay.tsx +48 -35
  77. package/src/components/CustomReferenceLineOverlay.tsx +58 -20
  78. package/src/components/DegenParticlesOverlay.tsx +40 -5
  79. package/src/components/DotOverlay.tsx +24 -8
  80. package/src/components/LeftEdgeFade.tsx +12 -6
  81. package/src/components/LiveChart.tsx +186 -53
  82. package/src/components/LiveChartSeries.tsx +214 -29
  83. package/src/components/LoadingOverlay.tsx +26 -12
  84. package/src/components/MarkerOverlay.tsx +20 -3
  85. package/src/components/MultiSeriesDots.tsx +4 -2
  86. package/src/components/MultiSeriesTooltipStack.tsx +4 -2
  87. package/src/components/MultiSeriesValueLabels.tsx +4 -2
  88. package/src/components/MultiSeriesValueLines.tsx +4 -2
  89. package/src/components/PerSeriesTooltipOverlay.tsx +287 -0
  90. package/src/components/ReferenceLineOverlay.tsx +274 -141
  91. package/src/components/ThresholdSplitShader.tsx +5 -37
  92. package/src/components/YAxisOverlay.tsx +5 -1
  93. package/src/components/thresholdSplitShaderSource.ts +70 -0
  94. package/src/core/liveIndicatorVisibility.ts +25 -0
  95. package/src/core/resolveConfig.ts +86 -0
  96. package/src/core/useLiveChartEngine.ts +110 -42
  97. package/src/core/useLiveChartSeriesEngine.ts +137 -70
  98. package/src/draw/particleAtlas.ts +21 -10
  99. package/src/hooks/crosshairSeries.ts +262 -33
  100. package/src/hooks/crosshairShared.ts +41 -2
  101. package/src/hooks/delayedPanGuard.ts +111 -6
  102. package/src/hooks/useChartPaths.ts +36 -15
  103. package/src/hooks/useCrosshair.ts +73 -14
  104. package/src/hooks/useCrosshairSeries.ts +106 -13
  105. package/src/hooks/useMomentum.ts +8 -1
  106. package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
  107. package/src/hooks/usePanScroll.ts +46 -5
  108. package/src/index.ts +2 -0
  109. package/src/math/lerp.ts +7 -0
  110. package/src/math/momentum.ts +22 -5
  111. package/src/math/referenceLines.ts +38 -0
  112. package/src/math/squiggly.ts +28 -11
  113. package/src/math/threshold.ts +2 -2
  114. package/src/types.ts +197 -17
@@ -1,10 +1,4 @@
1
- import {
2
- useEffect,
3
- useLayoutEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from "react";
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
8
2
  import { StyleSheet, View } from "react-native";
9
3
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
10
4
  import Animated, {
@@ -27,6 +21,7 @@ import {
27
21
  Group,
28
22
  LinearGradient,
29
23
  Path,
24
+ Rect,
30
25
  vec,
31
26
  } from "@shopify/react-native-skia";
32
27
 
@@ -63,7 +58,11 @@ import {
63
58
  resolveYAxis,
64
59
  } from "../core/resolveConfig";
65
60
  import type { ResolvedThresholdConfig } from "../core/resolveConfig";
66
- import { resolveSegment } from "../core/resolveSegment";
61
+ import {
62
+ liveIndicatorScrollOpacity,
63
+ resolveHideLiveOnScrollBack,
64
+ } from "../core/liveIndicatorVisibility";
65
+ import { resolveSegment, type ResolvedSegment } from "../core/resolveSegment";
67
66
  import { useLiveChartEngine } from "../core/useLiveChartEngine";
68
67
  import { pulseRadialOutset } from "../draw/line";
69
68
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
@@ -83,7 +82,10 @@ import { useMarkers } from "../hooks/useMarkers";
83
82
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
84
83
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
85
84
  import { useModeBlend } from "../hooks/useModeBlend";
86
- import { useMomentum } from "../hooks/useMomentum";
85
+ import {
86
+ resolveMomentumProp,
87
+ useMomentum,
88
+ } from "../hooks/useMomentum";
87
89
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
88
90
  import { usePinchZoom } from "../hooks/usePinchZoom";
89
91
  import { useVisibleRange } from "../hooks/useVisibleRange";
@@ -110,6 +112,7 @@ import {
110
112
  import {
111
113
  collectReferenceValues,
112
114
  referenceLineForm,
115
+ referenceLineReactKeys,
113
116
  resolveReferenceGroupBadge,
114
117
  } from "../math/referenceLines";
115
118
  import {
@@ -253,6 +256,7 @@ function useLiveChartController({
253
256
  font: fontProp,
254
257
  insets,
255
258
  style,
259
+ canvasMode = "transparent",
256
260
 
257
261
  // ── Candlestick ─────────────────────────────────────────────────────────
258
262
  mode = "line",
@@ -315,6 +319,7 @@ function useLiveChartController({
315
319
  renderTooltip,
316
320
  renderOverlay,
317
321
  renderReferenceLine,
322
+ renderOffAxisReferenceLine,
318
323
  referenceLineGrouping,
319
324
  leftEdgeFade = true,
320
325
 
@@ -391,18 +396,18 @@ function useLiveChartController({
391
396
  const seededRef = useRef<(number | undefined)[]>([]);
392
397
  const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
393
398
  useEffect(() => {
394
- const active = dragActive.value;
395
- const cur = dragValues.value;
399
+ const active = dragActive.get();
400
+ const cur = dragValues.get();
396
401
  const seeded = seededRef.current;
397
- dragValues.value = allRefLines.map((l, i) => {
402
+ dragValues.set(allRefLines.map((l, i) => {
398
403
  const prop = l.value ?? 0;
399
404
  if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
400
405
  if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
401
406
  return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
402
- });
407
+ }));
403
408
  seededRef.current = allRefLines.map((l) => l.value);
404
- if (dragActive.value.length !== allRefLines.length) {
405
- dragActive.value = allRefLines.map((_, i) => active[i] ?? false);
409
+ if (dragActive.get().length !== allRefLines.length) {
410
+ dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
406
411
  }
407
412
  // allRefLines is rebuilt every render; key off the value signature + length.
408
413
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -414,6 +419,17 @@ function useLiveChartController({
414
419
  allRefLines,
415
420
  renderReferenceLine,
416
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[]>([]);
417
433
 
418
434
  // Live Y values of the *draggable* Form-A lines, folded into the engine's
419
435
  // axis-range fit so dragging a line toward / past the visible edge expands the
@@ -425,13 +441,17 @@ function useLiveChartController({
425
441
  // contributes its live value; a series contributes its window min/max
426
442
  // (respecting `extendToNow`), so an off-range break-even expands the axis and
427
443
  // stays on-plot like a reference line would.
428
- const draggableRefIdx = allRefLines
429
- .map((l, i) =>
430
- l.draggable && !l.excludeFromRange && referenceLineForm(l) === "line"
431
- ? i
432
- : -1,
433
- )
434
- .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
+ }
435
455
  const thresholdInRange = thresholdCfg?.includeInRange === true;
436
456
  // Constant benchmark → its live value rides this channel. A series threshold
437
457
  // is folded inside the engine tick instead (`thresholdRangePoints`), which
@@ -576,6 +596,7 @@ function useLiveChartController({
576
596
  // react-doctor-disable-next-line react-doctor/no-derived-state-effect -- Reanimated: must read the SharedValue off the render path
577
597
  useLayoutEffect(() => {
578
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
579
600
  setValueLayoutSample(value.get());
580
601
  }, [value]);
581
602
 
@@ -725,7 +746,7 @@ function useLiveChartController({
725
746
  if (
726
747
  referenceLineForm(l) !== "line" ||
727
748
  l.value === undefined ||
728
- refLineCustom[i]
749
+ refLineCustom[i] || refLineOffAxisCustom[i]
729
750
  ) {
730
751
  ys.push(-1);
731
752
  continue;
@@ -870,11 +891,8 @@ function useLiveChartController({
870
891
  // polyline passed as the last arg below).
871
892
  thresholdCfg?.fill && !thresholdIsSeries ? thresholdGeom.lineY : undefined,
872
893
  lineIsLinear,
873
- // Tip the line at the view-edge price (not the live value) while scrolled,
874
- // matching the live dot so the right edge doesn't drop to the off-screen
875
- // live value when `followViewEdge` tracks the scrolled-back window.
876
- engine.edgeValue,
877
- badgeCfg?.followViewEdge ?? false,
894
+ // The plotted line independently selects the visible edge while historical;
895
+ // badge/live-indicator options affect overlays only.
878
896
  // Match the standalone loading squiggle's wave during the reveal morph.
879
897
  loadingCfg?.amplitude,
880
898
  loadingCfg?.speed,
@@ -910,6 +928,15 @@ function useLiveChartController({
910
928
  );
911
929
 
912
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
+ );
913
940
 
914
941
  // ── Overlay hooks ─────────────────────────────────────────────────────
915
942
  // Scrub/crosshair must see the same stash-backed candles as the engine.
@@ -1001,6 +1028,14 @@ function useLiveChartController({
1001
1028
  ? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
1002
1029
  : (scrubCfg?.panGestureDelay ?? 0);
1003
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
+
1004
1039
  const crosshair = useCrosshair(
1005
1040
  engine,
1006
1041
  effectivePadding,
@@ -1030,6 +1065,7 @@ function useLiveChartController({
1030
1065
  timeScrollEnabled && scrollGestureMode === "axisDrag"
1031
1066
  ? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
1032
1067
  : 0,
1068
+ scrollActive,
1033
1069
  );
1034
1070
 
1035
1071
  // Capture only the shared value in the worklets below. Referencing
@@ -1052,6 +1088,10 @@ function useLiveChartController({
1052
1088
  minTime: scrollMinTime,
1053
1089
  enabled: timeScrollEnabled,
1054
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,
1055
1095
  // Clear any live crosshair when a scroll drag takes over.
1056
1096
  onScrollStart: () => {
1057
1097
  "worklet";
@@ -1176,10 +1216,40 @@ function useLiveChartController({
1176
1216
  // point instead — otherwise both dots show at once. Applies on static charts
1177
1217
  // too, now that they're scrubbable.
1178
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
+ );
1179
1227
  const liveDotOpacity = useDerivedValue(
1180
1228
  () =>
1181
1229
  reveal.dotOpacity.value *
1182
- (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
+ ),
1183
1253
  );
1184
1254
 
1185
1255
  // Fade the annotation overlays (markers + reference lines) out while scrubbing
@@ -1204,6 +1274,7 @@ function useLiveChartController({
1204
1274
  return {
1205
1275
  // passthrough props the render needs
1206
1276
  style,
1277
+ canvasMode,
1207
1278
  accessibilityLabel,
1208
1279
  accessibilityRole,
1209
1280
  emptyText,
@@ -1238,10 +1309,14 @@ function useLiveChartController({
1238
1309
  leftEdgeFadeCfg,
1239
1310
  metricsCfg,
1240
1311
  allRefLines,
1312
+ refLineKeys,
1241
1313
  refLineCustom,
1314
+ refLineOffAxisCustom,
1315
+ refLineCustomTagWidths,
1242
1316
  dragValues,
1243
1317
  dragActive,
1244
1318
  renderReferenceLine,
1319
+ renderOffAxisReferenceLine,
1245
1320
  refGroupingActive: refGroupingRadius != null,
1246
1321
  refGroupResult,
1247
1322
  groupHidden,
@@ -1314,9 +1389,12 @@ function useLiveChartController({
1314
1389
  dotX,
1315
1390
  dotY,
1316
1391
  liveDotOpacity,
1392
+ valueLineOpacity,
1393
+ liveBadgeOpacity,
1317
1394
  overlayScrubFade,
1318
1395
  markerGroupOpacity,
1319
1396
  momentumSV,
1397
+ badgeMomentumSV,
1320
1398
  onDegenShake,
1321
1399
  crosshair,
1322
1400
  rootGesture,
@@ -1362,9 +1440,7 @@ function ChartWithDegen({
1362
1440
  onDegenShake,
1363
1441
  isStatic,
1364
1442
  );
1365
- return (
1366
- <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />
1367
- );
1443
+ return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
1368
1444
  }
1369
1445
 
1370
1446
  /**
@@ -1419,6 +1495,7 @@ function ChartYAxisLayer({
1419
1495
  metricsCfg,
1420
1496
  gridStyleCfg,
1421
1497
  yAxisFloat,
1498
+ liveBadgeOpacity,
1422
1499
  } = model;
1423
1500
  return (
1424
1501
  <Group opacity={reveal.yAxisOpacity}>
@@ -1436,6 +1513,7 @@ function ChartYAxisLayer({
1436
1513
  badgeCenterY={badgeUsesRightGutter ? dotY : undefined}
1437
1514
  badgeFontSize={badgeUsesRightGutter ? badgeFont.getSize() : undefined}
1438
1515
  badgeOffsetY={badgeCfg?.offsetY ?? 0}
1516
+ badgeOpacity={badgeUsesRightGutter ? liveBadgeOpacity : undefined}
1439
1517
  gridStyle={gridStyleCfg}
1440
1518
  />
1441
1519
  </Group>
@@ -1675,8 +1753,10 @@ function ChartStack({
1675
1753
  fontProp,
1676
1754
  badgeCfg,
1677
1755
  valueLineCfg,
1756
+ valueLineOpacity,
1678
1757
  dotY,
1679
1758
  allRefLines,
1759
+ refLineKeys,
1680
1760
  dragValues,
1681
1761
  resolvedSegments,
1682
1762
  hasRecolorSegments,
@@ -1721,14 +1801,15 @@ function ChartStack({
1721
1801
  loadingStrokeWidth,
1722
1802
  loadingAmplitude,
1723
1803
  loadingSpeed,
1804
+ canvasMode,
1724
1805
  } = model;
1725
1806
  return (
1726
1807
  <Group transform={degen?.shakeTransform}>
1727
1808
  {/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
1728
1809
  painted on the line stroke itself, below — this overlay draws no fill. */}
1729
- {resolvedSegments.map((seg, i) => (
1810
+ {resolvedSegments.map((seg) => (
1730
1811
  <SegmentDividerOverlay
1731
- key={`seg-${seg.from ?? "start"}-${seg.to ?? "end"}-${i}`}
1812
+ key={segmentReactKey(seg)}
1732
1813
  engine={engine}
1733
1814
  padding={effectivePadding}
1734
1815
  segment={seg}
@@ -1738,7 +1819,7 @@ function ChartStack({
1738
1819
 
1739
1820
  {/* Value line + reference line (behind chart line) */}
1740
1821
  {valueLineCfg && (
1741
- <Group opacity={reveal.lineOpacity}>
1822
+ <Group opacity={valueLineOpacity}>
1742
1823
  <ValueLineOverlay
1743
1824
  dotY={dotY}
1744
1825
  engine={engine}
@@ -1750,14 +1831,12 @@ function ChartStack({
1750
1831
  </Group>
1751
1832
  )}
1752
1833
 
1753
- {/* Index keys: reference lines are a positional array and two may share
1754
- value + label (e.g. duplicate working orders at the same price), which a
1755
- content-derived key would collapse to one. Wrapped in a fade group so
1756
- `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. */}
1757
1836
  <Group opacity={overlayScrubFade}>
1758
1837
  {allRefLines.map((rl, i) => (
1759
1838
  <ReferenceLineOverlay
1760
- key={i}
1839
+ key={refLineKeys[i]}
1761
1840
  engine={engine}
1762
1841
  padding={effectivePadding}
1763
1842
  line={rl}
@@ -1861,7 +1940,6 @@ function ChartStack({
1861
1940
  dotX={dotX}
1862
1941
  dotY={dotY}
1863
1942
  palette={palette}
1864
- engine={engine}
1865
1943
  pulse={pulseCfg}
1866
1944
  radius={dotCfg.radius}
1867
1945
  ring={dotCfg.ring}
@@ -1939,6 +2017,7 @@ function ChartStack({
1939
2017
  lineStrokeWidth={loadingStrokeWidth}
1940
2018
  waveAmplitude={loadingAmplitude}
1941
2019
  waveSpeed={loadingSpeed}
2020
+ opaqueCanvas={canvasMode === "opaque"}
1942
2021
  />
1943
2022
  </Group>
1944
2023
  );
@@ -2003,6 +2082,7 @@ function ChartScrubLayer({
2003
2082
  selectionDot,
2004
2083
  selectionColor,
2005
2084
  renderTooltip,
2085
+ canvasMode,
2006
2086
  } = model;
2007
2087
  // A custom tooltip is an RN overlay (sibling of <Canvas>), so the built-in
2008
2088
  // Skia tooltip is suppressed here while it's active — the line pill in line
@@ -2047,6 +2127,7 @@ function ChartScrubLayer({
2047
2127
  tooltipBorderRadius={scrubCfg.tooltipBorderRadius}
2048
2128
  tooltipShowValue={scrubCfg.tooltipShowValue}
2049
2129
  tooltipShowTime={scrubCfg.tooltipShowTime}
2130
+ opaqueCanvas={canvasMode === "opaque"}
2050
2131
  >
2051
2132
  {/* Candle charts render a multi-line OHLC tooltip; the line
2052
2133
  chart falls back to CrosshairOverlay's default value/time
@@ -2065,6 +2146,19 @@ function ChartScrubLayer({
2065
2146
  );
2066
2147
  }
2067
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
+
2068
2162
  /** Live-value text drawn as its own canvas layer, above both the area gradient
2069
2163
  * and the left-edge fade, so the large number stays crisp at the left edge
2070
2164
  * instead of being washed out by the fade's `dstOut` blend. */
@@ -2116,14 +2210,14 @@ function ChartBadgeLayer({
2116
2210
  }) {
2117
2211
  const {
2118
2212
  badgeFont,
2119
- reveal,
2120
2213
  engine,
2121
2214
  effectivePadding,
2122
2215
  palette,
2123
2216
  formatValue,
2124
- momentumSV,
2217
+ badgeMomentumSV,
2125
2218
  metricsCfg,
2126
2219
  yAxisFloat,
2220
+ liveBadgeOpacity,
2127
2221
  } = model;
2128
2222
  const badgeCfg = model.badgeCfg!;
2129
2223
  const badgeData = useBadge(
@@ -2134,7 +2228,7 @@ function ChartBadgeLayer({
2134
2228
  badgeFont,
2135
2229
  badgeCfg.variant,
2136
2230
  badgeCfg.tail,
2137
- momentumSV,
2231
+ badgeMomentumSV,
2138
2232
  badgeCfg.position,
2139
2233
  badgeCfg.background,
2140
2234
  metricsCfg.badge,
@@ -2147,7 +2241,7 @@ function ChartBadgeLayer({
2147
2241
  );
2148
2242
  return (
2149
2243
  <Group transform={degen?.shakeTransform}>
2150
- <Group opacity={reveal.badgeOpacity}>
2244
+ <Group opacity={liveBadgeOpacity}>
2151
2245
  <BadgeOverlay
2152
2246
  badge={badgeData}
2153
2247
  font={badgeFont}
@@ -2174,7 +2268,10 @@ function ChartRefBadgeLayer({
2174
2268
  }) {
2175
2269
  const {
2176
2270
  allRefLines,
2271
+ refLineKeys,
2177
2272
  refLineCustom,
2273
+ refLineOffAxisCustom,
2274
+ refLineCustomTagWidths,
2178
2275
  dragValues,
2179
2276
  groupHidden,
2180
2277
  refGroupResult,
@@ -2195,7 +2292,7 @@ function ChartRefBadgeLayer({
2195
2292
  <Group transform={degen?.shakeTransform} opacity={overlayScrubFade}>
2196
2293
  {allRefLines.map((rl, i) => (
2197
2294
  <ReferenceLineOverlay
2198
- key={i}
2295
+ key={refLineKeys[i]}
2199
2296
  engine={engine}
2200
2297
  padding={effectivePadding}
2201
2298
  line={rl}
@@ -2207,6 +2304,8 @@ function ChartRefBadgeLayer({
2207
2304
  dragValues={dragValues}
2208
2305
  index={i}
2209
2306
  suppressTag={refLineCustom[i]}
2307
+ suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
2308
+ customTagWidths={refLineCustomTagWidths}
2210
2309
  groupHidden={refGroupingActive ? groupHidden : undefined}
2211
2310
  />
2212
2311
  ))}
@@ -2277,8 +2376,11 @@ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2277
2376
  markerClusterCfg,
2278
2377
  renderMarker,
2279
2378
  renderReferenceLine,
2379
+ renderOffAxisReferenceLine,
2280
2380
  allRefLines,
2281
2381
  refLineCustom,
2382
+ refLineOffAxisCustom,
2383
+ refLineCustomTagWidths,
2282
2384
  dragValues,
2283
2385
  dragActive,
2284
2386
  engine,
@@ -2317,6 +2419,21 @@ function ChartCustomAnnotations({ model }: { model: LiveChartModel }) {
2317
2419
  formatValue={formatValue}
2318
2420
  dragValues={dragValues}
2319
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
2320
2437
  />
2321
2438
  )}
2322
2439
  </Animated.View>
@@ -2358,12 +2475,14 @@ function ChartView({
2358
2475
  renderTooltip,
2359
2476
  renderOverlay,
2360
2477
  renderReferenceLine,
2478
+ renderOffAxisReferenceLine,
2361
2479
  allRefLines,
2362
2480
  scrubCfg,
2363
2481
  crosshair,
2364
2482
  extremaTimeOffset,
2365
2483
  topConnector,
2366
2484
  bottomConnector,
2485
+ canvasMode,
2367
2486
  } = model;
2368
2487
 
2369
2488
  return (
@@ -2375,7 +2494,21 @@ function ChartView({
2375
2494
  accessibilityLabel={accessibilityLabel}
2376
2495
  accessibilityRole={accessibilityRole}
2377
2496
  >
2378
- <Canvas style={{ flex: 1 }}>
2497
+ {/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
2498
+ <Canvas
2499
+ key={canvasMode}
2500
+ style={{ flex: 1 }}
2501
+ opaque={canvasMode === "opaque"}
2502
+ >
2503
+ {canvasMode === "opaque" && (
2504
+ <Rect
2505
+ x={0}
2506
+ y={0}
2507
+ width={engine.canvasWidth}
2508
+ height={engine.canvasHeight}
2509
+ color={backgroundColor}
2510
+ />
2511
+ )}
2379
2512
  {/* Background fills first, then the left-edge fade (a canvas-space sibling
2380
2513
  so dstOut blends correctly), then the line stack on top — so the fade
2381
2514
  softens only the fills and the line stays crisp at the left edge. */}
@@ -2392,15 +2525,14 @@ function ChartView({
2392
2525
  startColor={leftEdgeFadeCfg.startColor}
2393
2526
  endColor={leftEdgeFadeCfg.endColor}
2394
2527
  engine={engine}
2528
+ opaqueBackgroundRgb={
2529
+ canvasMode === "opaque" ? palette.bgRgb : undefined
2530
+ }
2395
2531
  />
2396
2532
  )}
2397
2533
 
2398
2534
  {/* Line stack above the fade so the line stays crisp at the left edge. */}
2399
- <ChartStack
2400
- model={model}
2401
- yAxisEntries={yAxisEntries}
2402
- degen={degen}
2403
- />
2535
+ <ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
2404
2536
 
2405
2537
  {/* "extrema-edge" connector lines (dot → edge readout), above the chart
2406
2538
  content so the dashed guide reads over the line / candles. */}
@@ -2453,7 +2585,8 @@ function ChartView({
2453
2585
  Skia markers (the wrapper is full-bleed; children keep their own
2454
2586
  absolute positions). */}
2455
2587
  {((markersActive && renderMarker) ||
2456
- (renderReferenceLine && allRefLines.length > 0)) && (
2588
+ ((renderReferenceLine || renderOffAxisReferenceLine) &&
2589
+ allRefLines.length > 0)) && (
2457
2590
  <ChartCustomAnnotations model={model} />
2458
2591
  )}
2459
2592