react-native-livechart 4.10.0 → 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 (73) 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/thresholdSplitShaderSource.d.ts +13 -0
  25. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
  26. package/dist/core/useLiveChartEngine.d.ts +9 -1
  27. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  28. package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
  29. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  30. package/dist/draw/particleAtlas.d.ts +3 -2
  31. package/dist/draw/particleAtlas.d.ts.map +1 -1
  32. package/dist/hooks/crosshairShared.d.ts +4 -0
  33. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  34. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  35. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  36. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  37. package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
  38. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/math/squiggly.d.ts +6 -3
  42. package/dist/math/squiggly.d.ts.map +1 -1
  43. package/dist/math/threshold.d.ts +2 -2
  44. package/dist/types.d.ts +28 -2
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/CrosshairLine.tsx +17 -0
  48. package/src/components/CrosshairOverlay.tsx +48 -35
  49. package/src/components/CustomReferenceLineOverlay.tsx +16 -7
  50. package/src/components/DegenParticlesOverlay.tsx +40 -5
  51. package/src/components/LeftEdgeFade.tsx +12 -6
  52. package/src/components/LiveChart.tsx +29 -16
  53. package/src/components/LiveChartSeries.tsx +94 -21
  54. package/src/components/LoadingOverlay.tsx +26 -12
  55. package/src/components/MarkerOverlay.tsx +20 -3
  56. package/src/components/MultiSeriesDots.tsx +4 -2
  57. package/src/components/MultiSeriesTooltipStack.tsx +4 -2
  58. package/src/components/MultiSeriesValueLabels.tsx +4 -2
  59. package/src/components/MultiSeriesValueLines.tsx +4 -2
  60. package/src/components/ThresholdSplitShader.tsx +5 -37
  61. package/src/components/thresholdSplitShaderSource.ts +70 -0
  62. package/src/core/useLiveChartEngine.ts +110 -42
  63. package/src/core/useLiveChartSeriesEngine.ts +137 -70
  64. package/src/draw/particleAtlas.ts +21 -10
  65. package/src/hooks/crosshairShared.ts +5 -0
  66. package/src/hooks/useChartPaths.ts +11 -0
  67. package/src/hooks/useCrosshair.ts +16 -10
  68. package/src/hooks/useCrosshairSeries.ts +11 -6
  69. package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
  70. package/src/index.ts +1 -0
  71. package/src/math/squiggly.ts +28 -11
  72. package/src/math/threshold.ts +2 -2
  73. package/src/types.ts +33 -3
@@ -1,7 +1,6 @@
1
1
  import { DashPathEffect, Group, Path } from "@shopify/react-native-skia";
2
2
 
3
3
  import { useDerivedValue } from "react-native-reanimated";
4
- import { MAX_MULTI_SERIES } from "../constants";
5
4
  import type { ChartPadding } from "../draw/line";
6
5
  import type { ResolvedValueLineConfig } from "../core/resolveConfig";
7
6
  import type { MultiEngineState } from "../core/useLiveChartEngine";
