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
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @see https://github.com/benjitaylor/liveline
6
6
  */
7
- import { Canvas, Group } from "@shopify/react-native-skia";
7
+ import { Canvas, Group, Rect } from "@shopify/react-native-skia";
8
8
  import { useLayoutEffect, useState } from "react";
9
9
  import { StyleSheet, View } from "react-native";
10
10
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
@@ -28,7 +28,6 @@ import {
28
28
  lineStyleSignatureFromArray,
29
29
  resolveMultiSeriesLineColorsSnapshot,
30
30
  resolveMultiSeriesLineStylesSnapshot,
31
- type SeriesLineStyle,
32
31
  } from "../core/multiSeriesLayout";
33
32
  import {
34
33
  resolveAxisLabel,
@@ -53,6 +52,7 @@ import { pulseRadialOutset } from "../draw/line";
53
52
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
54
53
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
55
54
  import { useChartReveal } from "../hooks/useChartReveal";
55
+ import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
56
56
  import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
57
57
  import { useCrosshairSeries } from "../hooks/useCrosshairSeries";
58
58
  import { useMarkers } from "../hooks/useMarkers";
@@ -70,7 +70,10 @@ import {
70
70
  } from "../lib/format";
71
71
  import { measureFontTextWidth } from "../lib/measureFontTextWidth";
72
72
  import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
73
- import { collectReferenceValues } from "../math/referenceLines";
73
+ import {
74
+ collectReferenceValues,
75
+ referenceLineReactKeys,
76
+ } from "../math/referenceLines";
74
77
  import {
75
78
  applyPaletteOverride,
76
79
  leftEdgeFadeColorsFromBgRgb,
@@ -78,11 +81,16 @@ import {
78
81
  } from "../theme";
79
82
  import type { LiveChartSeriesProps, Marker, SeriesConfig } from "../types";
80
83
  import { AxisLabelOverlay } from "./AxisLabelOverlay";
84
+ import { ChartOverlayLayer } from "./ChartOverlayLayer";
81
85
  import {
82
86
  ExtremaConnectorOverlay,
83
87
  labelConnector,
84
88
  } from "./ExtremaConnectorOverlay";
85
89
  import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
90
+ import {
91
+ CustomReferenceLineOverlay,
92
+ customReferenceLineFlags,
93
+ } from "./CustomReferenceLineOverlay";
86
94
  import { CrosshairLine } from "./CrosshairLine";
87
95
  import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
88
96
  import { LeftEdgeFade } from "./LeftEdgeFade";
@@ -92,6 +100,7 @@ import { MultiSeriesDots } from "./MultiSeriesDots";
92
100
  import { MultiSeriesStroke } from "./MultiSeriesStroke";
93
101
  import { MultiSeriesValueLabels } from "./MultiSeriesValueLabels";
94
102
  import { MultiSeriesValueLines } from "./MultiSeriesValueLines";
103
+ import { PerSeriesTooltipOverlay } from "./PerSeriesTooltipOverlay";
95
104
  import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
96
105
  import { SeriesToggleChips } from "./SeriesToggleChips";
97
106
  import { XAxisOverlay } from "./XAxisOverlay";
@@ -128,6 +137,7 @@ function useLiveChartSeriesController({
128
137
  font: fontProp,
129
138
  insets,
130
139
  style,
140
+ canvasMode = "transparent",
131
141
  timeWindow = 30,
132
142
  paused = false,
133
143
  loading = false,
@@ -172,6 +182,9 @@ function useLiveChartSeriesController({
172
182
  markerHitRadius = 16,
173
183
  markerCluster,
174
184
  renderMarker,
185
+ renderOverlay,
186
+ renderReferenceLine,
187
+ renderOffAxisReferenceLine,
175
188
  leftEdgeFade = true,
176
189
  }: LiveChartSeriesProps) {
177
190
  const emptyMarkers = useSharedValue<Marker[]>([]);
@@ -184,6 +197,10 @@ function useLiveChartSeriesController({
184
197
  const bottomLabelCfg = resolveAxisLabel(bottomLabel);
185
198
  const scrubCfg = resolveScrub(scrub);
186
199
  const scrubEnabled = scrubCfg !== null;
200
+ // Opt-in and subordinate to the existing master `tooltip` switch. Keeping
201
+ // this null by default preserves LiveChartSeries' historical guide-only scrub.
202
+ const seriesTooltipCfg =
203
+ scrubCfg?.tooltip === true ? scrubCfg.seriesTooltip : null;
187
204
 
188
205
  // Time-scroll + pinch-zoom (mirrors LiveChart). LiveChartSeries has no static
189
206
  // mode, so these gate on the props alone. `holdToScrub` requires the scrub
@@ -210,8 +227,8 @@ function useLiveChartSeriesController({
210
227
 
211
228
  // Multi-series defaults the scrub selection dot OFF: it can only track one
212
229
  // line (the leading series), which reads as a bug next to the other series.
213
- // The crosshair line + per-series tooltip stack already mark the scrub point.
214
- // Passing `selectionDot` explicitly (true / config) still opts it in.
230
+ // The crosshair line (and the opt-in tooltip's per-series intersection dots)
231
+ // mark the scrub point. Passing `selectionDot` explicitly still opts it in.
215
232
  const selectionDotCfg = resolveSelectionDot(selectionDot ?? false);
216
233
  const gridStyleCfg = resolveGridStyle(gridStyle);
217
234
  const dotCfg = resolveMultiSeriesDot(dotProp);
@@ -224,6 +241,24 @@ function useLiveChartSeriesController({
224
241
 
225
242
  const allRefLines = referenceLines ?? [];
226
243
  const refValues = collectReferenceValues(allRefLines);
244
+ // Form-A lines a custom renderer owns keep their line stroke but suppress the
245
+ // built-in Skia tag. The callback is probed per line on the JS thread, matching
246
+ // LiveChart and CustomMarkerOverlay's per-item fallback model.
247
+ const refLineCustom = customReferenceLineFlags(
248
+ allRefLines,
249
+ renderReferenceLine,
250
+ );
251
+ // A full custom tag owns both states, so it takes precedence over an
252
+ // off-axis-only renderer for the same line.
253
+ const refLineOffAxisCustom = customReferenceLineFlags(
254
+ allRefLines,
255
+ renderOffAxisReferenceLine,
256
+ "off-axis",
257
+ ).map((custom, index) => custom && !refLineCustom[index]);
258
+ const refLineKeys = referenceLineReactKeys(allRefLines);
259
+ // RN custom tags report their measured widths here so the Skia connector can
260
+ // start after the native badge instead of the hidden built-in pill.
261
+ const refLineCustomTagWidths = useSharedValue<number[]>([]);
227
262
 
228
263
  const palette = applyPaletteOverride(
229
264
  resolveTheme(accentColor, theme),
@@ -253,9 +288,15 @@ function useLiveChartSeriesController({
253
288
  // render scope for memoization, which trips Reanimated's strict-mode warning;
254
289
  // it leaves the `.get()` method call inside the effect.
255
290
  // react-doctor-disable-next-line react-hooks-js/set-state-in-effect -- Reanimated: seeding from a SharedValue off render is the warning-free path
291
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Reanimated: seed from the SharedValue outside render to avoid strict-mode access warnings
256
292
  setSeriesSnapshot(series.get().slice());
257
293
  }, [series]);
258
294
 
295
+ // Mount per-series drawing worklets only for real series. The previous fixed
296
+ // 12-slot render kept 144 derived-value mappers alive for the default stroke,
297
+ // dot, and value-label layers even when a chart contained only one line.
298
+ const activeSeriesCount = Math.min(seriesSnapshot.length, MAX_MULTI_SERIES);
299
+
259
300
  const maxSeriesLabelWidth = dotCfg.valueLabel
260
301
  ? Math.max(
261
302
  0,
@@ -336,7 +377,11 @@ function useLiveChartSeriesController({
336
377
  nowOverride,
337
378
  });
338
379
  const { layoutHeight, onLayout } = useCanvasLayout(engine);
339
- const linePaths = useMultiSeriesLinePaths(engine, effectivePadding);
380
+ const linePaths = useMultiSeriesLinePaths(
381
+ engine,
382
+ effectivePadding,
383
+ activeSeriesCount,
384
+ );
340
385
 
341
386
  // Per-series colors and stroke styles, derived from the off-render snapshot
342
387
  // (React state — so no Reanimated read-during-render). The reaction below
@@ -385,6 +430,20 @@ function useLiveChartSeriesController({
385
430
  skiaFont,
386
431
  );
387
432
 
433
+ // Cross-gesture arbitration for the one-finger touch. `Gesture.Race` below is
434
+ // NOT arbitration — RNGH's Race adds no relation between its children, so both
435
+ // pans recognize independently and each can activate while the other already
436
+ // owns the touch. This latch (written by the scroll pan, read by the scrub's
437
+ // long-press guard) makes "the scroll already won" a hard fact; `scrubActive`
438
+ // (written by the crosshair, read by the scroll pan) is the mirror image.
439
+ const scrollActive = useSharedValue(false);
440
+ // `liveEdge` includes the optional right breathing-room buffer. The time pill
441
+ // must clamp against the real "now" anchor so its live bucket never ends in
442
+ // that future buffer.
443
+ const tooltipMaxTime = useDerivedValue(
444
+ () => engine.liveEdge.get() - windowBuffer * timeWindow,
445
+ );
446
+
388
447
  const crosshair = useCrosshairSeries(
389
448
  engine,
390
449
  effectivePadding,
@@ -393,6 +452,17 @@ function useLiveChartSeriesController({
393
452
  scrubHoldMs,
394
453
  onGestureStart,
395
454
  onGestureEnd,
455
+ scrollActive,
456
+ seriesTooltipCfg
457
+ ? {
458
+ config: seriesTooltipCfg,
459
+ formatValue,
460
+ formatTime,
461
+ font: skiaFont,
462
+ colors: lineColors,
463
+ maxTime: tooltipMaxTime,
464
+ }
465
+ : undefined,
396
466
  );
397
467
 
398
468
  // Capture only the shared value in the worklets below. Referencing
@@ -436,6 +506,10 @@ function useLiveChartSeriesController({
436
506
  minTime: scrollMinTime,
437
507
  enabled: timeScrollEnabled,
438
508
  mode: scrollGestureMode,
509
+ scrollActive,
510
+ // Once a scrub is engaged the chart is locked: scrolling goes inert so the
511
+ // finger only moves the price indicator across a fixed window.
512
+ scrubActive: crosshairScrubActive,
439
513
  onScrollStart: () => {
440
514
  "worklet";
441
515
  crosshairScrubActive.set(false);
@@ -503,6 +577,7 @@ function useLiveChartSeriesController({
503
577
  // passthrough props the render needs
504
578
  series,
505
579
  style,
580
+ canvasMode,
506
581
  accessibilityLabel,
507
582
  accessibilityRole,
508
583
  emptyText,
@@ -512,6 +587,7 @@ function useLiveChartSeriesController({
512
587
  yAxisCfg,
513
588
  xAxisCfg,
514
589
  scrubCfg,
590
+ seriesTooltipCfg,
515
591
  gridStyleCfg,
516
592
  dotCfg,
517
593
  dotOuterRadius,
@@ -519,6 +595,10 @@ function useLiveChartSeriesController({
519
595
  degenCfg,
520
596
  metricsCfg,
521
597
  allRefLines,
598
+ refLineKeys,
599
+ refLineCustom,
600
+ refLineOffAxisCustom,
601
+ refLineCustomTagWidths,
522
602
  leftEdgeFadeCfg,
523
603
  // theme / layout / fonts
524
604
  palette,
@@ -540,6 +620,7 @@ function useLiveChartSeriesController({
540
620
  layoutHeight,
541
621
  onLayout,
542
622
  linePaths,
623
+ activeSeriesCount,
543
624
  lineColors,
544
625
  lineStyles,
545
626
  degenPack,
@@ -555,6 +636,9 @@ function useLiveChartSeriesController({
555
636
  markerGroupOpacity,
556
637
  overlayScrubFade,
557
638
  renderMarker,
639
+ renderOverlay,
640
+ renderReferenceLine,
641
+ renderOffAxisReferenceLine,
558
642
  // selection dot: resolved config + fallback color (the leading series' color)
559
643
  selectionDot: selectionDotCfg,
560
644
  selectionColor: lineColors[0],
@@ -610,6 +694,9 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
610
694
  loadingStrokeWidth,
611
695
  loadingAmplitude,
612
696
  loadingSpeed,
697
+ canvasMode,
698
+ activeSeriesCount,
699
+ refLineKeys,
613
700
  } = model;
614
701
 
615
702
  return (
@@ -629,14 +716,12 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
629
716
  </Group>
630
717
  )}
631
718
 
632
- {/* Index keys: reference lines are a positional array and two may share
633
- value + label (e.g. duplicate working orders at the same price), which a
634
- content-derived key would collapse to one. Fade group lets
635
- `scrub.hideOverlaysOnScrub` ease the lines out while scrubbing. */}
719
+ {/* Fade group lets `scrub.hideOverlaysOnScrub` ease the lines out. Explicit
720
+ ids keep each reference line mounted when the caller reorders it. */}
636
721
  <Group opacity={overlayScrubFade}>
637
722
  {allRefLines.map((rl, i) => (
638
723
  <ReferenceLineOverlay
639
- key={i}
724
+ key={refLineKeys[i]}
640
725
  engine={engine}
641
726
  padding={effectivePadding}
642
727
  line={rl}
@@ -654,12 +739,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
654
739
  padding={effectivePadding}
655
740
  colors={lineColors}
656
741
  config={dotCfg.valueLine}
742
+ seriesCount={activeSeriesCount}
657
743
  />
658
744
  </Group>
659
745
  )}
660
746
 
661
747
  <Group opacity={reveal.lineOpacity}>
662
- {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
748
+ {Array.from({ length: activeSeriesCount }, (_, i) => (
663
749
  <MultiSeriesStroke
664
750
  key={i}
665
751
  index={i}
@@ -694,6 +780,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
694
780
  color={dotCfg.color}
695
781
  pulse={dotCfg.pulse}
696
782
  viewEnd={engine.viewEnd}
783
+ seriesCount={activeSeriesCount}
697
784
  />
698
785
  </Group>
699
786
  )}
@@ -749,6 +836,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
749
836
  lineStrokeWidth={loadingStrokeWidth}
750
837
  waveAmplitude={loadingAmplitude}
751
838
  waveSpeed={loadingSpeed}
839
+ opaqueCanvas={canvasMode === "opaque"}
752
840
  />
753
841
  </Group>
754
842
  );
@@ -767,6 +855,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
767
855
  skiaFont,
768
856
  reveal,
769
857
  degenShakeTransform,
858
+ activeSeriesCount,
770
859
  } = model;
771
860
  if (!dotCfg.valueLabel) return null;
772
861
  return (
@@ -778,6 +867,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
778
867
  colors={lineColors}
779
868
  font={skiaFont}
780
869
  dotRadius={dotOuterRadius}
870
+ seriesCount={activeSeriesCount}
781
871
  />
782
872
  </Group>
783
873
  </Group>
@@ -789,6 +879,10 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
789
879
  function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
790
880
  const {
791
881
  allRefLines,
882
+ refLineKeys,
883
+ refLineCustom,
884
+ refLineOffAxisCustom,
885
+ refLineCustomTagWidths,
792
886
  engine,
793
887
  effectivePadding,
794
888
  palette,
@@ -802,7 +896,7 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
802
896
  <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
803
897
  {allRefLines.map((rl, i) => (
804
898
  <ReferenceLineOverlay
805
- key={i}
899
+ key={refLineKeys[i]}
806
900
  engine={engine}
807
901
  padding={effectivePadding}
808
902
  line={rl}
@@ -810,12 +904,22 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
810
904
  formatValue={formatValue}
811
905
  font={skiaFont}
812
906
  badgeLayer
907
+ suppressTag={refLineCustom[i]}
908
+ suppressTagWhenOffAxis={refLineOffAxisCustom[i]}
909
+ customTagWidths={refLineCustomTagWidths}
813
910
  />
814
911
  ))}
815
912
  </Group>
816
913
  );
817
914
  }
818
915
 
916
+ /** Owns the price/time projection worklets for the multi-series custom overlay. */
917
+ function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel }) {
918
+ const { engine, effectivePadding, renderOverlay } = model;
919
+ const overlayContext = useChartOverlayContext(engine, effectivePadding);
920
+ return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
921
+ }
922
+
819
923
  export function LiveChartSeries(props: LiveChartSeriesProps) {
820
924
  const model = useLiveChartSeriesController(props);
821
925
  const {
@@ -833,6 +937,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
833
937
  effectivePadding,
834
938
  engine,
835
939
  scrubCfg,
940
+ seriesTooltipCfg,
836
941
  crosshair,
837
942
  palette,
838
943
  dotCfg,
@@ -848,7 +953,17 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
848
953
  markersSV,
849
954
  markerClusterCfg,
850
955
  renderMarker,
956
+ renderOverlay,
957
+ renderReferenceLine,
958
+ renderOffAxisReferenceLine,
959
+ allRefLines,
960
+ refLineCustom,
961
+ refLineOffAxisCustom,
962
+ refLineCustomTagWidths,
851
963
  overlayScrubFade,
964
+ canvasMode,
965
+ activeSeriesCount,
966
+ skiaFont,
852
967
  } = model;
853
968
 
854
969
  // Mirror the Skia overlay fade onto the RN custom-marker sibling so
@@ -891,7 +1006,21 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
891
1006
  else points map into a taller area and the x-axis draws past the edge. */}
892
1007
  <GestureDetector gesture={rootGesture}>
893
1008
  <View style={{ flex: 1 }} onLayout={onLayout}>
894
- <Canvas style={{ flex: 1, minHeight: layoutHeight || 1 }}>
1009
+ {/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
1010
+ <Canvas
1011
+ key={canvasMode}
1012
+ style={{ flex: 1, minHeight: layoutHeight || 1 }}
1013
+ opaque={canvasMode === "opaque"}
1014
+ >
1015
+ {canvasMode === "opaque" && (
1016
+ <Rect
1017
+ x={0}
1018
+ y={0}
1019
+ width={engine.canvasWidth}
1020
+ height={engine.canvasHeight}
1021
+ color={backgroundColor}
1022
+ />
1023
+ )}
895
1024
  <SeriesChartStack model={model} />
896
1025
 
897
1026
  {/* "extrema-edge" connector lines (dot → edge readout). Outside the
@@ -910,6 +1039,9 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
910
1039
  startColor={leftEdgeFadeCfg.startColor}
911
1040
  endColor={leftEdgeFadeCfg.endColor}
912
1041
  engine={engine}
1042
+ opaqueBackgroundRgb={
1043
+ canvasMode === "opaque" ? palette.bgRgb : undefined
1044
+ }
913
1045
  />
914
1046
  )}
915
1047
 
@@ -929,15 +1061,37 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
929
1061
  selectionColor={selectionColor}
930
1062
  dimOpacity={scrubCfg.dimOpacity}
931
1063
  liveDotExtent={liveDotExtent}
932
- crosshairLineColor={scrubCfg.crosshairLineColor}
933
- crosshairDash={scrubCfg.crosshairDash}
1064
+ crosshairLineColor={
1065
+ seriesTooltipCfg?.guideColor ??
1066
+ scrubCfg.crosshairLineColor
1067
+ }
1068
+ crosshairLineWidth={seriesTooltipCfg?.guideWidth}
1069
+ crosshairDash={
1070
+ seriesTooltipCfg
1071
+ ? seriesTooltipCfg.guideDashPattern
1072
+ : scrubCfg.crosshairDash
1073
+ }
934
1074
  crosshairDimColor={scrubCfg.crosshairDimColor}
1075
+ opaqueCanvas={canvasMode === "opaque"}
935
1076
  />
936
1077
  )}
937
1078
 
938
1079
  {/* Per-series value labels on top of the scrub dim so the dim never
939
1080
  clips them (they track each series' live value, not the scrub). */}
940
1081
  <SeriesValueLabelLayer model={model} />
1082
+
1083
+ {seriesTooltipCfg && (
1084
+ <PerSeriesTooltipOverlay
1085
+ layout={crosshair.tooltipLayout}
1086
+ font={skiaFont}
1087
+ palette={palette}
1088
+ config={seriesTooltipCfg}
1089
+ seriesCount={activeSeriesCount}
1090
+ tooltipBackground={scrubCfg?.tooltipBackground}
1091
+ tooltipColor={scrubCfg?.tooltipColor}
1092
+ tooltipBorderColor={scrubCfg?.tooltipBorderColor}
1093
+ />
1094
+ )}
941
1095
  </Canvas>
942
1096
 
943
1097
  {/* RN labels floated over the canvas (sibling of <Canvas>, an RN
@@ -952,25 +1106,56 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
952
1106
  padding={effectivePadding}
953
1107
  />
954
1108
 
955
- {/* Custom-rendered markers — RN views floated over the canvas
956
- (non-Skia), pinned to each marker's live position. Box-none fade
957
- wrapper so `scrub.hideOverlaysOnScrub` hides them with the Skia
958
- markers (full-bleed; children keep their own absolute positions). */}
959
- {markersActive && renderMarker && (
1109
+ {/* Custom annotations — RN views floated over the canvas (non-Skia),
1110
+ pinned to their live positions. As a post-Canvas sibling they render
1111
+ above the left-edge fade and scrub overlay. The box-none fade wrapper
1112
+ keeps `scrub.hideOverlaysOnScrub` behavior aligned with Skia. */}
1113
+ {((markersActive && renderMarker) ||
1114
+ ((renderReferenceLine || renderOffAxisReferenceLine) &&
1115
+ allRefLines.length > 0)) && (
960
1116
  <Animated.View
961
1117
  pointerEvents="box-none"
962
1118
  style={[StyleSheet.absoluteFill, overlayFadeStyle]}
963
1119
  >
964
- <CustomMarkerOverlay
965
- markers={markersSV}
966
- renderMarker={renderMarker}
967
- engine={engine}
968
- padding={effectivePadding}
969
- series={series}
970
- cluster={markerClusterCfg}
971
- />
1120
+ {markersActive && renderMarker && (
1121
+ <CustomMarkerOverlay
1122
+ markers={markersSV}
1123
+ renderMarker={renderMarker}
1124
+ engine={engine}
1125
+ padding={effectivePadding}
1126
+ series={series}
1127
+ cluster={markerClusterCfg}
1128
+ />
1129
+ )}
1130
+ {renderReferenceLine && allRefLines.length > 0 && (
1131
+ <CustomReferenceLineOverlay
1132
+ lines={allRefLines}
1133
+ renderReferenceLine={renderReferenceLine}
1134
+ custom={refLineCustom}
1135
+ engine={engine}
1136
+ padding={effectivePadding}
1137
+ formatValue={formatValue}
1138
+ tagWidths={refLineCustomTagWidths}
1139
+ />
1140
+ )}
1141
+ {renderOffAxisReferenceLine && allRefLines.length > 0 && (
1142
+ <CustomReferenceLineOverlay
1143
+ lines={allRefLines}
1144
+ renderReferenceLine={renderOffAxisReferenceLine}
1145
+ custom={refLineOffAxisCustom}
1146
+ engine={engine}
1147
+ padding={effectivePadding}
1148
+ formatValue={formatValue}
1149
+ tagWidths={refLineCustomTagWidths}
1150
+ offAxisOnly
1151
+ />
1152
+ )}
972
1153
  </Animated.View>
973
1154
  )}
1155
+
1156
+ {/* Custom consumer overlay — topmost RN sibling with the live plot
1157
+ rect, including this multi-series chart's resolved axis inset. */}
1158
+ {renderOverlay && <SeriesCustomConsumerOverlay model={model} />}
974
1159
  </View>
975
1160
  </GestureDetector>
976
1161
  {legendCfg.position === "bottom" ? legend : null}
@@ -8,6 +8,7 @@ import {
8
8
  vec,
9
9
  type SkFont,
10
10
  } from "@shopify/react-native-skia";
11
+ import { useRef } from "react";
11
12
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
12
13
  import {
13
14
  BADGE_METRICS_DEFAULTS,
@@ -67,6 +68,7 @@ export function LoadingOverlay({
67
68
  lineStrokeWidth,
68
69
  waveAmplitude = 14,
69
70
  waveSpeed = 1,
71
+ opaqueCanvas = false,
70
72
  }: {
71
73
  engine: ChartEngineLayout;
72
74
  padding: ChartPadding;
@@ -86,6 +88,8 @@ export function LoadingOverlay({
86
88
  waveAmplitude?: number;
87
89
  /** Breathing-wave speed multiplier. */
88
90
  waveSpeed?: number;
91
+ /** Paint the owned background instead of erasing destination alpha. */
92
+ opaqueCanvas?: boolean;
89
93
  /** Mirror the badge prop so labels align with GridOverlay's label positions. */
90
94
  badge?: boolean;
91
95
  /** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
@@ -99,15 +103,26 @@ export function LoadingOverlay({
99
103
  }) {
100
104
  // Same left-inset formula as GridOverlay (only used when badge=true)
101
105
  const leftInset =
102
- badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
106
+ badgeMetrics.dotGap +
107
+ badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
103
108
 
104
109
  // Loading-shell color (squiggle + skeleton placeholders) and squiggle stroke,
105
110
  // both overridable via `loading={{ color, strokeWidth }}`.
106
111
  const loadingColor = lineColor ?? palette.gridLine;
107
112
  const loadingStroke = lineStrokeWidth ?? strokeWidth;
113
+ const [bgR, bgG, bgB] = palette.bgRgb;
114
+ const gapMaskColors = opaqueCanvas
115
+ ? [
116
+ `rgba(${bgR},${bgG},${bgB},0)`,
117
+ `rgb(${bgR},${bgG},${bgB})`,
118
+ `rgb(${bgR},${bgG},${bgB})`,
119
+ `rgba(${bgR},${bgG},${bgB},0)`,
120
+ ]
121
+ : ["rgba(0,0,0,0)", "rgba(0,0,0,1)", "rgba(0,0,0,1)", "rgba(0,0,0,0)"];
108
122
 
109
123
  // Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
110
124
  const squigglyBuilder = usePathBuilder();
125
+ const squigglyPtsRef = useRef<number[]>([]);
111
126
 
112
127
  // Squiggly path — animated each frame via timestamp
113
128
  const squigglyPath = useDerivedValue(() => {
@@ -122,6 +137,7 @@ export function LoadingOverlay({
122
137
  engine.timestamp.get(),
123
138
  waveAmplitude,
124
139
  waveSpeed,
140
+ squigglyPtsRef.current,
125
141
  );
126
142
  return buildSplineDetached(b, pts);
127
143
  });
@@ -254,17 +270,15 @@ export function LoadingOverlay({
254
270
  />
255
271
 
256
272
  {/* Erase a soft horizontal band through the squiggle for the label (dstOut) */}
257
- <Group opacity={showGapGroup} blendMode="dstOut">
273
+ <Group
274
+ opacity={showGapGroup}
275
+ blendMode={opaqueCanvas ? undefined : "dstOut"}
276
+ >
258
277
  <Rect x={gapLeft} y={gapTop} width={gapWidth} height={gapHeight}>
259
278
  <LinearGradient
260
279
  start={vec(0, 0)}
261
280
  end={gapGradEnd}
262
- colors={[
263
- "rgba(0,0,0,0)",
264
- "rgba(0,0,0,1)",
265
- "rgba(0,0,0,1)",
266
- "rgba(0,0,0,0)",
267
- ]}
281
+ colors={gapMaskColors}
268
282
  positions={gapGradientPositions}
269
283
  />
270
284
  </Rect>
@@ -279,7 +293,7 @@ export function LoadingOverlay({
279
293
  width={RECT_W}
280
294
  height={RECT_H}
281
295
  r={RECT_R}
282
- color={loadingColor}
296
+ color={loadingColor}
283
297
  />
284
298
  <RoundedRect
285
299
  x={lx}
@@ -287,7 +301,7 @@ export function LoadingOverlay({
287
301
  width={RECT_W}
288
302
  height={RECT_H}
289
303
  r={RECT_R}
290
- color={loadingColor}
304
+ color={loadingColor}
291
305
  />
292
306
  <RoundedRect
293
307
  x={lx}
@@ -295,7 +309,7 @@ export function LoadingOverlay({
295
309
  width={RECT_W}
296
310
  height={RECT_H}
297
311
  r={RECT_R}
298
- color={loadingColor}
312
+ color={loadingColor}
299
313
  />
300
314
  <RoundedRect
301
315
  x={lx}
@@ -303,7 +317,7 @@ export function LoadingOverlay({
303
317
  width={RECT_W}
304
318
  height={RECT_H}
305
319
  r={RECT_R}
306
- color={loadingColor}
320
+ color={loadingColor}
307
321
  />
308
322
  </>
309
323
  ) : null}
@@ -351,6 +351,18 @@ export function MarkerOverlay({
351
351
  if (projRef.current === null) {
352
352
  projRef.current = { a: [], b: [], tick: false };
353
353
  }
354
+ const atlasFrameRef = useRef<{
355
+ a: { transforms: SkRSXform[]; sprites: SkRect[] };
356
+ b: { transforms: SkRSXform[]; sprites: SkRect[] };
357
+ tick: boolean;
358
+ } | null>(null);
359
+ if (atlasFrameRef.current === null) {
360
+ atlasFrameRef.current = {
361
+ a: { transforms: [], sprites: [] },
362
+ b: { transforms: [], sprites: [] },
363
+ tick: false,
364
+ };
365
+ }
354
366
 
355
367
  // Single per-frame worklet: project all markers, then emit a transform +
356
368
  // source rect for each visible atlas marker. Three mappers total regardless
@@ -377,8 +389,13 @@ export function MarkerOverlay({
377
389
  lineLinear,
378
390
  });
379
391
  clusterMarkers(ms, buf, { config: cluster });
380
- const transforms: SkRSXform[] = [];
381
- const sprites: SkRect[] = [];
392
+ const atlasFrames = atlasFrameRef.current!;
393
+ atlasFrames.tick = !atlasFrames.tick;
394
+ const frame = atlasFrames.tick ? atlasFrames.a : atlasFrames.b;
395
+ const transforms = frame.transforms;
396
+ const sprites = frame.sprites;
397
+ transforms.length = 0;
398
+ sprites.length = 0;
382
399
  for (let i = 0; i < ms.length; i++) {
383
400
  const pt = buf[i];
384
401
  // Collapsed-cluster members fold into their representative's badge.
@@ -455,7 +472,7 @@ export function MarkerOverlay({
455
472
  );
456
473
  sprites.push(cell.rect);
457
474
  }
458
- return { transforms, sprites };
475
+ return frame;
459
476
  },
460
477
  [
461
478
  cells,