react-native-livechart 2.0.1 → 3.0.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 (34) hide show
  1. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  2. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  3. package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
  4. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  5. package/dist/components/ValueLineOverlay.d.ts.map +1 -1
  6. package/dist/components/XAxisOverlay.d.ts.map +1 -1
  7. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  8. package/dist/hooks/useBadge.d.ts +2 -2
  9. package/dist/hooks/useBadge.d.ts.map +1 -1
  10. package/dist/hooks/useCandlePaths.d.ts +8 -10
  11. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  12. package/dist/hooks/useChartPaths.d.ts +8 -7
  13. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  14. package/dist/hooks/useMultiSeriesLinePaths.d.ts +9 -8
  15. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  16. package/dist/hooks/usePathBuilder.d.ts +27 -0
  17. package/dist/hooks/usePathBuilder.d.ts.map +1 -0
  18. package/dist/math/spline.d.ts +10 -2
  19. package/dist/math/spline.d.ts.map +1 -1
  20. package/package.json +2 -2
  21. package/src/components/LoadingOverlay.tsx +15 -29
  22. package/src/components/MarkerOverlay.tsx +23 -28
  23. package/src/components/MultiSeriesValueLines.tsx +20 -40
  24. package/src/components/ReferenceLineOverlay.tsx +65 -107
  25. package/src/components/ValueLineOverlay.tsx +9 -28
  26. package/src/components/XAxisOverlay.tsx +9 -29
  27. package/src/components/YAxisOverlay.tsx +6 -22
  28. package/src/draw/markerAtlas.ts +9 -9
  29. package/src/hooks/useBadge.ts +17 -32
  30. package/src/hooks/useCandlePaths.ts +24 -63
  31. package/src/hooks/useChartPaths.ts +27 -39
  32. package/src/hooks/useMultiSeriesLinePaths.ts +26 -28
  33. package/src/hooks/usePathBuilder.ts +42 -0
  34. package/src/math/spline.ts +22 -2
