react-native-livechart 4.24.0 → 4.25.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 (89) hide show
  1. package/dist/components/DegenParticlesOverlay.d.ts +12 -11
  2. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  3. package/dist/components/LiveChart.d.ts.map +1 -1
  4. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  5. package/dist/components/LoadingOverlay.d.ts +5 -1
  6. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  7. package/dist/components/YAxisOverlay.d.ts +3 -1
  8. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  9. package/dist/constants.d.ts +3 -3
  10. package/dist/core/historyRangeCache.d.ts +25 -0
  11. package/dist/core/historyRangeCache.d.ts.map +1 -0
  12. package/dist/core/liveChartEngineTick.d.ts +4 -1
  13. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  14. package/dist/core/resolveConfig.d.ts +1 -1
  15. package/dist/core/useHistoryRevision.d.ts +5 -0
  16. package/dist/core/useHistoryRevision.d.ts.map +1 -0
  17. package/dist/core/useLiveChartEngine.d.ts +17 -1
  18. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  19. package/dist/core/useLiveChartSeriesEngine.d.ts +2 -0
  20. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  21. package/dist/draw/candleBodyPath.d.ts +10 -0
  22. package/dist/draw/candleBodyPath.d.ts.map +1 -0
  23. package/dist/draw/lineFillPaths.d.ts +10 -0
  24. package/dist/draw/lineFillPaths.d.ts.map +1 -0
  25. package/dist/draw/volume.d.ts +4 -1
  26. package/dist/draw/volume.d.ts.map +1 -1
  27. package/dist/draw/volumeRange.d.ts +12 -0
  28. package/dist/draw/volumeRange.d.ts.map +1 -0
  29. package/dist/hooks/useCandlePaths.d.ts +1 -1
  30. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  31. package/dist/hooks/useChartPaths.d.ts +1 -1
  32. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  33. package/dist/hooks/useDegen.d.ts +1 -0
  34. package/dist/hooks/useDegen.d.ts.map +1 -1
  35. package/dist/hooks/useDemandFrameLoop.d.ts +11 -0
  36. package/dist/hooks/useDemandFrameLoop.d.ts.map +1 -0
  37. package/dist/hooks/useLoadingLook.d.ts +12 -0
  38. package/dist/hooks/useLoadingLook.d.ts.map +1 -0
  39. package/dist/hooks/useMarkers.d.ts +3 -1
  40. package/dist/hooks/useMarkers.d.ts.map +1 -1
  41. package/dist/hooks/useMultiSeriesDegen.d.ts +1 -0
  42. package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -1
  43. package/dist/hooks/usePanScroll.d.ts +2 -0
  44. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  45. package/dist/hooks/usePinchZoom.d.ts +2 -0
  46. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  47. package/dist/hooks/useVolumeRange.d.ts +14 -0
  48. package/dist/hooks/useVolumeRange.d.ts.map +1 -0
  49. package/dist/hooks/useXAxis.d.ts +7 -0
  50. package/dist/hooks/useXAxis.d.ts.map +1 -1
  51. package/dist/hooks/useYAxis.d.ts.map +1 -1
  52. package/dist/math/spline.d.ts +3 -1
  53. package/dist/math/spline.d.ts.map +1 -1
  54. package/dist/math/squiggly.d.ts +8 -0
  55. package/dist/math/squiggly.d.ts.map +1 -1
  56. package/dist/types.d.ts +11 -2
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/components/DegenParticlesOverlay.tsx +56 -35
  60. package/src/components/LiveChart.tsx +34 -4
  61. package/src/components/LiveChartSeries.tsx +14 -6
  62. package/src/components/LoadingOverlay.tsx +53 -12
  63. package/src/components/YAxisOverlay.tsx +38 -2
  64. package/src/constants.ts +3 -3
  65. package/src/core/historyRangeCache.ts +109 -0
  66. package/src/core/liveChartEngineTick.ts +24 -57
  67. package/src/core/resolveConfig.ts +1 -1
  68. package/src/core/useHistoryRevision.ts +36 -0
  69. package/src/core/useLiveChartEngine.ts +151 -64
  70. package/src/core/useLiveChartSeriesEngine.ts +16 -1
  71. package/src/draw/candleBodyPath.ts +38 -0
  72. package/src/draw/lineFillPaths.ts +63 -0
  73. package/src/draw/volume.ts +21 -23
  74. package/src/draw/volumeRange.ts +51 -0
  75. package/src/hooks/useCandlePaths.ts +68 -94
  76. package/src/hooks/useChartPaths.ts +87 -111
  77. package/src/hooks/useDegen.ts +10 -6
  78. package/src/hooks/useDemandFrameLoop.ts +82 -0
  79. package/src/hooks/useLoadingLook.ts +30 -0
  80. package/src/hooks/useMarkers.ts +18 -4
  81. package/src/hooks/useMultiSeriesDegen.ts +7 -3
  82. package/src/hooks/usePanScroll.ts +5 -0
  83. package/src/hooks/usePinchZoom.ts +5 -0
  84. package/src/hooks/useVolumeRange.ts +40 -0
  85. package/src/hooks/useXAxis.ts +27 -3
  86. package/src/hooks/useYAxis.ts +18 -2
  87. package/src/math/spline.ts +16 -8
  88. package/src/math/squiggly.ts +12 -0
  89. package/src/types.ts +11 -2
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import { Gesture } from "react-native-gesture-handler";
3
3
  import {
4
+ useAnimatedReaction,
4
5
  useFrameCallback,
5
6
  useSharedValue,
6
7
  type SharedValue,
@@ -57,6 +58,8 @@ export function useMarkers(
57
58
  cluster: ResolvedMarkerCluster = ANCHORED_CLUSTER,
58
59
  /** Runtime gate shared with the chart engine. */
59
60
  isFrameLoopActive?: SharedValue<boolean>,
61
+ /** Experimental input-driven projection instead of continuous frames; defaults to false. */
62
+ autoSleep = false,
60
63
  ): {
61
64
  projected: SharedValue<ProjectedMarker[]>;
62
65
  tapGesture: ReturnType<typeof Gesture.Tap>;
@@ -79,7 +82,7 @@ export function useMarkers(
79
82
  onMarkerPress?.(event);
80
83
  };
81
84
 
82
- const markerFrameCallback = useFrameCallback(
85
+ const project =
83
86
  /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
84
87
  "worklet";
85
88
  if (isFrameLoopActive?.get() === false) return;
@@ -111,13 +114,24 @@ export function useMarkers(
111
114
  maxY: engine.canvasHeight.get(),
112
115
  });
113
116
  projected.set(buf);
117
+ };
118
+ const legacyActive = autostart && !autoSleep;
119
+ const markerFrameCallback = useFrameCallback(project, legacyActive);
120
+ useAnimatedReaction(
121
+ () => {
122
+ if (!autoSleep || !autostart) return null;
123
+ return [active, markers.get(), engine.canvasWidth.get(), engine.canvasHeight.get(),
124
+ engine.timestamp.get(), engine.displayWindow.get(), engine.displayMin.get(),
125
+ engine.displayMax.get(), seriesSV?.get(), lineData?.get(), isFrameLoopActive?.get()];
126
+ },
127
+ (inputs) => {
128
+ if (inputs !== null) project();
114
129
  },
115
- autostart,
116
130
  );
117
131
  useEffect(() => {
118
132
  // Reanimated does not reactively apply later `autostart` values.
119
- markerFrameCallback.setActive(autostart);
120
- }, [autostart, markerFrameCallback]);
133
+ markerFrameCallback.setActive(legacyActive);
134
+ }, [legacyActive, markerFrameCallback]);
121
135
 
