react-native-livechart 4.9.2 → 4.11.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/README.md +24 -0
  2. package/dist/components/CrosshairLine.d.ts +3 -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 +4 -2
  7. package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  9. package/dist/components/LeftEdgeFade.d.ts +3 -1
  10. package/dist/components/LeftEdgeFade.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/LoadingOverlay.d.ts +3 -1
  14. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  15. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  16. package/dist/components/MultiSeriesDots.d.ts +3 -1
  17. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  18. package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
  19. package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
  20. package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
  21. package/dist/components/MultiSeriesValueLines.d.ts +3 -1
  22. package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
  23. package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
  24. package/dist/components/YAxisOverlay.d.ts +9 -1
  25. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  26. package/dist/components/thresholdSplitShaderSource.d.ts +13 -0
  27. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
  28. package/dist/core/useLiveChartEngine.d.ts +9 -1
  29. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  30. package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
  31. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  32. package/dist/draw/markerAtlas.d.ts +2 -0
  33. package/dist/draw/markerAtlas.d.ts.map +1 -1
  34. package/dist/draw/particleAtlas.d.ts +3 -2
  35. package/dist/draw/particleAtlas.d.ts.map +1 -1
  36. package/dist/hooks/crosshairShared.d.ts +6 -6
  37. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  38. package/dist/hooks/delayedPanGuard.d.ts +31 -0
  39. package/dist/hooks/delayedPanGuard.d.ts.map +1 -0
  40. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  41. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  42. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  43. package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
  44. package/dist/hooks/useMultiSeriesLinePaths.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/squiggly.d.ts +6 -3
  48. package/dist/math/squiggly.d.ts.map +1 -1
  49. package/dist/math/threshold.d.ts +2 -2
  50. package/dist/types.d.ts +35 -4
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/components/CrosshairLine.tsx +17 -0
  54. package/src/components/CrosshairOverlay.tsx +54 -36
  55. package/src/components/CustomReferenceLineOverlay.tsx +16 -7
  56. package/src/components/DegenParticlesOverlay.tsx +40 -5
  57. package/src/components/LeftEdgeFade.tsx +12 -6
  58. package/src/components/LiveChart.tsx +34 -16
  59. package/src/components/LiveChartSeries.tsx +94 -21
  60. package/src/components/LoadingOverlay.tsx +26 -12
  61. package/src/components/MarkerOverlay.tsx +31 -19
  62. package/src/components/MultiSeriesDots.tsx +4 -2
  63. package/src/components/MultiSeriesTooltipStack.tsx +4 -2
  64. package/src/components/MultiSeriesValueLabels.tsx +4 -2
  65. package/src/components/MultiSeriesValueLines.tsx +4 -2
  66. package/src/components/ThresholdSplitShader.tsx +5 -37
  67. package/src/components/YAxisOverlay.tsx +43 -1
  68. package/src/components/thresholdSplitShaderSource.ts +70 -0
  69. package/src/core/useLiveChartEngine.ts +110 -42
  70. package/src/core/useLiveChartSeriesEngine.ts +137 -70
  71. package/src/draw/markerAtlas.ts +22 -2
  72. package/src/draw/particleAtlas.ts +21 -10
  73. package/src/hooks/crosshairShared.ts +24 -18
  74. package/src/hooks/delayedPanGuard.ts +80 -0
  75. package/src/hooks/useChartPaths.ts +11 -0
  76. package/src/hooks/useCrosshair.ts +53 -10
  77. package/src/hooks/useCrosshairSeries.ts +49 -6
  78. package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
  79. package/src/index.ts +1 -0
  80. package/src/math/squiggly.ts +28 -11
  81. package/src/math/threshold.ts +2 -2
  82. package/src/types.ts +40 -5
@@ -5,6 +5,7 @@ import {
5
5
  type SkRSXform,
6
6
  type SkRect,
7
7
  } from "@shopify/react-native-skia";