@@ -1,5 +1,4 @@
1
- import { Skia, type SkPath } from "@shopify/react-native-skia";
2
- import { useRef } from "react";
1
+ import { Skia } from "@shopify/react-native-skia";
3
2
  import {
4
3
  useDerivedValue,
5
4
  useFrameCallback,
@@ -12,15 +11,15 @@ import { buildCandleGeometry } from "../draw/candle";
12
11
  import type { ChartPadding } from "../draw/line";
13
12
  import { lerp } from "../math/lerp";
14
13
  import type { CandleMetrics, CandlePoint } from "../types";
14
+ import { usePathBuilder } from "./usePathBuilder";
15
15
 
16
16
  const CANDLE_WIDTH_LERP_SPEED = 0.08;
17
17
 
18
18
  /**
19
- * Persistent candle `SkPath`s — two per geometry slot (up/down bodies + up/down wicks).
20
- * Each derived value ping-pongs between its pair and mutates the picked path in place
21
- * so we don't allocate a new JSI-backed `SkPath` per frame. See {@link useChartPaths}
22
- * for the rationale (this was the primary driver of the iOS memory climb under live
23
- * ticking).
19
+ * Candle paths (up/down bodies + up/down wicks). Each is built into a reused
20
+ * `Skia.PathBuilder` and finalized with `detach()` each frame a fresh
21
+ * immutable `SkPath`, so Skia repaints without a per-curve ping-pong and no
22
+ * mutable `SkPath` is retained across frames.
24
23
  */
25
24
  export function useCandlePaths(
26
25
  engine: SingleEngineState,
@@ -34,36 +33,10 @@ export function useCandlePaths(
34
33
  const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
35
34
  const displayCandleWidth = useSharedValue(candleWidthSecs);
36
35
 
37
- const cacheRef = useRef<{
38
- upBodiesA: SkPath;
39
- upBodiesB: SkPath;
40
- downBodiesA: SkPath;
41
- downBodiesB: SkPath;
42
- upWicksA: SkPath;
43
- upWicksB: SkPath;
44
- downWicksA: SkPath;
45
- downWicksB: SkPath;
46
- ubTick: boolean;
47
- dbTick: boolean;
48
- uwTick: boolean;
49
- dwTick: boolean;
50
- } | null>(null);
51
- if (cacheRef.current === null) {
52
- cacheRef.current = {
53
- upBodiesA: Skia.Path.Make(),
54
- upBodiesB: Skia.Path.Make(),
55
- downBodiesA: Skia.Path.Make(),
56
- downBodiesB: Skia.Path.Make(),
57
- upWicksA: Skia.Path.Make(),
58
- upWicksB: Skia.Path.Make(),
59
- downWicksA: Skia.Path.Make(),
60
- downWicksB: Skia.Path.Make(),
61
- ubTick: false,
62
- dbTick: false,
63
- uwTick: false,
64
- dwTick: false,
65
- };
66
- }
36
+ const upBodiesBuilder = usePathBuilder();
37
+ const downBodiesBuilder = usePathBuilder();
38
+ const upWicksBuilder = usePathBuilder();
39
+ const downWicksBuilder = usePathBuilder();
67
40
 
68
41
  useFrameCallback((frameInfo) => {
69
42
  "worklet";
@@ -99,68 +72,56 @@ export function useCandlePaths(
99
72
 
100
73
  /* istanbul ignore next -- worklet */
101
74
  const upBodiesPath = useDerivedValue(() => {
102
- const cache = cacheRef.current!;
103
- cache.ubTick = !cache.ubTick;
104
- const path = cache.ubTick ? cache.upBodiesA : cache.upBodiesB;
105
- path.reset();
75
+ const b = upBodiesBuilder.value;
106
76
  const { bodies } = geometry.value;
107
77
  for (let i = 0; i < bodies.length; i++) {
108
78
  if (bodies[i].up) {
109
- path.addRect(
79
+ b.addRect(
110
80
  Skia.XYWHRect(bodies[i].x, bodies[i].y, bodies[i].w, bodies[i].h),
111
81
  );
112
82
  }
113
83
  }
114
- return path;
84
+ return b.detach();
115
85
  });
116
86
 
117
87
  /* istanbul ignore next -- worklet */
118
88
  const downBodiesPath = useDerivedValue(() => {
119
- const cache = cacheRef.current!;
120
- cache.dbTick = !cache.dbTick;
121
- const path = cache.dbTick ? cache.downBodiesA : cache.downBodiesB;
122
- path.reset();
89
+ const b = downBodiesBuilder.value;
123
90
  const { bodies } = geometry.value;
124
91
  for (let i = 0; i < bodies.length; i++) {
125
92
  if (!bodies[i].up) {
126
- path.addRect(
93
+ b.addRect(
127
94
  Skia.XYWHRect(bodies[i].x, bodies[i].y, bodies[i].w, bodies[i].h),
128
95
  );
129
96
  }
130
97
  }
131
- return path;
98
+ return b.detach();
132
99
  });
133
100
 
134
101
  /* istanbul ignore next -- worklet */
135
102
  const upWicksPath = useDerivedValue(() => {
136
- const cache = cacheRef.current!;
137
- cache.uwTick = !cache.uwTick;
138
- const path = cache.uwTick ? cache.upWicksA : cache.upWicksB;
139
- path.reset();
103
+ const b = upWicksBuilder.value;
140
104
  const { wicks } = geometry.value;
141
105
  for (let i = 0; i < wicks.length; i++) {
142
106
  if (wicks[i].up) {
143
- path.moveTo(wicks[i].x, wicks[i].y1);
144
- path.lineTo(wicks[i].x, wicks[i].y2);
107
+ b.moveTo(wicks[i].x, wicks[i].y1);
108
+ b.lineTo(wicks[i].x, wicks[i].y2);
145
109
  }
146
110
  }
147
- return path;
111
+ return b.detach();
148
112
  });
149
113
 
150
114
  /* istanbul ignore next -- worklet */
151
115
  const downWicksPath = useDerivedValue(() => {
152
- const cache = cacheRef.current!;
153
- cache.dwTick = !cache.dwTick;
154
- const path = cache.dwTick ? cache.downWicksA : cache.downWicksB;
155
- path.reset();
116
+ const b = downWicksBuilder.value;
156
117
  const { wicks } = geometry.value;
157
118
  for (let i = 0; i < wicks.length; i++) {
158
119
  if (!wicks[i].up) {
159
- path.moveTo(wicks[i].x, wicks[i].y1);
160
- path.lineTo(wicks[i].x, wicks[i].y2);
120
+ b.moveTo(wicks[i].x, wicks[i].y1);
121
+ b.lineTo(wicks[i].x, wicks[i].y2);
161
122
  }
162
123
  }
163
- return path;
124
+ return b.detach();
164
125
  });
165
126
 
166
127
  return { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } as const;
@@ -5,30 +5,29 @@ import type { SingleEngineState } from "../core/useLiveChartEngine";
5
5
  import { buildLinePoints, type ChartPadding } from "../draw/line";
6
6
  import { drawSpline, makeSplineScratch } from "../math/spline";
7
7
  import { blendPtsY, squigglifyPts } from "../math/squiggly";
8
+ import { usePathBuilder } from "./usePathBuilder";
8
9
 
9
10
  /**
10
- * Persistent `linePath` / `fillPath` `SkPath`s that are **mutated in place** each frame.
11
+ * Builds the `linePath` / `fillPath` with `Skia.PathBuilder`s reused across
12
+ * frames (one per curve, held in a SharedValue) and finalized with `detach()` —
13
+ * which returns a fresh immutable `SkPath` each frame and resets the builder.
14
+ * The fresh reference makes Reanimated notify subscribers (re-record + repaint)
15
+ * without the two-SkPath ping-pong the mutable-path pool needed.
11
16
  *
12
- * Why not `useDerivedValue(() => Skia.Path.Make())`? Each call would allocate a brand-new
13
- * JSI-backed `SkPath`, and on iOS the native GC lags the JS GC under a steady firehose —
14
- * resident memory climbs steadily for as long as the chart is mounted. Here we allocate
15
- * two paths per curve once and ping-pong the reference each frame so Reanimated's
16
- * value-equality early-return still fires its subscribers (i.e. the Skia reanimated
17
- * container re-records and repaints).
17
+ * The flat point buffer still ping-pongs (ptsA/ptsB) so the intermediate
18
+ * `flatPts` derived value changes reference each frame and re-runs linePath /
19
+ * fillPath.
18
20
  */
19
21
  export function useChartPaths(
20
22
  engine: SingleEngineState,
21
23
  padding: ChartPadding,
22
24
  morphT?: SharedValue<number>,
23
25
  ) {
24
- // Two persistent paths per curve; ping-pong so the returned reference changes
25
- // every frame, which keeps Reanimated's setter from short-circuiting the notify.
26
+ const lineBuilder = usePathBuilder();
27
+ const fillBuilder = usePathBuilder();
28
+
26
29
  const cacheRef = useRef<{
27
- lineA: SkPath;
28
- lineB: SkPath;
29
- fillA: SkPath;
30
- fillB: SkPath;
31
- tick: boolean;
30
+ emptyPath: SkPath;
32
31
  ptsA: number[];
33
32
  ptsB: number[];
34
33
  ptsTick: boolean;
@@ -36,13 +35,7 @@ export function useChartPaths(
36
35
  } | null>(null);
37
36
  if (cacheRef.current === null) {
38
37
  cacheRef.current = {
39
- lineA: Skia.Path.Make(),
40
- lineB: Skia.Path.Make(),
41
- fillA: Skia.Path.Make(),
42
- fillB: Skia.Path.Make(),
43
- tick: false,
44
- // Ping-pong point buffers: reused across frames but the returned reference
45
- // still alternates, so Reanimated keeps notifying linePath/fillPath.
38
+ emptyPath: Skia.Path.Make(),
46
39
  ptsA: [] as number[],
47
40
  ptsB: [] as number[],
48
41
  ptsTick: false,
@@ -89,32 +82,27 @@ export function useChartPaths(
89
82
  const linePath = useDerivedValue(() => {
90
83
  const cache = cacheRef.current!;
91
84
  const pts = flatPts.get();
92
- cache.tick = !cache.tick;
93
- const path = cache.tick ? cache.lineA : cache.lineB;
94
- path.reset();
95
85
  const n = pts.length >> 1;
96
- if (n < 2) return path;
97
- path.moveTo(pts[0], pts[1]);
98
- drawSpline(path, pts, cache.scratch);
99
- return path;
86
+ if (n < 2) return cache.emptyPath;
87
+ const b = lineBuilder.value;
88
+ b.moveTo(pts[0], pts[1]);
89
+ drawSpline(b, pts, cache.scratch);
90
+ return b.detach();
100
91
  });
101
92
 
102
93
  const fillPath = useDerivedValue(() => {
103
94
  const cache = cacheRef.current!;
104
95
  const pts = flatPts.get();
105
- // Use the same tick flag flipped by linePath — flipping again here would
106
- // keep both paths in sync, so just read the current parity.
107
- const path = cache.tick ? cache.fillA : cache.fillB;
108
- path.reset();
109
96
  const n = pts.length >> 1;
110
- if (n < 2) return path;
111
- path.moveTo(pts[0], pts[1]);
112
- drawSpline(path, pts, cache.scratch);
97
+ if (n < 2) return cache.emptyPath;
98
+ const b = fillBuilder.value;
99
+ b.moveTo(pts[0], pts[1]);
100
+ drawSpline(b, pts, cache.scratch);
113
101
  const bottom = engine.canvasHeight.get() - padding.bottom;
114
- path.lineTo(pts[(n - 1) * 2], bottom);
115
- path.lineTo(pts[0], bottom);
116
- path.close();
117
- return path;
102
+ b.lineTo(pts[(n - 1) * 2], bottom);
103
+ b.lineTo(pts[0], bottom);
104
+ b.close();
105
+ return b.detach();
118
106
  });
119
107
 
120
108
  return { linePath, fillPath } as const;
@@ -5,51 +5,46 @@ import { MAX_MULTI_SERIES } from "../constants";
5
5
  import type { MultiEngineState } from "../core/useLiveChartEngine";
6
6
  import { buildLinePoints, type ChartPadding } from "../draw/line";
7
7
  import { drawSpline, makeSplineScratch } from "../math/spline";
8
+ import { usePathBuilders } from "./usePathBuilder";
8
9
 
9
10
  /**
10
- * One derived shared value holding up to `MAX_MULTI_SERIES` Skia paths (unused slots are empty paths).
11
+ * One derived shared value holding up to `MAX_MULTI_SERIES` Skia paths (unused
12
+ * slots reuse one cached empty path).
11
13
  *
12
- * Each slot ping-pongs between two persistent `SkPath`s so `MultiSeriesStroke`'s
13
- * per-slot `useDerivedValue(() => paths.value[index])` sees a fresh reference each
14
- * tick (otherwise Reanimated's setter short-circuits and Skia never repaints).
15
- * No new JSI-backed `SkPath` is allocated per frame. See {@link useChartPaths}
16
- * for the memory rationale.
14
+ * Each visible series' path is built into a per-slot `Skia.PathBuilder` (reused
15
+ * across frames via a SharedValue) and finalized with `detach()` a fresh
16
+ * immutable `SkPath` per frame, so `MultiSeriesStroke`'s per-slot
17
+ * `useDerivedValue(() => paths.value[index])` repaints without the two-SkPath
18
+ * ping-pong. Only visible series allocate a path.
17
19
  */
18
20
  export function useMultiSeriesLinePaths(
19
21
  engine: MultiEngineState,
20
22
  padding: ChartPadding,
21
- ): SharedValue<ReturnType<typeof Skia.Path.Make>[]> {
23
+ ): SharedValue<SkPath[]> {
24
+ const builders = usePathBuilders(MAX_MULTI_SERIES);
25
+
22
26
  const poolRef = useRef<{
23
- a: SkPath[];
24
- b: SkPath[];
25
- tick: boolean;
27
+ empty: SkPath;
26
28
  ptsBuf: number[];
27
29
  scratch: ReturnType<typeof makeSplineScratch>;
28
30
  } | null>(null);
29
31
  if (poolRef.current === null) {
30
- const a: SkPath[] = [];
31
- const b: SkPath[] = [];
32
- for (let i = 0; i < MAX_MULTI_SERIES; i++) {
33
- a.push(Skia.Path.Make());
34
- b.push(Skia.Path.Make());
35
- }
36
- // One point buffer + spline scratch, reused across slots (built sequentially)
37
- // and across frames — no per-frame, per-series array allocation.
38
- poolRef.current = { a, b, tick: false, ptsBuf: [] as number[], scratch: makeSplineScratch() };
32
+ poolRef.current = {
33
+ empty: Skia.Path.Make(),
34
+ ptsBuf: [] as number[],
35
+ scratch: makeSplineScratch(),
36
+ };
39
37
  }
40
38
 
41
39
  return useDerivedValue(() => {
42
40
  const pool = poolRef.current!;
43
- pool.tick = !pool.tick;
44
- const slots = pool.tick ? pool.a : pool.b;
41
+ const slots = builders.value;
45
42
  const s = engine.series.get();
46
43
  const displays = engine.displaySeriesValues.get();
47
- const out: ReturnType<typeof Skia.Path.Make>[] = [];
44
+ const out: SkPath[] = [];
48
45
  for (let i = 0; i < MAX_MULTI_SERIES; i++) {
49
- const path = slots[i];
50
- path.reset();
51
46
  if (i >= s.length) {
52
- out.push(path);
47
+ out.push(pool.empty);
53
48
  continue;
54
49
  }
55
50
  const pts = buildLinePoints(
@@ -66,10 +61,13 @@ export function useMultiSeriesLinePaths(
66
61
  );
67
62
  const n = pts.length >> 1;
68
63
  if (n >= 2) {
69
- path.moveTo(pts[0], pts[1]);
70
- drawSpline(path, pts, pool.scratch);
64
+ const b = slots[i];
65
+ b.moveTo(pts[0], pts[1]);
66
+ drawSpline(b, pts, pool.scratch);
67
+ out.push(b.detach());
68
+ } else {
69
+ out.push(pool.empty);
71
70
  }
72
- out.push(path);
73
71
  }
74
72
  return out;
75
73
  });
@@ -0,0 +1,42 @@
1
+ import { Skia } from "@shopify/react-native-skia";
2
+ import { useMemo } from "react";
3
+ import { useSharedValue, type SharedValue } from "react-native-reanimated";
4
+
5
+ /** A `Skia.PathBuilder` instance (typed without depending on a named export). */
6
+ export type ReanimatedPathBuilder = ReturnType<typeof Skia.PathBuilder.Make>;
7
+
8
+ /**
9
+ * A `Skia.PathBuilder` made accessible inside Reanimated worklets.
10
+ *
11
+ * Unlike `SkPath`, an `SkPathBuilder` is **not** a Reanimated-shareable host
12
+ * object: stashing one in a `useRef` and reading it from a `useDerivedValue`
13
+ * worklet yields `undefined`. Wrapping it in a `SharedValue` is the supported
14
+ * way to cross the JS→UI boundary, so the builder is created once on the JS
15
+ * thread and reused every frame on the UI thread.
16
+ *
17
+ * Per-frame usage in a worklet:
18
+ *
19
+ * const b = builder.value;
20
+ * b.moveTo(x, y); b.lineTo(...); // emit verbs
21
+ * return b.detach(); // fresh immutable SkPath; resets the builder
22
+ *
23
+ * `detach()` returns a new `SkPath` and resets the builder for the next frame,
24
+ * so the returned reference changes every frame and Reanimated notifies
25
+ * subscribers without the two-path ping-pong the mutable-`SkPath` pool needs.
26
+ */
27
+ export function usePathBuilder(): SharedValue<ReanimatedPathBuilder> {
28
+ const builder = useMemo(() => Skia.PathBuilder.Make(), []);
29
+ return useSharedValue(builder);
30
+ }
31
+
32
+ /** Like {@link usePathBuilder} but a fixed-length array of builders (e.g. one per series). */
33
+ export function usePathBuilders(
34
+ count: number,
35
+ ): SharedValue<ReanimatedPathBuilder[]> {
36
+ const builders = useMemo(() => {
37
+ const arr: ReanimatedPathBuilder[] = [];
38
+ for (let i = 0; i < count; i++) arr.push(Skia.PathBuilder.Make());
39
+ return arr;
40
+ }, [count]);
41
+ return useSharedValue(builders);
42
+ }
@@ -1,4 +1,3 @@
1
- import type { SkPath } from "@shopify/react-native-skia";
2
1
  import type { LiveChartPoint } from "../types";
3
2
 
4
3
  /**
@@ -33,7 +32,28 @@ export function makeSplineScratch(): SplineScratch {
33
32
  *
34
33
  * @see https://github.com/benjitaylor/liveline
35
34
  */
36
- export function drawSpline(path: SkPath, pts: number[], scratch?: SplineScratch) {
35
+ /**
36
+ * Minimal structural sink for {@link drawSpline}: just the verb-emitting methods
37
+ * it calls. Both `SkPath` and `SkPathBuilder` satisfy it, so the spline can be
38
+ * built into either a mutable `SkPath` or a `Skia.PathBuilder`.
39
+ */
40
+ export type SplinePathSink = {
41
+ lineTo: (x: number, y: number) => unknown;
42
+ cubicTo: (
43
+ c1x: number,
44
+ c1y: number,
45
+ c2x: number,
46
+ c2y: number,
47
+ x: number,
48
+ y: number,
49
+ ) => unknown;
50
+ };
51
+
52
+ export function drawSpline(
53
+ path: SplinePathSink,
54
+ pts: number[],
55
+ scratch?: SplineScratch,
56
+ ) {
37
57
  "worklet";
38
58
  const n = pts.length >> 1;
39
59
  if (n < 2) return;