@@ -72,15 +71,18 @@ export function MultiSeriesValueLines({
72
71
  padding,
73
72
  colors,
74
73
  config,
74
+ seriesCount,
75
75
  }: {
76
76
  engine: MultiEngineState;
77
77
  padding: ChartPadding;
78
78
  colors: string[];
79
79
  config: ResolvedValueLineConfig;
80
+ /** Number of live series slots to mount. */
81
+ seriesCount: number;
80
82
  }) {
81
83
  return (
82
84
  <Group>
83
- {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
85
+ {Array.from({ length: seriesCount }, (_, i) => (
84
86
  <SeriesValueLineAtIndex
85
87
  key={i}
86
88
  index={i}
@@ -2,6 +2,7 @@ import { Shader, Skia, type Uniforms } from "@shopify/react-native-skia";
2
2
  import type { SharedValue } from "react-native-reanimated";
3
3
 
4
4
  import { THRESHOLD_SAMPLE_COUNT } from "../math/threshold";
5
+ import { buildThresholdSplitShaderSource } from "./thresholdSplitShaderSource";
5
6
 
6
7
  /**
7
8
  * Per-fragment split shader for a *time-varying* threshold. For each fragment it
@@ -15,49 +16,16 @@ import { THRESHOLD_SAMPLE_COUNT } from "../math/threshold";
15
16
  * alpha-reduced colors). Output is premultiplied — Skia's shader convention, as in
16
17
  * `AreaDotsOverlay`. One GPU draw, no per-frame clip / `saveLayer`.
17
18
  */
18
- const SPLIT_SKSL = `
19
- // Pixel-X span of the sample grid (thresholdSampleSpanX) — overhangs the plot
20
- // and glides with the window so features move fluidly, frame to frame.
21
- uniform float sampleLeft;
22
- uniform float sampleRight;
23
- // X where the threshold ends (extendToNow: false → the last point's pixel-X,
24
- // else a huge sentinel). Fragments right of it paint restColor: the plain line
25
- // color for the stroke, transparent for the band.
26
- uniform float clipRight;
27
- uniform vec4 aboveColor; // straight-alpha rgba, 0..1
28
- uniform vec4 belowColor; // straight-alpha rgba, 0..1
29
- uniform vec4 restColor; // straight-alpha rgba, 0..1
30
- uniform float samples[${THRESHOLD_SAMPLE_COUNT}];
31
-
32
- half4 main(vec2 xy) {
33
- if (xy.x > clipRight) {
34
- return half4(half3(restColor.rgb) * restColor.a, restColor.a);
35
- }
36
- float span = sampleRight - sampleLeft;
37
- float u = span > 0.0 ? (xy.x - sampleLeft) / span : 0.0;
38
- u = clamp(u, 0.0, 1.0) * float(${THRESHOLD_SAMPLE_COUNT - 1});
39
- // SkSL forbids indexing a uniform array with a non-constant index, so walk the
40
- // segments in an unrolled, constant-bound loop — the index is the loop variable
41
- // (a compile-time constant per unrolled iteration) — and keep the last segment
42
- // whose left edge is at or before the fragment's u.
43
- float thrY = samples[0];
44
- for (int j = 0; j < ${THRESHOLD_SAMPLE_COUNT - 1}; j++) {
45
- if (u >= float(j)) {
46
- thrY = mix(samples[j], samples[j + 1], clamp(u - float(j), 0.0, 1.0));
47
- }
48
- }
49
- // 1 above the boundary, 0 below, with ~1px antialiasing across it.
50
- float above = clamp(thrY - xy.y + 0.5, 0.0, 1.0);
51
- vec4 c = mix(belowColor, aboveColor, above);
52
- return half4(half3(c.rgb) * c.a, c.a);
53
- }`;
19
+ const THRESHOLD_SPLIT_SKSL = buildThresholdSplitShaderSource(
20
+ THRESHOLD_SAMPLE_COUNT,
21
+ );
54
22
 
55
23
  // Compiled once. `RuntimeEffect.Make` THROWS on a compile error, so guard it —
56
24
  // a shader failure must degrade to "no split" (the line keeps its plain color),
57
25
  // never crash every chart screen at import. Null → the component no-ops.
58
26
  let SPLIT_EFFECT: ReturnType<typeof Skia.RuntimeEffect.Make> = null;
59
27
  try {
60
- SPLIT_EFFECT = Skia.RuntimeEffect.Make(SPLIT_SKSL);
28
+ SPLIT_EFFECT = Skia.RuntimeEffect.Make(THRESHOLD_SPLIT_SKSL);
61
29
  } catch {
62
30
  SPLIT_EFFECT = null;
63
31
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Build a balanced constant-index lookup for the threshold sample array.
3
+ *
4
+ * SkSL does not allow a runtime value to index a uniform array. A linear loop
5
+ * works around that restriction, but makes every covered fragment visit every
6
+ * segment. This tree keeps every array index constant while selecting the same
7
+ * segment in at most `ceil(log2(sampleCount - 1))` comparisons (six for the
8
+ * production 64-sample shader).
9
+ */
10
+ export function buildThresholdLookupTree(sampleCount: number): string {
11
+ if (!Number.isInteger(sampleCount) || sampleCount < 2) {
12
+ throw new Error("Threshold shader needs at least two samples");
13
+ }
14
+
15
+ const build = (first: number, end: number, indent: string): string => {
16
+ if (end - first === 1) {
17
+ return `${indent}return mix(samples[${first}], samples[${first + 1}], clamp(u - float(${first}), 0.0, 1.0));`;
18
+ }
19
+
20
+ const middle = Math.floor((first + end) / 2);
21
+ const childIndent = `${indent} `;
22
+ return `${indent}if (u < float(${middle})) {
23
+ ${build(first, middle, childIndent)}
24
+ ${indent}} else {
25
+ ${build(middle, end, childIndent)}
26
+ ${indent}}`;
27
+ };
28
+
29
+ return build(0, sampleCount - 1, " ");
30
+ }
31
+
32
+ /** Build the complete time-varying threshold split shader. */
33
+ export function buildThresholdSplitShaderSource(sampleCount: number): string {
34
+ const lookup = buildThresholdLookupTree(sampleCount);
35
+ return `
36
+ // Pixel-X span of the sample grid (thresholdSampleSpanX) — overhangs the plot
37
+ // and glides with the window so features move fluidly, frame to frame.
38
+ uniform float sampleLeft;
39
+ uniform float sampleRight;
40
+ // X where the threshold ends (extendToNow: false → the last point's pixel-X,
41
+ // else a huge sentinel). Fragments right of it paint restColor: the plain line
42
+ // color for the stroke, transparent for the band.
43
+ uniform float clipRight;
44
+ uniform vec4 aboveColor; // straight-alpha rgba, 0..1
45
+ uniform vec4 belowColor; // straight-alpha rgba, 0..1
46
+ uniform vec4 restColor; // straight-alpha rgba, 0..1
47
+ uniform float samples[${sampleCount}];
48
+
49
+ float thresholdAt(float u) {
50
+ ${lookup}
51
+ }
52
+
53
+ half4 main(vec2 xy) {
54
+ if (xy.x > clipRight) {
55
+ return half4(half3(restColor.rgb) * restColor.a, restColor.a);
56
+ }
57
+ float span = sampleRight - sampleLeft;
58
+ float u = span > 0.0 ? (xy.x - sampleLeft) / span : 0.0;
59
+ u = clamp(u, 0.0, 1.0) * float(${sampleCount - 1});
60
+ // Uniform-array indices must stay compile-time constants in SkSL. The
61
+ // generated branch tree selects the same segment as floor(u), including the
62
+ // clamped final endpoint, in logarithmic comparisons instead of walking every
63
+ // segment for every fragment.
64
+ float thrY = thresholdAt(u);
65
+ // 1 above the boundary, 0 below, with ~1px antialiasing across it.
66
+ float above = clamp(thrY - xy.y + 0.5, 0.0, 1.0);
67
+ vec4 c = mix(belowColor, aboveColor, above);
68
+ return half4(half3(c.rgb) * c.a, c.a);
69
+ }`;
70
+ }
@@ -18,7 +18,11 @@ import {
18
18
  } from "react-native-reanimated";
19
19
  import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
20
20
  import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
21
- import { tickLiveChartEngineFrame } from "./liveChartEngineTick";
21
+ import {
22
+ tickLiveChartEngineFrame,
23
+ type EngineTickInput,
24
+ type EngineTickMutable,
25
+ } from "./liveChartEngineTick";
22
26
 
23
27
  export interface EngineConfig {
24
28
  data: SharedValue<LiveChartPoint[]>;
@@ -229,6 +233,42 @@ export interface EngineFrameRefs {
229
233
  extremaMaxTime: SharedValue<number>;
230
234
  }
231
235
 
236
+ /** Stable state/input containers reused by the UI-thread frame callback. */
237
+ export interface EngineFrameScratch {
238
+ state: EngineTickMutable;
239
+ input: EngineTickInput;
240
+ }
241
+
242
+ /** Allocate one single-series frame scratch. Call once per engine instance. */
243
+ export function makeEngineFrameScratch(): EngineFrameScratch {
244
+ return {
245
+ state: {
246
+ displayValue: 0,
247
+ displayMin: 0,
248
+ displayMax: 1,
249
+ displayWindow: 0,
250
+ timestamp: 0,
251
+ liveEdge: 0,
252
+ edgeValue: 0,
253
+ extremaMinValue: NaN,
254
+ extremaMaxValue: NaN,
255
+ extremaMinTime: NaN,
256
+ extremaMaxTime: NaN,
257
+ },
258
+ input: {
259
+ dt: MS_PER_FRAME_60FPS,
260
+ canvasWidth: 0,
261
+ canvasHeight: 0,
262
+ timeWindow: 0,
263
+ smoothing: 0,
264
+ exaggerate: false,
265
+ referenceValue: undefined,
266
+ targetValue: 0,
267
+ points: [],
268
+ },
269
+ };
270
+ }
271
+
232
272
  /**
233
273
  * Shared between the `useFrameCallback` worklet and unit tests.
234
274
  * Mutates shared values from a snapshot tick (`tickLiveChartEngineFrame`).
@@ -236,54 +276,77 @@ export interface EngineFrameRefs {
236
276
  export function applyLiveChartEngineFrame(
237
277
  frameInfo: { timeSincePreviousFrame?: number | null },
238
278
  sv: EngineFrameRefs,
279
+ scratch?: EngineFrameScratch,
239
280
  ): void {
240
281
  "worklet";
241
282
  const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
242
283
  // One-shot settle: a `snapKey` change set this flag; consume it for this tick
243
284
  // and clear it below so only this frame snaps (live ticks stay smoothed).
244
285
  const snap = sv.snapSV?.value ?? false;
245
- const state = {
246
- displayValue: sv.displayValue.value,
247
- displayMin: sv.displayMin.value,
248
- displayMax: sv.displayMax.value,
249
- displayWindow: sv.displayWindow.value,
250
- timestamp: sv.timestamp.value,
251
- liveEdge: sv.liveEdgeSV?.value ?? 0,
252
- edgeValue: sv.edgeValueSV?.value ?? 0,
253
- extremaMinValue: sv.extremaMinValue.value,
254
- extremaMaxValue: sv.extremaMaxValue.value,
255
- extremaMinTime: sv.extremaMinTime.value,
256
- extremaMaxTime: sv.extremaMaxTime.value,
286
+ const state: EngineTickMutable = scratch?.state ?? {
287
+ displayValue: 0,
288
+ displayMin: 0,
289
+ displayMax: 1,
290
+ displayWindow: 0,
291
+ timestamp: 0,
292
+ liveEdge: 0,
293
+ edgeValue: 0,
294
+ extremaMinValue: NaN,
295
+ extremaMaxValue: NaN,
296
+ extremaMinTime: NaN,
297
+ extremaMaxTime: NaN,
257
298
  };
258
- tickLiveChartEngineFrame(state, {
299
+ state.displayValue = sv.displayValue.value;
300
+ state.displayMin = sv.displayMin.value;
301
+ state.displayMax = sv.displayMax.value;
302
+ state.displayWindow = sv.displayWindow.value;
303
+ state.timestamp = sv.timestamp.value;
304
+ state.liveEdge = sv.liveEdgeSV?.value ?? 0;
305
+ state.edgeValue = sv.edgeValueSV?.value ?? 0;
306
+ state.extremaMinValue = sv.extremaMinValue.value;
307
+ state.extremaMaxValue = sv.extremaMaxValue.value;
308
+ state.extremaMinTime = sv.extremaMinTime.value;
309
+ state.extremaMaxTime = sv.extremaMaxTime.value;
310
+
311
+ const input: EngineTickInput = scratch?.input ?? {
259
312
  dt,
260
- canvasWidth: sv.canvasWidth.value,
261
- canvasHeight: sv.canvasHeight.value,
262
- timeWindow: sv.timeWindow.value,
263
- smoothing: sv.smoothing.value,
264
- adaptiveSpeedBoost: sv.adaptiveSpeedBoostSV?.value,
265
- exaggerate: sv.exaggerateSV.value,
266
- referenceValue: sv.referenceValue.value,
267
- referenceValues: sv.referenceValues?.value,
268
- thresholdRangePoints: sv.thresholdRangePoints?.value,
269
- thresholdRangeExtendToNow: sv.thresholdRangeExtendToNow?.value ?? true,
270
- nonNegative: sv.nonNegativeSV?.value ?? false,
271
- maxValue: sv.maxValueSV?.value,
272
- nowOverride: sv.nowOverrideSV?.value,
273
- windowBuffer: sv.windowBufferSV?.value ?? 0,
274
- targetValue: sv.value.value,
275
- points: sv.data.value,
276
- nowSeconds: Date.now() / 1000,
277
- paused: sv.pausedSV.value,
278
- snap,
279
- viewEnd: sv.viewEndSV?.value,
280
- returnT: sv.returnTSV?.value,
281
- returnFrom: sv.returnFromSV?.value,
282
- viewWindow: sv.viewWindowSV?.value,
283
- mode: sv.modeSV.value,
284
- candles: sv.candles?.value,
285
- liveCandle: sv.liveCandle?.value,
286
- });
313
+ canvasWidth: 0,
314
+ canvasHeight: 0,
315
+ timeWindow: 0,
316
+ smoothing: 0,
317
+ exaggerate: false,
318
+ referenceValue: undefined,
319
+ targetValue: 0,
320
+ points: [],
321
+ };
322
+ input.dt = dt;
323
+ input.canvasWidth = sv.canvasWidth.value;
324
+ input.canvasHeight = sv.canvasHeight.value;
325
+ input.timeWindow = sv.timeWindow.value;
326
+ input.smoothing = sv.smoothing.value;
327
+ input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
328
+ input.exaggerate = sv.exaggerateSV.value;
329
+ input.referenceValue = sv.referenceValue.value;
330
+ input.referenceValues = sv.referenceValues?.value;
331
+ input.thresholdRangePoints = sv.thresholdRangePoints?.value;
332
+ input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
333
+ input.nonNegative = sv.nonNegativeSV?.value ?? false;
334
+ input.maxValue = sv.maxValueSV?.value;
335
+ input.nowOverride = sv.nowOverrideSV?.value;
336
+ input.windowBuffer = sv.windowBufferSV?.value ?? 0;
337
+ input.targetValue = sv.value.value;
338
+ input.points = sv.data.value;
339
+ input.nowSeconds = Date.now() / 1000;
340
+ input.paused = sv.pausedSV.value;
341
+ input.snap = snap;
342
+ input.viewEnd = sv.viewEndSV?.value;
343
+ input.returnT = sv.returnTSV?.value;
344
+ input.returnFrom = sv.returnFromSV?.value;
345
+ input.viewWindow = sv.viewWindowSV?.value;
346
+ input.mode = sv.modeSV.value;
347
+ input.candles = sv.candles?.value;
348
+ input.liveCandle = sv.liveCandle?.value;
349
+ tickLiveChartEngineFrame(state, input);
287
350
  sv.displayValue.value = state.displayValue;
288
351
  sv.displayMin.value = state.displayMin;
289
352
  sv.displayMax.value = state.displayMax;
@@ -467,12 +530,16 @@ export function useLiveChartEngine(
467
530
  extremaMinTime,
468
531
  extremaMaxTime,
469
532
  };
533
+ const frameScratchRef = useRef<EngineFrameScratch | null>(null);
534
+ if (frameScratchRef.current === null) {
535
+ frameScratchRef.current = makeEngineFrameScratch();
536
+ }
470
537
 
471
538
  // `autostart=false` registers the frame callback without running it — the live
472
539
  // loop is fully inert in static mode (the invariant that makes this worth it).
473
540
  useFrameCallback((frameInfo) => {
474
541
  "worklet";
475
- applyLiveChartEngineFrame(frameInfo, frameRefs);
542
+ applyLiveChartEngineFrame(frameInfo, frameRefs, frameScratchRef.current!);
476
543
  }, !config.static);
477
544
 
478
545
  // When time-scroll is disabled while scrolled back, return the window to the
@@ -532,6 +599,7 @@ export function useLiveChartEngine(
532
599
  applyLiveChartEngineFrame(
533
600
  { timeSincePreviousFrame: MS_PER_FRAME_60FPS },
534
601
  frameRefs,
602
+ frameScratchRef.current!,
535
603
  );
536
604
  },
537
605
  );
@@ -11,7 +11,11 @@ import {
11
11
  } from "react-native-reanimated";
12
12
  import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
13
13
  import type { LiveChartPoint, SeriesConfig } from "../types";
14
- import { tickLiveChartSeriesEngineFrame } from "./liveChartSeriesEngineTick";
14
+ import {
15
+ tickLiveChartSeriesEngineFrame,
16
+ type MultiEngineTickInput,
17
+ type MultiEngineTickMutable,
18
+ } from "./liveChartSeriesEngineTick";
15
19
  import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
16
20
 
17
21
  export interface MultiSeriesEngineConfig {
@@ -94,10 +98,10 @@ export interface MultiEngineFrameRefs {
94
98
  }
95
99
 
96
100
  /**
97
- * Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The two
101
+ * Reusable per-frame scratch for {@link applyLiveChartSeriesEngineFrame}. The
98
102
  * output arrays ping-pong so the assigned reference still changes each frame
99
- * (Reanimated propagates on reference change) without allocating a fresh array
100
- * per frame. Create one per engine (the React Compiler keeps it stable).
103
+ * (Reanimated propagates on reference change), while the state/input containers
104
+ * are mutated in place. Create one per engine.
101
105
  */
102
106
  export interface MultiSeriesEngineScratch {
103
107
  dvA: number[];
@@ -105,6 +109,46 @@ export interface MultiSeriesEngineScratch {
105
109
  opA: number[];
106
110
  opB: number[];
107
111
  tick: boolean;
112
+ state: MultiEngineTickMutable;
113
+ input: MultiEngineTickInput;
114
+ }
115
+
116
+ /** Allocate one multi-series frame scratch. Call once per engine instance. */
117
+ export function makeMultiSeriesEngineScratch(): MultiSeriesEngineScratch {
118
+ const dvA: number[] = [];
119
+ const dvB: number[] = [];
120
+ const opA: number[] = [];
121
+ const opB: number[] = [];
122
+ return {
123
+ dvA,
124
+ dvB,
125
+ opA,
126
+ opB,
127
+ tick: false,
128
+ state: {
129
+ displayMin: 0,
130
+ displayMax: 1,
131
+ displayWindow: 0,
132
+ timestamp: 0,
133
+ liveEdge: 0,
134
+ displayValues: dvA,
135
+ opacities: opA,
136
+ extremaMinValue: NaN,
137
+ extremaMaxValue: NaN,
138
+ extremaMinTime: NaN,
139
+ extremaMaxTime: NaN,
140
+ },
141
+ input: {
142
+ dt: MS_PER_FRAME_60FPS,
143
+ canvasWidth: 0,
144
+ canvasHeight: 0,
145
+ timeWindow: 0,
146
+ smoothing: 0,
147
+ exaggerate: false,
148
+ referenceValue: undefined,
149
+ series: [],
150
+ },
151
+ };
108
152
  }
109
153
 
110
154
  /**
@@ -113,8 +157,8 @@ export interface MultiSeriesEngineScratch {
113
157
  * to lerp per-series display values and opacities. Extracted as a
114
158
  * pure function so it can be called from both `useFrameCallback` and tests.
115
159
  *
116
- * Pass `scratch` to reuse the output arrays across frames instead of allocating
117
- * two via `.slice()` each frame; omit it (tests) to fall back to allocation.
160
+ * Pass `scratch` to reuse the output arrays plus state/input containers across
161
+ * frames; omit it (tests) to retain the standalone allocation fallback.
118
162
  */
119
163
  export function applyLiveChartSeriesEngineFrame(
120
164
  frameInfo: { timeSincePreviousFrame?: number | null },
@@ -143,42 +187,63 @@ export function applyLiveChartSeriesEngineFrame(
143
187
  displayValues = curDv.slice();
144
188
  opacities = curOp.slice();
145
189
  }
146
- const state = {
147
- displayMin: sv.displayMin.value,
148
- displayMax: sv.displayMax.value,
149
- displayWindow: sv.displayWindow.value,
150
- timestamp: sv.timestamp.value,
151
- liveEdge: sv.liveEdgeSV?.value ?? 0,
190
+ const state: MultiEngineTickMutable = scratch?.state ?? {
191
+ displayMin: 0,
192
+ displayMax: 1,
193
+ displayWindow: 0,
194
+ timestamp: 0,
195
+ liveEdge: 0,
152
196
  displayValues,
153
197
  opacities,
154
- extremaMinValue: sv.extremaMinValue.value,
155
- extremaMaxValue: sv.extremaMaxValue.value,
156
- extremaMinTime: sv.extremaMinTime.value,
157
- extremaMaxTime: sv.extremaMaxTime.value,
198
+ extremaMinValue: NaN,
199
+ extremaMaxValue: NaN,
200
+ extremaMinTime: NaN,
201
+ extremaMaxTime: NaN,
158
202
  };
159
- tickLiveChartSeriesEngineFrame(state, {
203
+ state.displayMin = sv.displayMin.value;
204
+ state.displayMax = sv.displayMax.value;
205
+ state.displayWindow = sv.displayWindow.value;
206
+ state.timestamp = sv.timestamp.value;
207
+ state.liveEdge = sv.liveEdgeSV?.value ?? 0;
208
+ state.displayValues = displayValues;
209
+ state.opacities = opacities;
210
+ state.extremaMinValue = sv.extremaMinValue.value;
211
+ state.extremaMaxValue = sv.extremaMaxValue.value;
212
+ state.extremaMinTime = sv.extremaMinTime.value;
213
+ state.extremaMaxTime = sv.extremaMaxTime.value;
214
+
215
+ const input: MultiEngineTickInput = scratch?.input ?? {
160
216
  dt,
161
- canvasWidth: sv.canvasWidth.value,
162
- canvasHeight: sv.canvasHeight.value,
163
- timeWindow: sv.timeWindow.value,
164
- smoothing: sv.smoothing.value,
165
- adaptiveSpeedBoost: sv.adaptiveSpeedBoostSV?.value,
166
- exaggerate: sv.exaggerateSV.value,
167
- referenceValue: sv.referenceValue.value,
168
- referenceValues: sv.referenceValues?.value,
169
- nonNegative: sv.nonNegativeSV?.value ?? false,
170
- maxValue: sv.maxValueSV?.value,
171
- nowOverride: sv.nowOverrideSV?.value,
172
- windowBuffer: sv.windowBufferSV?.value ?? 0,
217
+ canvasWidth: 0,
218
+ canvasHeight: 0,
219
+ timeWindow: 0,
220
+ smoothing: 0,
221
+ exaggerate: false,
222
+ referenceValue: undefined,
173
223
  series: seriesSnap,
174
- nowSeconds: Date.now() / 1000,
175
- paused: sv.pausedSV.value,
176
- snap,
177
- viewEnd: sv.viewEndSV?.value,
178
- returnT: sv.returnTSV?.value,
179
- returnFrom: sv.returnFromSV?.value,
180
- viewWindow: sv.viewWindowSV?.value,
181
- });
224
+ };
225
+ input.dt = dt;
226
+ input.canvasWidth = sv.canvasWidth.value;
227
+ input.canvasHeight = sv.canvasHeight.value;
228
+ input.timeWindow = sv.timeWindow.value;
229
+ input.smoothing = sv.smoothing.value;
230
+ input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
231
+ input.exaggerate = sv.exaggerateSV.value;
232
+ input.referenceValue = sv.referenceValue.value;
233
+ input.referenceValues = sv.referenceValues?.value;
234
+ input.nonNegative = sv.nonNegativeSV?.value ?? false;
235
+ input.maxValue = sv.maxValueSV?.value;
236
+ input.nowOverride = sv.nowOverrideSV?.value;
237
+ input.windowBuffer = sv.windowBufferSV?.value ?? 0;
238
+ input.series = seriesSnap;
239
+ input.nowSeconds = Date.now() / 1000;
240
+ input.paused = sv.pausedSV.value;
241
+ input.snap = snap;
242
+ input.viewEnd = sv.viewEndSV?.value;
243
+ input.returnT = sv.returnTSV?.value;
244
+ input.returnFrom = sv.returnFromSV?.value;
245
+ input.viewWindow = sv.viewWindowSV?.value;
246
+ tickLiveChartSeriesEngineFrame(state, input);
182
247
  sv.displayMin.value = state.displayMin;
183
248
  sv.displayMax.value = state.displayMax;
184
249
  sv.displayWindow.value = state.displayWindow;
@@ -272,46 +337,48 @@ export function useLiveChartSeriesEngine(
272
337
  // Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
273
338
  const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
274
339
  if (scratchRef.current === null) {
275
- scratchRef.current = { dvA: [], dvB: [], opA: [], opB: [], tick: false };
340
+ scratchRef.current = makeMultiSeriesEngineScratch();
276
341
  }
277
342
 
343
+ const frameRefs: MultiEngineFrameRefs = {
344
+ series,
345
+ displaySeriesValues,
346
+ seriesOpacities,
347
+ displayMin,
348
+ displayMax,
349
+ displayWindow,
350
+ timestamp,
351
+ canvasWidth,
352
+ canvasHeight,
353
+ timeWindow,
354
+ smoothing,
355
+ adaptiveSpeedBoostSV,
356
+ exaggerateSV,
357
+ referenceValue,
358
+ referenceValues,
359
+ nonNegativeSV,
360
+ maxValueSV,
361
+ nowOverrideSV,
362
+ windowBufferSV,
363
+ pausedSV,
364
+ viewEndSV: viewEnd,
365
+ returnTSV: returnT,
366
+ returnFromSV: returnFrom,
367
+ viewWindowSV: viewWindow,
368
+ liveEdgeSV: liveEdge,
369
+ snapSV,
370
+ extremaMinValue,
371
+ extremaMaxValue,
372
+ extremaMinTime,
373
+ extremaMaxTime,
374
+ };
375
+
278
376
  useFrameCallback((frameInfo) => {
279
377
  "worklet";
280
378
  const scratch = scratchRef.current!;
281
379
  applyLiveChartSeriesEngineFrame(
282
380
  frameInfo,
283
- {
284
- series,
285
- displaySeriesValues,
286
- seriesOpacities,
287
- displayMin,
288
- displayMax,
289
- displayWindow,
290
- timestamp,
291
- canvasWidth,
292
- canvasHeight,
293
- timeWindow,
294
- smoothing,
295
- adaptiveSpeedBoostSV,
296
- exaggerateSV,
297
- referenceValue,
298
- referenceValues,
299
- nonNegativeSV,
300
- maxValueSV,
301
- nowOverrideSV,
302
- windowBufferSV,
303
- pausedSV,
304
- viewEndSV: viewEnd,
305
- returnTSV: returnT,
306
- returnFromSV: returnFrom,
307
- viewWindowSV: viewWindow,
308
- liveEdgeSV: liveEdge,
309
- snapSV,
310
- extremaMinValue,
311
- extremaMaxValue,
312
- extremaMinTime,
313
- extremaMaxTime,
314
- },
381
+ frameRefs,
315
382
  scratch,
316
383
  );
317
384
  });
@@ -62,7 +62,8 @@ export interface ParticleInstance {
62
62
  * scale = r / spriteRadius
63
63
  * alpha = life * particleOpacity
64
64
  * Inactive (active <= 0.5), pre-spawn (dt < 0) and fully-faded (life <= 0)
65
- * slots are skipped.
65
+ * slots are skipped. Pass distinct `out` and `pool` arrays to retain inactive
66
+ * instance objects for later bursts while returning only the active prefix.
66
67
  */
67
68
  export function buildParticleInstances(
68
69
  buf: Float64Array,
@@ -71,9 +72,13 @@ export function buildParticleInstances(
71
72
  burstDur: number,
72
73
  particleOpacity: number,
73
74
  spriteRadius: number,
75
+ out?: ParticleInstance[],
76
+ pool?: ParticleInstance[],
74
77
  ): ParticleInstance[] {
75
78
  "worklet";
76
- const out: ParticleInstance[] = [];
79
+ const target = out ?? [];
80
+ const instances = pool ?? target;
81
+ let count = 0;
77
82
  for (let i = 0; i < slots; i++) {
78
83
  const base = i * DEGEN_STRIDE;
79
84
  if (!(buf[base + 5] > 0.5)) continue;
@@ -83,13 +88,19 @@ export function buildParticleInstances(
83
88
  if (life <= 0) continue;
84
89
  const size = buf[base + 6] || 1;
85
90
  const r = size * (0.5 + life * 0.5);
86
- out.push({
87
- x: buf[base + 0],
88
- y: buf[base + 1],
89
- scale: r / spriteRadius,
90
- alpha: life * particleOpacity,
91
- colorIndex: Math.max(0, Math.floor(buf[base + 7])),
92
- });
91
+ let instance = instances[count];
92
+ if (!instance) {
93
+ instance = { x: 0, y: 0, scale: 0, alpha: 0, colorIndex: 0 };
94
+ instances[count] = instance;
95
+ }
96
+ instance.x = buf[base + 0];
97
+ instance.y = buf[base + 1];
98
+ instance.scale = r / spriteRadius;
99
+ instance.alpha = life * particleOpacity;
100
+ instance.colorIndex = Math.max(0, Math.floor(buf[base + 7]));
101
+ target[count] = instance;
102
+ count += 1;
93
103
  }
94
- return out;
104
+ target.length = count;
105
+ return target;
95
106
  }