8
+ import { useRef } from "react";
8
9
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
9
10
  import {
10
11
  buildParticleInstances,
@@ -63,14 +64,46 @@ export function DegenParticlesOverlay({
63
64
  // memoizes this on `colorList`.
64
65
  const colorRgb = colorList.map((c) => parseColorRgb(c));
65
66
 
67
+ const poolRef = useRef<{
68
+ a: { transforms: SkRSXform[]; sprites: SkRect[]; colors: SkColor[] };
69
+ b: { transforms: SkRSXform[]; sprites: SkRect[]; colors: SkColor[] };
70
+ tick: boolean;
71
+ instances: ReturnType<typeof buildParticleInstances>;
72
+ instancePool: ReturnType<typeof buildParticleInstances>;
73
+ spriteRect: SkRect;
74
+ } | null>(null);
75
+ if (poolRef.current === null) {
76
+ const instancePool = Array.from({ length: particleSlotCount }, () => ({
77
+ x: 0,
78
+ y: 0,
79
+ scale: 0,
80
+ alpha: 0,
81
+ colorIndex: 0,
82
+ }));
83
+ poolRef.current = {
84
+ a: { transforms: [], sprites: [], colors: [] },
85
+ b: { transforms: [], sprites: [], colors: [] },
86
+ tick: false,
87
+ instances: [],
88
+ instancePool,
89
+ spriteRect: Skia.XYWHRect(0, 0, sprite.size, sprite.size),
90
+ };
91
+ }
92
+
66
93
  // Single per-frame worklet. Reads `packRevision` so it re-runs each frame
67
94
  // while a burst is alive (the pack buffer is mutated in place, so its
68
95
  // reference is stable and wouldn't otherwise re-notify).
69
96
  const atlasData = useDerivedValue(() => {
70
97
  const rev = packRevision.get();
71
- const transforms: SkRSXform[] = [];
72
- const sprites: SkRect[] = [];
73
- const colorsOut: SkColor[] = [];
98
+ const pool = poolRef.current!;
99
+ pool.tick = !pool.tick;
100
+ const frame = pool.tick ? pool.a : pool.b;
101
+ const transforms = frame.transforms;
102
+ const sprites = frame.sprites;
103
+ const colorsOut = frame.colors;
104
+ transforms.length = 0;
105
+ sprites.length = 0;
106
+ colorsOut.length = 0;
74
107
  if (rev >= 0) {
75
108
  const instances = buildParticleInstances(
76
109
  pack.get(),
@@ -79,6 +112,8 @@ export function DegenParticlesOverlay({
79
112
  particleBurstDurationSec,
80
113
  particleOpacity,
81
114
  sprite.radius,
115
+ pool.instances,
116
+ pool.instancePool,
82
117
  );
83
118
  const half = sprite.size / 2;
84
119
  for (let i = 0; i < instances.length; i++) {
@@ -92,12 +127,12 @@ export function DegenParticlesOverlay({
92
127
  inst.y - inst.scale * half,
93
128
  ),
94
129
  );
95
- sprites.push(Skia.XYWHRect(0, 0, sprite.size, sprite.size));
130
+ sprites.push(pool.spriteRect);
96
131
  const [r, g, b] = colorRgb[inst.colorIndex % colorRgb.length];
97
132
  colorsOut.push(Skia.Color(`rgba(${r}, ${g}, ${b}, ${inst.alpha})`));
98
133
  }
99
134
  }
100
- return { transforms, sprites, colors: colorsOut };
135
+ return frame;
101
136
  }, [
102
137
  pack,
103
138
  packRevision,
@@ -2,6 +2,7 @@ import { Group, LinearGradient, Rect, vec } from "@shopify/react-native-skia";
2
2
 
3
3
  import { useDerivedValue } from "react-native-reanimated";
4
4
  import type { ChartEngineLayout } from "../core/useLiveChartEngine";
5
+ import { parseColorRgba } from "../theme";
5
6
 
6
7
  export function LeftEdgeFade({
7
8
  paddingLeft,
@@ -9,27 +10,32 @@ export function LeftEdgeFade({
9
10
  startColor,
10
11
  endColor,
11
12
  engine,
13
+ opaqueBackgroundRgb,
12
14
  }: {
13
15
  paddingLeft: number;
14
16
  fadeWidth: number;
15
17
  startColor: string;
16
18
  endColor: string;
17
19
  engine: ChartEngineLayout;
20
+ /** When set, paint the owned background instead of erasing destination alpha. */
21
+ opaqueBackgroundRgb?: [number, number, number];
18
22
  }) {
19
23
  const rectWidth = paddingLeft + fadeWidth;
20
24
  const height = useDerivedValue(() => engine.canvasHeight.value);
21
25
 
22
26
  const gStart = vec(paddingLeft, 0);
23
27
  const gEnd = vec(paddingLeft + fadeWidth, 0);
28
+ const colors = opaqueBackgroundRgb
29
+ ? [
30
+ `rgba(${opaqueBackgroundRgb[0]},${opaqueBackgroundRgb[1]},${opaqueBackgroundRgb[2]},${parseColorRgba(startColor)[3]})`,
31
+ `rgba(${opaqueBackgroundRgb[0]},${opaqueBackgroundRgb[1]},${opaqueBackgroundRgb[2]},${parseColorRgba(endColor)[3]})`,
32
+ ]
33
+ : [startColor, endColor];
24
34
 
25
35
  return (
26
- <Group blendMode="dstOut">
36
+ <Group blendMode={opaqueBackgroundRgb ? undefined : "dstOut"}>
27
37
  <Rect x={0} y={0} width={rectWidth} height={height}>
28
- <LinearGradient
29
- start={gStart}
30
- end={gEnd}
31
- colors={[startColor, endColor]}
32
- />
38
+ <LinearGradient start={gStart} end={gEnd} colors={colors} />
33
39
  </Rect>
34
40
  </Group>
35
41
  );
@@ -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
 
@@ -253,6 +248,7 @@ function useLiveChartController({
253
248
  font: fontProp,
254
249
  insets,
255
250
  style,
251
+ canvasMode = "transparent",
256
252
 
257
253
  // ── Candlestick ─────────────────────────────────────────────────────────
258
254
  mode = "line",
@@ -1204,6 +1200,7 @@ function useLiveChartController({
1204
1200
  return {
1205
1201
  // passthrough props the render needs
1206
1202
  style,
1203
+ canvasMode,
1207
1204
  accessibilityLabel,
1208
1205
  accessibilityRole,
1209
1206
  emptyText,
@@ -1362,9 +1359,7 @@ function ChartWithDegen({
1362
1359
  onDegenShake,
1363
1360
  isStatic,
1364
1361
  );
1365
- return (
1366
- <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />
1367
- );
1362
+ return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
1368
1363
  }
1369
1364
 
1370
1365
  /**
@@ -1412,8 +1407,10 @@ function ChartYAxisLayer({
1412
1407
  effectivePadding,
1413
1408
  palette,
1414
1409
  skiaFont,
1410
+ dotY,
1415
1411
  badgeUsesRightGutter,
1416
1412
  badgeCfg,
1413
+ badgeFont,
1417
1414
  metricsCfg,
1418
1415
  gridStyleCfg,
1419
1416
  yAxisFloat,
@@ -1431,6 +1428,9 @@ function ChartYAxisLayer({
1431
1428
  badge={badgeUsesRightGutter}
1432
1429
  badgeTail={badgeCfg?.tail ?? true}
1433
1430
  badgeMetrics={metricsCfg.badge}
1431
+ badgeCenterY={badgeUsesRightGutter ? dotY : undefined}
1432
+ badgeFontSize={badgeUsesRightGutter ? badgeFont.getSize() : undefined}
1433
+ badgeOffsetY={badgeCfg?.offsetY ?? 0}
1434
1434
  gridStyle={gridStyleCfg}
1435
1435
  />
1436
1436
  </Group>
@@ -1716,6 +1716,7 @@ function ChartStack({
1716
1716
  loadingStrokeWidth,
1717
1717
  loadingAmplitude,
1718
1718
  loadingSpeed,
1719
+ canvasMode,
1719
1720
  } = model;
1720
1721
  return (
1721
1722
  <Group transform={degen?.shakeTransform}>
@@ -1934,6 +1935,7 @@ function ChartStack({
1934
1935
  lineStrokeWidth={loadingStrokeWidth}
1935
1936
  waveAmplitude={loadingAmplitude}
1936
1937
  waveSpeed={loadingSpeed}
1938
+ opaqueCanvas={canvasMode === "opaque"}
1937
1939
  />
1938
1940
  </Group>
1939
1941
  );
@@ -1998,6 +2000,7 @@ function ChartScrubLayer({
1998
2000
  selectionDot,
1999
2001
  selectionColor,
2000
2002
  renderTooltip,
2003
+ canvasMode,
2001
2004
  } = model;
2002
2005
  // A custom tooltip is an RN overlay (sibling of <Canvas>), so the built-in
2003
2006
  // Skia tooltip is suppressed here while it's active — the line pill in line
@@ -2042,6 +2045,7 @@ function ChartScrubLayer({
2042
2045
  tooltipBorderRadius={scrubCfg.tooltipBorderRadius}
2043
2046
  tooltipShowValue={scrubCfg.tooltipShowValue}
2044
2047
  tooltipShowTime={scrubCfg.tooltipShowTime}
2048
+ opaqueCanvas={canvasMode === "opaque"}
2045
2049
  >
2046
2050
  {/* Candle charts render a multi-line OHLC tooltip; the line
2047
2051
  chart falls back to CrosshairOverlay's default value/time
@@ -2359,6 +2363,7 @@ function ChartView({
2359
2363
  extremaTimeOffset,
2360
2364
  topConnector,
2361
2365
  bottomConnector,
2366
+ canvasMode,
2362
2367
  } = model;
2363
2368
 
2364
2369
  return (
@@ -2370,7 +2375,21 @@ function ChartView({
2370
2375
  accessibilityLabel={accessibilityLabel}
2371
2376
  accessibilityRole={accessibilityRole}
2372
2377
  >
2373
- <Canvas style={{ flex: 1 }}>
2378
+ {/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
2379
+ <Canvas
2380
+ key={canvasMode}
2381
+ style={{ flex: 1 }}
2382
+ opaque={canvasMode === "opaque"}
2383
+ >
2384
+ {canvasMode === "opaque" && (
2385
+ <Rect
2386
+ x={0}
2387
+ y={0}
2388
+ width={engine.canvasWidth}
2389
+ height={engine.canvasHeight}
2390
+ color={backgroundColor}
2391
+ />
2392
+ )}
2374
2393
  {/* Background fills first, then the left-edge fade (a canvas-space sibling
2375
2394
  so dstOut blends correctly), then the line stack on top — so the fade
2376
2395
  softens only the fills and the line stays crisp at the left edge. */}
@@ -2387,15 +2406,14 @@ function ChartView({
2387
2406
  startColor={leftEdgeFadeCfg.startColor}
2388
2407
  endColor={leftEdgeFadeCfg.endColor}
2389
2408
  engine={engine}
2409
+ opaqueBackgroundRgb={
2410
+ canvasMode === "opaque" ? palette.bgRgb : undefined
2411
+ }
2390
2412
  />
2391
2413
  )}
2392
2414
 
2393
2415
  {/* Line stack above the fade so the line stays crisp at the left edge. */}
2394
- <ChartStack
2395
- model={model}
2396
- yAxisEntries={yAxisEntries}
2397
- degen={degen}
2398
- />
2416
+ <ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
2399
2417
 
2400
2418
  {/* "extrema-edge" connector lines (dot → edge readout), above the chart
2401
2419
  content so the dashed guide reads over the line / candles. */}
@@ -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";
@@ -83,6 +83,10 @@ import {
83
83
  labelConnector,
84
84
  } from "./ExtremaConnectorOverlay";
85
85
  import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
86
+ import {
87
+ CustomReferenceLineOverlay,
88
+ customReferenceLineFlags,
89
+ } from "./CustomReferenceLineOverlay";
86
90
  import { CrosshairLine } from "./CrosshairLine";
87
91
  import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
88
92
  import { LeftEdgeFade } from "./LeftEdgeFade";
@@ -128,6 +132,7 @@ function useLiveChartSeriesController({
128
132
  font: fontProp,
129
133
  insets,
130
134
  style,
135
+ canvasMode = "transparent",
131
136
  timeWindow = 30,
132
137
  paused = false,
133
138
  loading = false,
@@ -172,6 +177,7 @@ function useLiveChartSeriesController({
172
177
  markerHitRadius = 16,
173
178
  markerCluster,
174
179
  renderMarker,
180
+ renderReferenceLine,
175
181
  leftEdgeFade = true,
176
182
  }: LiveChartSeriesProps) {
177
183
  const emptyMarkers = useSharedValue<Marker[]>([]);
@@ -224,6 +230,13 @@ function useLiveChartSeriesController({
224
230
 
225
231
  const allRefLines = referenceLines ?? [];
226
232
  const refValues = collectReferenceValues(allRefLines);
233
+ // Form-A lines a custom renderer owns keep their line stroke but suppress the
234
+ // built-in Skia tag. The callback is probed per line on the JS thread, matching
235
+ // LiveChart and CustomMarkerOverlay's per-item fallback model.
236
+ const refLineCustom = customReferenceLineFlags(
237
+ allRefLines,
238
+ renderReferenceLine,
239
+ );
227
240
 
228
241
  const palette = applyPaletteOverride(
229
242
  resolveTheme(accentColor, theme),
@@ -256,6 +269,11 @@ function useLiveChartSeriesController({
256
269
  setSeriesSnapshot(series.get().slice());
257
270
  }, [series]);
258
271
 
272
+ // Mount per-series drawing worklets only for real series. The previous fixed
273
+ // 12-slot render kept 144 derived-value mappers alive for the default stroke,
274
+ // dot, and value-label layers even when a chart contained only one line.
275
+ const activeSeriesCount = Math.min(seriesSnapshot.length, MAX_MULTI_SERIES);
276
+
259
277
  const maxSeriesLabelWidth = dotCfg.valueLabel
260
278
  ? Math.max(
261
279
  0,
@@ -336,7 +354,11 @@ function useLiveChartSeriesController({
336
354
  nowOverride,
337
355
  });
338
356
  const { layoutHeight, onLayout } = useCanvasLayout(engine);
339
- const linePaths = useMultiSeriesLinePaths(engine, effectivePadding);
357
+ const linePaths = useMultiSeriesLinePaths(
358
+ engine,
359
+ effectivePadding,
360
+ activeSeriesCount,
361
+ );
340
362
 
341
363
  // Per-series colors and stroke styles, derived from the off-render snapshot
342
364
  // (React state — so no Reanimated read-during-render). The reaction below
@@ -503,6 +525,7 @@ function useLiveChartSeriesController({
503
525
  // passthrough props the render needs
504
526
  series,
505
527
  style,
528
+ canvasMode,
506
529
  accessibilityLabel,
507
530
  accessibilityRole,
508
531
  emptyText,
@@ -519,6 +542,7 @@ function useLiveChartSeriesController({
519
542
  degenCfg,
520
543
  metricsCfg,
521
544
  allRefLines,
545
+ refLineCustom,
522
546
  leftEdgeFadeCfg,
523
547
  // theme / layout / fonts
524
548
  palette,
@@ -540,6 +564,7 @@ function useLiveChartSeriesController({
540
564
  layoutHeight,
541
565
  onLayout,
542
566
  linePaths,
567
+ activeSeriesCount,
543
568
  lineColors,
544
569
  lineStyles,
545
570
  degenPack,
@@ -555,6 +580,7 @@ function useLiveChartSeriesController({
555
580
  markerGroupOpacity,
556
581
  overlayScrubFade,
557
582
  renderMarker,
583
+ renderReferenceLine,
558
584
  // selection dot: resolved config + fallback color (the leading series' color)
559
585
  selectionDot: selectionDotCfg,
560
586
  selectionColor: lineColors[0],
@@ -610,6 +636,8 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
610
636
  loadingStrokeWidth,
611
637
  loadingAmplitude,
612
638
  loadingSpeed,
639
+ canvasMode,
640
+ activeSeriesCount,
613
641
  } = model;
614
642
 
615
643
  return (
@@ -629,12 +657,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
629
657
  </Group>
630
658
  )}
631
659
 
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. */}
660
+ {/* Reference lines are index-addressed throughout the drag/press API, and
661
+ duplicate working orders may share all visible content. Positional keys
662
+ keep both orders mounted without collapsing them to one React child.
663
+ Fade group lets `scrub.hideOverlaysOnScrub` ease the lines out. */}
636
664
  <Group opacity={overlayScrubFade}>
637
665
  {allRefLines.map((rl, i) => (
666
+ /* react-doctor-disable-next-line react-doctor/no-array-index-as-key */
638
667
  <ReferenceLineOverlay
639
668
  key={i}
640
669
  engine={engine}
@@ -654,12 +683,13 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
654
683
  padding={effectivePadding}
655
684
  colors={lineColors}
656
685
  config={dotCfg.valueLine}
686
+ seriesCount={activeSeriesCount}
657
687
  />
658
688
  </Group>
659
689
  )}
660
690
 
661
691
  <Group opacity={reveal.lineOpacity}>
662
- {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
692
+ {Array.from({ length: activeSeriesCount }, (_, i) => (
663
693
  <MultiSeriesStroke
664
694
  key={i}
665
695
  index={i}
@@ -694,6 +724,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
694
724
  color={dotCfg.color}
695
725
  pulse={dotCfg.pulse}
696
726
  viewEnd={engine.viewEnd}
727
+ seriesCount={activeSeriesCount}
697
728
  />
698
729
  </Group>
699
730
  )}
@@ -749,6 +780,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
749
780
  lineStrokeWidth={loadingStrokeWidth}
750
781
  waveAmplitude={loadingAmplitude}
751
782
  waveSpeed={loadingSpeed}
783
+ opaqueCanvas={canvasMode === "opaque"}
752
784
  />
753
785
  </Group>
754
786
  );
@@ -767,6 +799,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
767
799
  skiaFont,
768
800
  reveal,
769
801
  degenShakeTransform,
802
+ activeSeriesCount,
770
803
  } = model;
771
804
  if (!dotCfg.valueLabel) return null;
772
805
  return (
@@ -778,6 +811,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
778
811
  colors={lineColors}
779
812
  font={skiaFont}
780
813
  dotRadius={dotOuterRadius}
814
+ seriesCount={activeSeriesCount}
781
815
  />
782
816
  </Group>
783
817
  </Group>
@@ -789,6 +823,7 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
789
823
  function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
790
824
  const {
791
825
  allRefLines,
826
+ refLineCustom,
792
827
  engine,
793
828
  effectivePadding,
794
829
  palette,
@@ -796,11 +831,13 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
796
831
  skiaFont,
797
832
  degenShakeTransform,
798
833
  overlayScrubFade,
834
+ canvasMode,
799
835
  } = model;
800
836
  if (allRefLines.length === 0) return null;
801
837
  return (
802
838
  <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
803
839
  {allRefLines.map((rl, i) => (
840
+ /* react-doctor-disable-next-line react-doctor/no-array-index-as-key */
804
841
  <ReferenceLineOverlay
805
842
  key={i}
806
843
  engine={engine}
@@ -810,6 +847,7 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
810
847
  formatValue={formatValue}
811
848
  font={skiaFont}
812
849
  badgeLayer
850
+ suppressTag={refLineCustom[i]}
813
851
  />
814
852
  ))}
815
853
  </Group>
@@ -848,7 +886,11 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
848
886
  markersSV,
849
887
  markerClusterCfg,
850
888
  renderMarker,
889
+ renderReferenceLine,
890
+ allRefLines,
891
+ refLineCustom,
851
892
  overlayScrubFade,
893
+ canvasMode,
852
894
  } = model;
853
895
 
854
896
  // Mirror the Skia overlay fade onto the RN custom-marker sibling so
@@ -891,7 +933,21 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
891
933
  else points map into a taller area and the x-axis draws past the edge. */}
892
934
  <GestureDetector gesture={rootGesture}>
893
935
  <View style={{ flex: 1 }} onLayout={onLayout}>
894
- <Canvas style={{ flex: 1, minHeight: layoutHeight || 1 }}>
936
+ {/* Skia chooses TextureView vs SurfaceView when the native Canvas mounts. */}
937
+ <Canvas
938
+ key={canvasMode}
939
+ style={{ flex: 1, minHeight: layoutHeight || 1 }}
940
+ opaque={canvasMode === "opaque"}
941
+ >
942
+ {canvasMode === "opaque" && (
943
+ <Rect
944
+ x={0}
945
+ y={0}
946
+ width={engine.canvasWidth}
947
+ height={engine.canvasHeight}
948
+ color={backgroundColor}
949
+ />
950
+ )}
895
951
  <SeriesChartStack model={model} />
896
952
 
897
953
  {/* "extrema-edge" connector lines (dot → edge readout). Outside the
@@ -910,6 +966,9 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
910
966
  startColor={leftEdgeFadeCfg.startColor}
911
967
  endColor={leftEdgeFadeCfg.endColor}
912
968
  engine={engine}
969
+ opaqueBackgroundRgb={
970
+ canvasMode === "opaque" ? palette.bgRgb : undefined
971
+ }
913
972
  />
914
973
  )}
915
974
 
@@ -932,6 +991,7 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
932
991
  crosshairLineColor={scrubCfg.crosshairLineColor}
933
992
  crosshairDash={scrubCfg.crosshairDash}
934
993
  crosshairDimColor={scrubCfg.crosshairDimColor}
994
+ opaqueCanvas={canvasMode === "opaque"}
935
995
  />
936
996
  )}
937
997
 
@@ -952,23 +1012,36 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
952
1012
  padding={effectivePadding}
953
1013
  />
954
1014
 
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 && (
1015
+ {/* Custom annotations — RN views floated over the canvas (non-Skia),
1016
+ pinned to their live positions. As a post-Canvas sibling they render
1017
+ above the left-edge fade and scrub overlay. The box-none fade wrapper
1018
+ keeps `scrub.hideOverlaysOnScrub` behavior aligned with Skia. */}
1019
+ {((markersActive && renderMarker) ||
1020
+ (renderReferenceLine && allRefLines.length > 0)) && (
960
1021
  <Animated.View
961
1022
  pointerEvents="box-none"
962
1023
  style={[StyleSheet.absoluteFill, overlayFadeStyle]}
963
1024
  >
964
- <CustomMarkerOverlay
965
- markers={markersSV}
966
- renderMarker={renderMarker}
967
- engine={engine}
968
- padding={effectivePadding}
969
- series={series}
970
- cluster={markerClusterCfg}
971
- />
1025
+ {markersActive && renderMarker && (
1026
+ <CustomMarkerOverlay
1027
+ markers={markersSV}
1028
+ renderMarker={renderMarker}
1029
+ engine={engine}
1030
+ padding={effectivePadding}
1031
+ series={series}
1032
+ cluster={markerClusterCfg}
1033
+ />
1034
+ )}
1035
+ {renderReferenceLine && allRefLines.length > 0 && (
1036
+ <CustomReferenceLineOverlay
1037
+ lines={allRefLines}
1038
+ renderReferenceLine={renderReferenceLine}
1039
+ custom={refLineCustom}
1040
+ engine={engine}
1041
+ padding={effectivePadding}
1042
+ formatValue={formatValue}
1043
+ />
1044
+ )}
972
1045
  </Animated.View>
973
1046
  )}
974
1047
  </View>