122
136
  const tapGesture = Gesture.Tap().onEnd(
123
137
  /* istanbul ignore next -- gesture worklet runs on UI thread, not in Jest */ (
@@ -30,6 +30,7 @@ export function useMultiSeriesDegen(
30
30
  ): {
31
31
  pack: SharedValue<Float64Array<ArrayBuffer>>;
32
32
  packRevision: SharedValue<number>;
33
+ particleTimestamp: SharedValue<number>;
33
34
  shakeTransform: DerivedValue<
34
35
  [{ translateX: number }, { translateY: number }]
35
36
  >;
@@ -37,6 +38,7 @@ export function useMultiSeriesDegen(
37
38
  const MAX_SLOTS = 80;
38
39
  const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
39
40
  const packRevision = useSharedValue(0);
41
+ const particleTimestamp = useSharedValue(0);
40
42
  // Per-series previous momentum (0 = flat, 1 = up, 2 = down), index-aligned to series.
41
43
  const prevMoms = useSharedValue<number[]>([]);
42
44
  const writeRot = useSharedValue(0);
@@ -131,9 +133,10 @@ export function useMultiSeriesDegen(
131
133
  ]);
132
134
 
133
135
  useFrameCallback(
134
- /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
136
+ /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (frame) => {
135
137
  "worklet";
136
- const now = engine.timestamp.get();
138
+ // Effect timing is independent of the quantized/pinned viewport clock.
139
+ const now = frame.timestamp / 1000;
137
140
  const buf = pack.get();
138
141
  const slots = slotCountSV.get();
139
142
 
@@ -242,6 +245,7 @@ export function useMultiSeriesDegen(
242
245
  );
243
246
 
244
247
  if (activeCount > 0 || prevActiveCount.get() > 0) {
248
+ particleTimestamp.set(now);
245
249
  packRevision.set(packRevision.get() + 1);
246
250
  }
247
251
  prevActiveCount.set(activeCount);
@@ -256,5 +260,5 @@ export function useMultiSeriesDegen(
256
260
  ];
257
261
  });
258
262
 
259
- return { pack, packRevision, shakeTransform };
263
+ return { pack, packRevision, particleTimestamp, shakeTransform };
260
264
  }
@@ -22,6 +22,8 @@ export type PanScrollGestureMode = "holdToScrub" | "axisDrag";
22
22
 
23
23
  /** Engine SharedValues the pan-scroll gesture reads/writes (subset of the engine state). */
24
24
  export interface PanScrollEngineRefs {
25
+ /** Wake a demand-driven engine before reading the gesture bounds. */
26
+ wake?: () => void;
25
27
  /** Absolute right-edge time to freeze at, or `null` to follow the live edge. */
26
28
  viewEnd: SharedValue<number | null>;
27
29
  /** Right-edge time the engine would use if following live (advances each frame). */
@@ -263,10 +265,13 @@ export function usePanScroll({
263
265
  },
264
266
  );
265
267
 
268
+ const wakeEngine = engine.wake;
269
+
266
270
  const onStart =
267
271
  /* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
268
272
  () => {
269
273
  "worklet";
274
+ wakeEngine?.();
270
275
  // Stop an in-flight fling FIRST. `withDecay` keeps writing `viewEnd` after
271
276
  // the finger lifts, and `Gesture.Race` declares no relation between the two
272
277
  // pans, so this can run while a scrub is already engaged. Bailing out below
@@ -10,6 +10,8 @@ import { FOLLOW_SNAP, panLowerBound, panUpperBound } from "./usePanScroll";
10
10
 
11
11
  /** Engine SharedValues the pinch-zoom gesture reads/writes. */
12
12
  export interface PinchZoomEngineRefs {
13
+ /** Wake a demand-driven engine before reading the gesture bounds. */
14
+ wake?: () => void;
13
15
  /** Visible-window width override in seconds, or `null` to follow `timeWindow`. */
14
16
  viewWindow: SharedValue<number | null>;
15
17
  /** Absolute right-edge time, or `null` to follow the live edge (shared with pan). */
@@ -170,10 +172,13 @@ export function usePinchZoom({
170
172
  const startWindow = useSharedValue(timeWindow);
171
173
  const startViewEnd = useSharedValue(0);
172
174
 
175
+ const wakeEngine = engine.wake;
176
+
173
177
  const onStart =
174
178
  /* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
175
179
  () => {
176
180
  "worklet";
181
+ wakeEngine?.();
177
182
  cancelAnimation(viewEnd);
178
183
  cancelAnimation(viewWindow);
179
184
  const edge = liveEdge.get();
@@ -0,0 +1,40 @@
1
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+ import type { CandlePoint } from "../types";
3
+ import {
4
+ visibleMaxVolume,
5
+ volumeEndIndex,
6
+ volumeStartIndex,
7
+ } from "../draw/volumeRange";
8
+
9
+ /**
10
+ * Primitive index outputs suppress downstream scans while the window glides
11
+ * within the same buckets. The maximum also depends directly on candles, so
12
+ * every notified data revision (including same-length replacement or modify)
13
+ * invalidates it. Array length and object identity are never cache keys.
14
+ */
15
+ export function useVolumeRange(
16
+ candles: SharedValue<CandlePoint[]> | undefined,
17
+ timestamp: SharedValue<number>,
18
+ window: SharedValue<number>,
19
+ width: SharedValue<number>,
20
+ active: boolean,
21
+ ) {
22
+ const start = useDerivedValue(() =>
23
+ !active || !candles
24
+ ? 0
25
+ : volumeStartIndex(
26
+ candles.get(),
27
+ timestamp.get() - window.get(),
28
+ width.get(),
29
+ ),
30
+ );
31
+ const end = useDerivedValue(() =>
32
+ !active || !candles ? 0 : volumeEndIndex(candles.get(), timestamp.get()),
33
+ );
34
+ const max = useDerivedValue(() =>
35
+ !active || !candles
36
+ ? 0
37
+ : visibleMaxVolume(candles.get(), start.get(), end.get()),
38
+ );
39
+ return { start, end, max };
40
+ }
@@ -167,11 +167,16 @@ export function useXAxis(
167
167
  const edgeAlpha =
168
168
  fromEdge >= fadeZone ? 1 : fromEdge <= 0 ? 0 : fromEdge / fadeZone;
169
169
 
170
- const target = xAxisKeyIsTarget(key, targetKeys) ? edgeAlpha : 0;
170
+ const isTarget = xAxisKeyIsTarget(key, targetKeys);
171
+ const target = isTarget ? edgeAlpha : 0;
171
172
  let next = lerp(label.alpha, target, FADE, MS_PER_FRAME_60FPS);
172
173
  if (Math.abs(next - target) < 0.02) next = target;
173
174
 
174
- if (next < 0.01 && target === 0) {
175
+ // Only a key that left the target set is deleted once it has faded out. A
176
+ // target key can sit at alpha 0 (the set spans one interval past each edge,
177
+ // outside the plot); deleting it would re-create it on the next run and
178
+ // write `labelAlphas`, this mapper's own input, every frame.
179
+ if (next < 0.01 && !isTarget) {
175
180
  delete alphas[key];
176
181
  cacheChanged = true;
177
182
  } else {
@@ -182,7 +187,12 @@ export function useXAxis(
182
187
  }
183
188
  }
184
189
 
185
- if (cacheChanged) labelAlphas.set(alphas);
190
+ // Publish only a changed final cache so this derived value cannot keep
191
+ // waking itself after the engine sleeps. Off-screen target keys are retained
192
+ // above; this equality check also guards against other no-op cache updates.
193
+ if (cacheChanged && !sameXAxisLabels(previousAlphas, alphas)) {
194
+ labelAlphas.set(alphas);
195
+ }
186
196
 
187
197
  // Collect visible labels
188
198
  const raw: XAxisEntry[] = [];
@@ -226,3 +236,17 @@ export function useXAxis(
226
236
 
227
237
  return { xAxisEntries, font };
228
238
  }
239
+
240
+ export function sameXAxisLabels(
241
+ previous: Record<number, { alpha: number; text: string }>,
242
+ next: Record<number, { alpha: number; text: string }>,
243
+ ): boolean {
244
+ "worklet";
245
+ const keys = Object.keys(next);
246
+ if (keys.length !== Object.keys(previous).length) return false;
247
+ for (let i = 0; i < keys.length; i++) {
248
+ const key = Number(keys[i]);
249
+ if (!previous[key] || previous[key].alpha !== next[key].alpha || previous[key].text !== next[key].text) return false;
250
+ }
251
+ return true;
252
+ }
@@ -28,7 +28,15 @@ export function useYAxis(
28
28
  const yAxisEntries = useDerivedValue(() => {
29
29
  const dt = MS_PER_FRAME_60FPS;
30
30
 
31
- const alphas = labelAlphas.get();
31
+ // SharedValue payloads may be frozen after crossing the JS/UI boundary.
32
+ // Copy before computeGridEntries adds, updates, or removes label keys.
33
+ const previousAlphas = labelAlphas.get();
34
+ const alphas: Record<number, number> = {};
35
+ const previousKeys = Object.keys(previousAlphas);
36
+ for (let i = 0; i < previousKeys.length; i++) {
37
+ const key = Number(previousKeys[i]);
38
+ alphas[key] = previousAlphas[key];
39
+ }
32
40
  const result = computeGridEntries(
33
41
  engine.displayMin.get(),
34
42
  engine.displayMax.get(),
@@ -46,7 +54,15 @@ export function useYAxis(
46
54
  );
47
55
 
48
56
  prevInterval.set(result.interval);
49
- labelAlphas.set(alphas);
57
+ // This derived value also reads the cache. Only publish actual changes so
58
+ // a settled axis doesn't keep scheduling itself with fresh object identities.
59
+ const nextKeys = Object.keys(alphas);
60
+ let cacheChanged = nextKeys.length !== previousKeys.length;
61
+ for (let i = 0; i < nextKeys.length && !cacheChanged; i++) {
62
+ const key = Number(nextKeys[i]);
63
+ if (alphas[key] !== previousAlphas[key]) cacheChanged = true;
64
+ }
65
+ if (cacheChanged) labelAlphas.set(alphas);
50
66
 
51
67
  return result.entries;
52
68
  });
@@ -60,6 +60,9 @@ export function drawSpline(
60
60
  startPoint = 0,
61
61
  /** Last point index to draw (exclusive). Defaults to the full array. */
62
62
  endPoint = pts.length >> 1,
63
+ /** Optional paired sinks receive identical verbs from the same curve math. */
64
+ second?: SplinePathSink,
65
+ third?: SplinePathSink,
63
66
  ) {
64
67
  "worklet";
65
68
  const start = Math.max(0, startPoint);
@@ -70,12 +73,16 @@ export function drawSpline(
70
73
  for (let i = 1; i < n; i++) {
71
74
  const point = (start + i) * 2;
72
75
  path.lineTo(pts[point], pts[point + 1]);
76
+ second?.lineTo(pts[point], pts[point + 1]);
77
+ third?.lineTo(pts[point], pts[point + 1]);
73
78
  }
74
79
  return;
75
80
  }
76
81
  if (n === 2) {
77
82
  const point = (start + 1) * 2;
78
83
  path.lineTo(pts[point], pts[point + 1]);
84
+ second?.lineTo(pts[point], pts[point + 1]);
85
+ third?.lineTo(pts[point], pts[point + 1]);
79
86
  return;
80
87
  }
81
88
 
@@ -123,14 +130,15 @@ export function drawSpline(
123
130
  const i2 = (start + i) * 2;
124
131
  const j2 = i2 + 2;
125
132
  const hi = h[i];
126
- path.cubicTo(
127
- pts[i2] + hi / 3,
128
- pts[i2 + 1] + (m[i] * hi) / 3,
129
- pts[j2] - hi / 3,
130
- pts[j2 + 1] - (m[i + 1] * hi) / 3,
131
- pts[j2],
132
- pts[j2 + 1],
133
- );
133
+ const x1 = pts[i2] + hi / 3;
134
+ const y1 = pts[i2 + 1] + (m[i] * hi) / 3;
135
+ const x2 = pts[j2] - hi / 3;
136
+ const y2 = pts[j2 + 1] - (m[i + 1] * hi) / 3;
137
+ const x = pts[j2],
138
+ y = pts[j2 + 1];
139
+ path.cubicTo(x1, y1, x2, y2, x, y);
140
+ second?.cubicTo(x1, y1, x2, y2, x, y);
141
+ third?.cubicTo(x1, y1, x2, y2, x, y);
134
142
  }
135
143
  }
136
144
 
@@ -27,6 +27,18 @@ export function squigglyYAt(
27
27
  );
28
28
  }
29
29
 
30
+ /**
31
+ * The squiggle's clock, in seconds: wall time. The loading shell
32
+ * (`LoadingOverlay`) and the reveal's morph (`useChartPaths`) both phase their
33
+ * wave on it rather than on `engine.timestamp`, which a `nowOverride` freezes
34
+ * between data updates (freezing the loading line with it). Sharing one clock
35
+ * keeps the wave continuous where the shell hands over to the morph.
36
+ */
37
+ export function squiggleClockSeconds(): number {
38
+ "worklet";
39
+ return Date.now() / 1000;
40
+ }
41
+
30
42
  /**
31
43
  * Build a flat [x0,y0,x1,y1,...] point array for a standalone squiggly line
32
44
  * spanning the full chart area (used in loading / empty state).
package/src/types.ts CHANGED
@@ -757,8 +757,8 @@ export interface YAxisConfig {
757
757
  * numbers; they track the live range each frame.
758
758
  *
759
759
  * `minGap` still acts as a floor: if `count` labels won't fit at least
760
- * `minGap` px apart, the count is reduced to what fits. Clamped to at most 15
761
- * (the label pool size). Omit (or `0`) for the default dynamic grid.
760
+ * `minGap` px apart, the count is reduced to what fits. Clamped to at most 15.
761
+ * Omit (or `0`) for the default dynamic grid.
762
762
  */
763
763
  count?: number;
764
764
  /**
@@ -2704,6 +2704,15 @@ export interface LiveChartProps extends LiveChartCoreProps {
2704
2704
  * @experimental
2705
2705
  */
2706
2706
  isFrameLoopActive?: SharedValue<boolean>;
2707
+ /**
2708
+ * Experimental single-series demand scheduling. Stop engine frames after a
2709
+ * paused, parked, or fixed-time chart settles; shared input changes and
2710
+ * gestures wake it on the UI thread. Default false. Live scrolling, pulse,
2711
+ * loading/reveal, and enabled degen/trade effects keep the engine awake.
2712
+ * Does not cache a bitmap or disable gestures. `static` takes precedence.
2713
+ * @experimental
2714
+ */
2715
+ autoSleep?: boolean;
2707
2716
  /**
2708
2717
  * Optional development counter for engine frames that did or did not change
2709
2718
  * tracked engine values. It is not a count of Skia redraws. Supplying it adds