react-native-livechart 4.24.1 → 4.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/components/DegenParticlesOverlay.d.ts +2 -2
  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 +6 -2
  13. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  14. package/dist/core/liveChartSeriesEngineTick.d.ts +2 -1
  15. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  16. package/dist/core/resolveConfig.d.ts +1 -1
  17. package/dist/core/useHistoryRevision.d.ts +5 -0
  18. package/dist/core/useHistoryRevision.d.ts.map +1 -0
  19. package/dist/core/useLiveChartEngine.d.ts +20 -2
  20. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  21. package/dist/core/useLiveChartSeriesEngine.d.ts +5 -1
  22. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  23. package/dist/draw/candleBodyPath.d.ts +10 -0
  24. package/dist/draw/candleBodyPath.d.ts.map +1 -0
  25. package/dist/draw/lineFillPaths.d.ts +10 -0
  26. package/dist/draw/lineFillPaths.d.ts.map +1 -0
  27. package/dist/draw/volume.d.ts +4 -1
  28. package/dist/draw/volume.d.ts.map +1 -1
  29. package/dist/draw/volumeRange.d.ts +12 -0
  30. package/dist/draw/volumeRange.d.ts.map +1 -0
  31. package/dist/hooks/crosshairShared.d.ts +8 -0
  32. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  33. package/dist/hooks/useCandlePaths.d.ts +1 -1
  34. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  35. package/dist/hooks/useChartPaths.d.ts +1 -1
  36. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  37. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  38. package/dist/hooks/useDegen.d.ts +1 -0
  39. package/dist/hooks/useDegen.d.ts.map +1 -1
  40. package/dist/hooks/useDemandFrameLoop.d.ts +11 -0
  41. package/dist/hooks/useDemandFrameLoop.d.ts.map +1 -0
  42. package/dist/hooks/useLoadingLook.d.ts +12 -0
  43. package/dist/hooks/useLoadingLook.d.ts.map +1 -0
  44. package/dist/hooks/useMarkers.d.ts +3 -1
  45. package/dist/hooks/useMarkers.d.ts.map +1 -1
  46. package/dist/hooks/useMultiSeriesDegen.d.ts +1 -0
  47. package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -1
  48. package/dist/hooks/usePanScroll.d.ts +2 -0
  49. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  50. package/dist/hooks/usePinchZoom.d.ts +2 -0
  51. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  52. package/dist/hooks/useReferenceDrag.d.ts +3 -2
  53. package/dist/hooks/useReferenceDrag.d.ts.map +1 -1
  54. package/dist/hooks/useReferenceLineGrouping.d.ts +19 -0
  55. package/dist/hooks/useReferenceLineGrouping.d.ts.map +1 -0
  56. package/dist/hooks/useVolumeRange.d.ts +14 -0
  57. package/dist/hooks/useVolumeRange.d.ts.map +1 -0
  58. package/dist/hooks/useXAxis.d.ts +7 -0
  59. package/dist/hooks/useXAxis.d.ts.map +1 -1
  60. package/dist/hooks/useYAxis.d.ts.map +1 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/math/rangeAnimation.d.ts +4 -0
  64. package/dist/math/rangeAnimation.d.ts.map +1 -0
  65. package/dist/math/referenceDrag.d.ts +17 -5
  66. package/dist/math/referenceDrag.d.ts.map +1 -1
  67. package/dist/math/referenceGroup.d.ts +2 -2
  68. package/dist/math/spline.d.ts +3 -1
  69. package/dist/math/spline.d.ts.map +1 -1
  70. package/dist/math/squiggly.d.ts +8 -0
  71. package/dist/math/squiggly.d.ts.map +1 -1
  72. package/dist/types.d.ts +52 -3
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/src/components/DegenParticlesOverlay.tsx +5 -5
  76. package/src/components/LiveChart.tsx +37 -66
  77. package/src/components/LiveChartSeries.tsx +18 -6
  78. package/src/components/LoadingOverlay.tsx +53 -12
  79. package/src/components/YAxisOverlay.tsx +38 -2
  80. package/src/constants.ts +3 -3
  81. package/src/core/historyRangeCache.ts +109 -0
  82. package/src/core/liveChartEngineTick.ts +41 -63
  83. package/src/core/liveChartSeriesEngineTick.ts +18 -5
  84. package/src/core/resolveConfig.ts +1 -1
  85. package/src/core/useHistoryRevision.ts +36 -0
  86. package/src/core/useLiveChartEngine.ts +159 -64
  87. package/src/core/useLiveChartSeriesEngine.ts +22 -2
  88. package/src/draw/candleBodyPath.ts +38 -0
  89. package/src/draw/lineFillPaths.ts +63 -0
  90. package/src/draw/volume.ts +21 -23
  91. package/src/draw/volumeRange.ts +51 -0
  92. package/src/hooks/crosshairShared.ts +29 -0
  93. package/src/hooks/useCandlePaths.ts +68 -94
  94. package/src/hooks/useChartPaths.ts +87 -111
  95. package/src/hooks/useCrosshair.ts +15 -3
  96. package/src/hooks/useDegen.ts +10 -6
  97. package/src/hooks/useDemandFrameLoop.ts +82 -0
  98. package/src/hooks/useLoadingLook.ts +30 -0
  99. package/src/hooks/useMarkers.ts +18 -4
  100. package/src/hooks/useMultiSeriesDegen.ts +7 -3
  101. package/src/hooks/usePanScroll.ts +5 -0
  102. package/src/hooks/usePinchZoom.ts +5 -0
  103. package/src/hooks/useReferenceDrag.ts +28 -14
  104. package/src/hooks/useReferenceLineGrouping.ts +72 -0
  105. package/src/hooks/useVolumeRange.ts +40 -0
  106. package/src/hooks/useXAxis.ts +27 -3
  107. package/src/hooks/useYAxis.ts +18 -2
  108. package/src/index.ts +1 -0
  109. package/src/math/rangeAnimation.ts +28 -0
  110. package/src/math/referenceDrag.ts +21 -4
  111. package/src/math/referenceGroup.ts +2 -2
  112. package/src/math/spline.ts +16 -8
  113. package/src/math/squiggly.ts +12 -0
  114. package/src/types.ts +53 -3
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @see https://github.com/benjitaylor/liveline
7
7
  */
8
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
8
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
9
9
  import {
10
10
  cancelAnimation,
11
11
  Easing,
@@ -16,12 +16,14 @@ import {
16
16
  withTiming,
17
17
  type SharedValue,
18
18
  } from "react-native-reanimated";
19
+ import { useDemandFrameLoop } from "../hooks/useDemandFrameLoop";
19
20
  import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
20
21
  import type {
21
22
  CandleGap,
22
23
  CandlePoint,
23
24
  LiveChartFrameStats,
24
25
  LiveChartPoint,
26
+ RangeAnimationConfig,
25
27
  SeriesConfig,
26
28
  } from "../types";
27
29
  import {
@@ -30,11 +32,23 @@ import {
30
32
  type EngineTickMutable,
31
33
  } from "./liveChartEngineTick";
32
34
 
35
+ import {
36
+ makeHistoryRangeCache,
37
+ type HistoryRangeCache,
38
+ type HistoryRevision,
39
+ } from "./historyRangeCache";
40
+
41
+ import { useHistoryRevision } from "./useHistoryRevision";
42
+
33
43
  export interface EngineConfig {
44
+ /** Original inputs before a reveal/stash bridge that may suppress same-array notifications. */
45
+ dataChangeSource?: SharedValue<LiveChartPoint[]>;
46
+ candlesChangeSource?: SharedValue<CandlePoint[]>;
34
47
  data: SharedValue<LiveChartPoint[]>;
35
48
  value: SharedValue<number>;
36
49
  timeWindow: number;
37
50
  smoothing: number;
51
+ rangeAnimation?: RangeAnimationConfig;
38
52
  /** Extra catch-up speed added to `smoothing` when the live value lags. */
39
53
  adaptiveSpeedBoost?: number;
40
54
  exaggerate?: boolean;
@@ -96,6 +110,12 @@ export interface EngineConfig {
96
110
  static?: boolean;
97
111
  /** Runtime gate for the frame loop. Gestures and configuration stay mounted. */
98
112
  isFrameLoopActive?: SharedValue<boolean>;
113
+ /** Experimental automatic idle scheduling, single-series only. */
114
+ autoSleep?: boolean;
115
+ /** Effects that require continuous engine work. */
116
+ keepAwake?: SharedValue<boolean>;
117
+ /** Appearance/reveal changes that should wake a settled engine. */
118
+ wakeSignal?: SharedValue<unknown>;
99
119
  /** Optional development-only frame publication counters. */
100
120
  debugFrameStats?: SharedValue<LiveChartFrameStats>;
101
121
  /**
@@ -185,16 +205,21 @@ export interface ChartEngineEdge {
185
205
  edgeValue: SharedValue<number>;
186
206
  }
187
207
 
188
- export interface SingleEngineState extends ChartEngineLayout, ChartEngineExtrema {
208
+ export interface SingleEngineState
209
+ extends ChartEngineLayout,
210
+ ChartEngineExtrema {
189
211
  data: SharedValue<LiveChartPoint[]>;
190
212
  value: SharedValue<number>;
191
213
  displayValue: SharedValue<number>;
192
214
  }
193
215
 
194
- export interface MultiEngineState extends ChartEngineLayout, ChartEngineExtrema {
216
+ export interface MultiEngineState
217
+ extends ChartEngineLayout,
218
+ ChartEngineExtrema {
195
219
  data: SharedValue<LiveChartPoint[]>;
196
220
  value: SharedValue<number>;
197
221
  displayValue: SharedValue<number>;
222
+ /** Entries captured by the last measured engine frame; history arrays remain shared. */
198
223
  series: SharedValue<SeriesConfig[]>;
199
224
  displaySeriesValues: SharedValue<number[]>;
200
225
  seriesOpacities: SharedValue<number[]>;
@@ -208,6 +233,8 @@ export type ChartEngineWithLiveValue = ChartEngineLayout & {
208
233
  };
209
234
 
210
235
  export interface EngineFrameRefs {
236
+ pointHistory?: SharedValue<HistoryRevision<LiveChartPoint> | undefined>;
237
+ candleHistory?: SharedValue<HistoryRevision<CandlePoint> | undefined>;
211
238
  data: SharedValue<LiveChartPoint[]>;
212
239
  value: SharedValue<number>;
213
240
  displayValue: SharedValue<number>;
@@ -219,6 +246,7 @@ export interface EngineFrameRefs {
219
246
  timestamp: SharedValue<number>;
220
247
  timeWindow: SharedValue<number>;
221
248
  smoothing: SharedValue<number>;
249
+ rangeAnimationSV?: SharedValue<RangeAnimationConfig | undefined>;
222
250
  adaptiveSpeedBoostSV?: SharedValue<number | undefined>;
223
251
  exaggerateSV: SharedValue<boolean>;
224
252
  referenceValue: SharedValue<number | undefined>;
@@ -268,6 +296,7 @@ export interface EngineFrameRefs {
268
296
 
269
297
  /** Stable state/input containers reused by the UI-thread frame callback. */
270
298
  export interface EngineFrameScratch {
299
+ historyCache: HistoryRangeCache;
271
300
  state: EngineTickMutable;
272
301
  input: EngineTickInput;
273
302
  }
@@ -275,6 +304,7 @@ export interface EngineFrameScratch {
275
304
  /** Allocate one single-series frame scratch. Call once per engine instance. */
276
305
  export function makeEngineFrameScratch(): EngineFrameScratch {
277
306
  return {
307
+ historyCache: makeHistoryRangeCache(),
278
308
  state: {
279
309
  displayValue: 0,
280
310
  displayMin: 0,
@@ -361,6 +391,7 @@ export function applyLiveChartEngineFrame(
361
391
  input.canvasHeight = sv.canvasHeight.value;
362
392
  input.timeWindow = sv.timeWindow.value;
363
393
  input.smoothing = sv.smoothing.value;
394
+ input.rangeAnimation = sv.rangeAnimationSV?.value;
364
395
  input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
365
396
  input.exaggerate = sv.exaggerateSV.value;
366
397
  input.referenceValue = sv.referenceValue.value;
@@ -375,7 +406,9 @@ export function applyLiveChartEngineFrame(
375
406
  input.nowOverride = sv.nowOverrideSV?.value;
376
407
  input.windowBuffer = sv.windowBufferSV?.value ?? 0;
377
408
  input.targetValue = sv.value.value;
378
- input.points = sv.data.value;
409
+ const pointHistory = sv.pointHistory?.value;
410
+ const candleHistory = sv.candleHistory?.value;
411
+ input.points = pointHistory?.data ?? sv.data.value;
379
412
  input.nowSeconds = Date.now() / 1000;
380
413
  input.paused = sv.pausedSV.value;
381
414
  input.snap = snap;
@@ -385,15 +418,16 @@ export function applyLiveChartEngineFrame(
385
418
  input.returnFrom = sv.returnFromSV?.value;
386
419
  input.viewWindow = sv.viewWindowSV?.value;
387
420
  input.mode = sv.modeSV.value;
388
- input.candles = sv.candles?.value;
421
+ input.candles = candleHistory?.data ?? sv.candles?.value;
422
+ input.historyRevision =
423
+ input.mode === "candle" ? candleHistory : pointHistory;
389
424
  input.liveCandle = sv.liveCandle?.value;
390
425
  input.candleGaps = sv.candleGapsSV?.value;
391
- input.candleGapBridgeNoTrades =
392
- sv.candleGapBridgeNoTradesSV?.value ?? false;
426
+ input.candleGapBridgeNoTrades = sv.candleGapBridgeNoTradesSV?.value ?? false;
393
427
  input.candleGapBridgeUnavailable =
394
428
  sv.candleGapBridgeUnavailableSV?.value ?? false;
395
429
  input.candleGapBridgeUnknown = sv.candleGapBridgeUnknownSV?.value ?? false;
396
- tickLiveChartEngineFrame(state, input);
430
+ tickLiveChartEngineFrame(state, input, scratch?.historyCache);
397
431
  sv.displayValue.value = state.displayValue;
398
432
  sv.displayMin.value = state.displayMin;
399
433
  sv.displayMax.value = state.displayMax;
@@ -409,19 +443,26 @@ export function applyLiveChartEngineFrame(
409
443
  // once a real (measured) frame consumed it. The tick early-returns on a
410
444
  // zero-size canvas (before applying the snap), so keep the flag pending until
411
445
  // the canvas has laid out, or a snap arriving pre-layout would be dropped.
412
- if (snap && sv.snapSV && sv.canvasWidth.value !== 0 && sv.canvasHeight.value !== 0) {
446
+ if (
447
+ snap &&
448
+ sv.snapSV &&
449
+ sv.canvasWidth.value !== 0 &&
450
+ sv.canvasHeight.value !== 0
451
+ ) {
413
452
  sv.snapSV.value = false;
414
453
  }
415
454
  }
416
455
 
417
456
  export function useLiveChartEngine(
418
457
  config: EngineConfig,
419
- ): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
458
+ ): SingleEngineState & ChartEngineScroll & ChartEngineEdge & { wake: () => void } {
420
459
  const {
421
460
  data,
461
+ dataChangeSource = data,
422
462
  value,
423
463
  timeWindow: configuredTimeWindow,
424
464
  smoothing: configuredSmoothing,
465
+ rangeAnimation,
425
466
  adaptiveSpeedBoost,
426
467
  exaggerate,
427
468
  referenceValue: configuredReferenceValue,
@@ -442,9 +483,13 @@ export function useLiveChartEngine(
442
483
  static: isStatic,
443
484
  isFrameLoopActive,
444
485
  debugFrameStats,
486
+ autoSleep = false,
487
+ keepAwake,
488
+ wakeSignal,
445
489
  snapKey,
446
490
  mode,
447
491
  candles,
492
+ candlesChangeSource = candles,
448
493
  liveCandle,
449
494
  candleGaps,
450
495
  candleGapBridgeNoTrades,
@@ -472,9 +517,8 @@ export function useLiveChartEngine(
472
517
  );
473
518
  // Static charts snap to their target in one tick (smoothing=1), so the single
474
519
  // settle reaction below produces the final state with no per-frame easing.
475
- const smoothing = useDerivedValue(() =>
476
- isStatic ? 1 : configuredSmoothing,
477
- );
520
+ const smoothing = useDerivedValue(() => (isStatic ? 1 : configuredSmoothing));
521
+ const rangeAnimationSV = useDerivedValue(() => rangeAnimation);
478
522
  const adaptiveSpeedBoostSV = useDerivedValue(() => adaptiveSpeedBoost);
479
523
  const exaggerateSV = useDerivedValue(() => exaggerate ?? false);
480
524
  const referenceValue = useDerivedValue(() => configuredReferenceValue);
@@ -597,8 +641,14 @@ export function useLiveChartEngine(
597
641
  // React renders: Reanimated uses captured closure values as implicit reaction
598
642
  // dependencies, and rebuilding this object would unregister/re-register the
599
643
  // static settle mapper behind its path consumers. See #329.
644
+ // Observe original feeds as well as reveal bridges: same-array assignments
645
+ // may suppress bridge notifications. Live candle changes stay uncached.
646
+ const pointHistory = useHistoryRevision(data, dataChangeSource);
647
+ const candleHistory = useHistoryRevision(candles, candlesChangeSource);
600
648
  const frameRefs = useMemo<EngineFrameRefs>(
601
649
  () => ({
650
+ pointHistory,
651
+ candleHistory,
602
652
  data,
603
653
  value,
604
654
  displayValue,
@@ -610,6 +660,7 @@ export function useLiveChartEngine(
610
660
  canvasHeight,
611
661
  timeWindow,
612
662
  smoothing,
663
+ rangeAnimationSV,
613
664
  adaptiveSpeedBoostSV,
614
665
  exaggerateSV,
615
666
  referenceValue,
@@ -644,6 +695,8 @@ export function useLiveChartEngine(
644
695
  extremaMaxTime,
645
696
  }),
646
697
  [
698
+ pointHistory,
699
+ candleHistory,
647
700
  adaptiveSpeedBoostSV,
648
701
  allowFutureViewEndSV,
649
702
  candleGapBridgeNoTradesSV,
@@ -676,6 +729,7 @@ export function useLiveChartEngine(
676
729
  returnFrom,
677
730
  returnT,
678
731
  smoothing,
732
+ rangeAnimationSV,
679
733
  snapSV,
680
734
  thresholdRangeExtendToNow,
681
735
  thresholdRangeExtendToStart,
@@ -696,41 +750,78 @@ export function useLiveChartEngine(
696
750
  frameScratchRef.current = makeEngineFrameScratch();
697
751
  }
698
752
 
699
- // `autostart=false` registers the frame callback without running it — the live
700
- // loop is fully inert in static mode (the invariant that makes this worth it).
701
- const engineFrameCallback = useFrameCallback((frameInfo) => {
753
+ const tickFrame = useCallback((dt: number): boolean => {
702
754
  "worklet";
703
- if (isFrameLoopActive?.get() === false) return;
704
- const beforeValue = debugFrameStats ? displayValue.get() : 0;
705
- const beforeMin = debugFrameStats ? displayMin.get() : 0;
706
- const beforeMax = debugFrameStats ? displayMax.get() : 0;
707
- const beforeWindow = debugFrameStats ? displayWindow.get() : 0;
708
- const beforeTimestamp = debugFrameStats ? timestamp.get() : 0;
709
- const beforeLiveEdge = debugFrameStats ? liveEdge.get() : 0;
710
- const beforeEdgeValue = debugFrameStats ? edgeValue.get() : 0;
711
- applyLiveChartEngineFrame(frameInfo, frameRefs, frameScratchRef.current!);
755
+ if (isFrameLoopActive?.get() === false) return false;
756
+ if (!autoSleep && !debugFrameStats) {
757
+ applyLiveChartEngineFrame({ timeSincePreviousFrame: dt }, frameRefs, frameScratchRef.current!);
758
+ return true;
759
+ }
760
+ const beforeValue = displayValue.get();
761
+ const beforeMin = displayMin.get();
762
+ const beforeMax = displayMax.get();
763
+ const beforeWindow = displayWindow.get();
764
+ const beforeTimestamp = timestamp.get();
765
+ const beforeLiveEdge = liveEdge.get();
766
+ const beforeEdgeValue = edgeValue.get();
767
+ applyLiveChartEngineFrame({ timeSincePreviousFrame: dt }, frameRefs, frameScratchRef.current!);
768
+ const changed = beforeValue !== displayValue.get() || beforeMin !== displayMin.get() ||
769
+ beforeMax !== displayMax.get() || beforeWindow !== displayWindow.get() ||
770
+ beforeTimestamp !== timestamp.get() || beforeEdgeValue !== edgeValue.get();
712
771
  if (debugFrameStats) {
713
- const didPublish =
714
- beforeValue !== displayValue.get() ||
715
- beforeMin !== displayMin.get() ||
716
- beforeMax !== displayMax.get() ||
717
- beforeWindow !== displayWindow.get() ||
718
- beforeTimestamp !== timestamp.get() ||
719
- beforeLiveEdge !== liveEdge.get() ||
720
- beforeEdgeValue !== edgeValue.get();
772
+ const didPublish = changed || beforeLiveEdge !== liveEdge.get();
721
773
  const previous = debugFrameStats.get();
722
- debugFrameStats.set({
723
- frames: previous.frames + 1,
724
- published: previous.published + (didPublish ? 1 : 0),
725
- skipped: previous.skipped + (didPublish ? 0 : 1),
726
- });
774
+ debugFrameStats.set({ frames: previous.frames + 1, published: previous.published + (didPublish ? 1 : 0), skipped: previous.skipped + (didPublish ? 0 : 1) });
775
+ }
776
+ // The liveEdge clock alone is not a visible animation while parked/paused.
777
+ // Do not sleep if a stranded viewEnd actually fell back to following live.
778
+ const parked = viewEnd.get() !== null && timestamp.get() === viewEnd.get();
779
+ const clockMoves = nowOverrideSV.get() === undefined && !pausedSV.get() && !parked;
780
+ return changed || clockMoves || keepAwake?.get() === true || returnT.get() < 1;
781
+ }, [autoSleep, isFrameLoopActive, displayValue, displayMin, displayMax, displayWindow, timestamp,
782
+ liveEdge, edgeValue, frameRefs, frameScratchRef, debugFrameStats, viewEnd, nowOverrideSV, pausedSV, keepAwake, returnT]);
783
+
784
+ const prepareWake = useCallback(() => {
785
+ "worklet";
786
+ // Read the arrays themselves, not a length/endpoints fingerprint: modify()
787
+ // can change an interior value without changing any of those quantities.
788
+ // History revisions also notify when a reveal bridge suppresses a same-array
789
+ // source update. A sleeping engine must wake to consume that invalidation.
790
+ return [pointHistory.get(), candleHistory.get(), data.get(), value.get(), candles?.get(), liveCandle?.get(), canvasWidth.get(),
791
+ canvasHeight.get(), timeWindow.get(), smoothing.get(), rangeAnimationSV.get(), adaptiveSpeedBoostSV.get(),
792
+ exaggerateSV.get(), referenceValue.get(), referenceValues.get(), thresholdRangePoints.get(),
793
+ thresholdRangeExtendToStart.get(), thresholdRangeExtendToNow.get(), nonNegativeSV.get(),
794
+ maxValueSV.get(), yRangeScale?.get(), nowOverrideSV.get(), windowBufferSV.get(), pausedSV.get(),
795
+ viewEnd.get(), viewWindow.get(), allowFutureViewEndSV.get(), returnT.get(), returnFrom.get(),
796
+ snapSV.get(), modeSV.get(), candleGapsSV.get(), candleGapBridgeNoTradesSV.get(),
797
+ candleGapBridgeUnavailableSV.get(), candleGapBridgeUnknownSV.get(), isFrameLoopActive?.get(),
798
+ keepAwake?.get(), wakeSignal?.get()];
799
+ }, [pointHistory, candleHistory, data, value, candles, liveCandle, canvasWidth, canvasHeight, timeWindow, smoothing, rangeAnimationSV,
800
+ adaptiveSpeedBoostSV, exaggerateSV, referenceValue, referenceValues, thresholdRangePoints,
801
+ thresholdRangeExtendToStart, thresholdRangeExtendToNow, nonNegativeSV, maxValueSV, yRangeScale,
802
+ nowOverrideSV, windowBufferSV, pausedSV, viewEnd, viewWindow, allowFutureViewEndSV, returnT,
803
+ returnFrom, snapSV, modeSV, candleGapsSV, candleGapBridgeNoTradesSV, candleGapBridgeUnavailableSV,
804
+ candleGapBridgeUnknownSV, isFrameLoopActive, keepAwake, wakeSignal]);
805
+
806
+ const wakeLoop = useDemandFrameLoop(autoSleep && !isStatic, prepareWake, tickFrame);
807
+ const wake = useCallback(() => {
808
+ "worklet";
809
+ if (!autoSleep || isStatic) return;
810
+ // Gestures need a fresh live boundary BEFORE clamping their first delta.
811
+ if (isFrameLoopActive?.get() !== false) {
812
+ liveEdge.set((nowOverrideSV.get() ?? Date.now() / 1000) + windowBufferSV.get() * timeWindow.get());
727
813
  }
728
- }, !isStatic);
814
+ wakeLoop();
815
+ }, [autoSleep, isStatic, isFrameLoopActive, liveEdge, nowOverrideSV, windowBufferSV, timeWindow, wakeLoop]);
816
+
817
+ const legacyActive = !isStatic && !autoSleep;
818
+ const engineFrameCallback = useFrameCallback((frameInfo) => {
819
+ "worklet";
820
+ tickFrame(frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS);
821
+ }, legacyActive);
729
822
  useEffect(() => {
730
- // Reanimated only seeds `isActive` from `autostart` on mount. Keep the
731
- // imperative state aligned when a chart switches between static and live.
732
- engineFrameCallback.setActive(!isStatic);
733
- }, [isStatic, engineFrameCallback]);
823
+ engineFrameCallback.setActive(legacyActive);
824
+ }, [legacyActive, engineFrameCallback]);
734
825
 
735
826
  // When time-scroll is disabled while scrolled back, return the window to the
736
827
  // live edge. With a positive duration this glides: snapshot the frozen edge into
@@ -769,29 +860,32 @@ export function useLiveChartEngine(
769
860
  // constant so it never fires.
770
861
  useAnimatedReaction(
771
862
  () => {
772
- if (!isStaticSV.get()) return 0; // inert when not static — never changes
773
- const d = data.get();
774
- const n = d.length;
775
- return [
776
- n,
777
- n ? d[0].time : 0,
778
- n ? d[0].value : 0,
779
- n ? d[n - 1].time : 0,
780
- n ? d[n - 1].value : 0,
781
- value.get(),
782
- canvasWidth.get(),
783
- canvasHeight.get(),
784
- timeWindow.get(),
785
- // Range props can arrive after the data shared value. Re-settle when
786
- // those bounds change too, rather than retaining the previous scale
787
- // until another data/layout update (static charts have no frame loop).
788
- maxValueSV.get(),
789
- referenceValue.get(),
790
- ...(referenceValues.get() ?? []),
791
- ].join(",");
863
+ if (!isStaticSV.get()) return null; // inert when not static
864
+ return {
865
+ revision:
866
+ modeSV.get() === "candle" ? candleHistory.get() : pointHistory.get(),
867
+ fingerprint: [
868
+ value.get(),
869
+ canvasWidth.get(),
870
+ canvasHeight.get(),
871
+ timeWindow.get(),
872
+ // Range props can arrive after the data shared value. Re-settle when
873
+ // those bounds change too, rather than retaining the previous scale
874
+ // until another data/layout update (static charts have no frame loop).
875
+ maxValueSV.get(),
876
+ referenceValue.get(),
877
+ ...(referenceValues.get() ?? []),
878
+ ].join(","),
879
+ };
792
880
  },
793
881
  (curr, prev) => {
794
- if (!isStaticSV.get() || curr === prev) return;
882
+ if (
883
+ !isStaticSV.get() ||
884
+ curr === null ||
885
+ (curr.revision === prev?.revision &&
886
+ curr.fingerprint === prev?.fingerprint)
887
+ )
888
+ return;
795
889
  applyLiveChartEngineFrame(
796
890
  { timeSincePreviousFrame: MS_PER_FRAME_60FPS },
797
891
  frameRefs,
@@ -805,6 +899,7 @@ export function useLiveChartEngine(
805
899
  );
806
900
 
807
901
  return {
902
+ wake,
808
903
  data,
809
904
  value,
810
905
  displayValue,
@@ -10,7 +10,7 @@ import {
10
10
  type SharedValue,
11
11
  } from "react-native-reanimated";
12
12
  import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
13
- import type { LiveChartPoint, SeriesConfig } from "../types";
13
+ import type { LiveChartPoint, RangeAnimationConfig, SeriesConfig } from "../types";
14
14
  import {
15
15
  tickLiveChartSeriesEngineFrame,
16
16
  type MultiEngineTickInput,
@@ -22,6 +22,7 @@ export interface MultiSeriesEngineConfig {
22
22
  series: SharedValue<SeriesConfig[]>;
23
23
  timeWindow: number;
24
24
  smoothing: number;
25
+ rangeAnimation?: RangeAnimationConfig;
25
26
  /** Extra catch-up speed added to `smoothing` when a series tip lags. */
26
27
  adaptiveSpeedBoost?: number;
27
28
  exaggerate?: boolean;
@@ -64,6 +65,8 @@ export interface MultiSeriesEngineConfig {
64
65
 
65
66
  export interface MultiEngineFrameRefs {
66
67
  series: SharedValue<SeriesConfig[]>;
68
+ /** Drawing input published with this frame's range/tips. Optional for standalone callers. */
69
+ renderedSeries?: SharedValue<SeriesConfig[]>;
67
70
  displaySeriesValues: SharedValue<number[]>;
68
71
  seriesOpacities: SharedValue<number[]>;
69
72
  displayMin: SharedValue<number>;
@@ -74,6 +77,7 @@ export interface MultiEngineFrameRefs {
74
77
  canvasHeight: SharedValue<number>;
75
78
  timeWindow: SharedValue<number>;
76
79
  smoothing: SharedValue<number>;
80
+ rangeAnimationSV?: SharedValue<RangeAnimationConfig | undefined>;
77
81
  adaptiveSpeedBoostSV?: SharedValue<number | undefined>;
78
82
  exaggerateSV: SharedValue<boolean>;
79
83
  referenceValue: SharedValue<number | undefined>;
@@ -238,6 +242,7 @@ export function applyLiveChartSeriesEngineFrame(
238
242
  input.canvasHeight = sv.canvasHeight.value;
239
243
  input.timeWindow = sv.timeWindow.value;
240
244
  input.smoothing = sv.smoothing.value;
245
+ input.rangeAnimation = sv.rangeAnimationSV?.value;
241
246
  input.adaptiveSpeedBoost = sv.adaptiveSpeedBoostSV?.value;
242
247
  input.exaggerate = sv.exaggerateSV.value;
243
248
  input.referenceValue = sv.referenceValue.value;
@@ -268,6 +273,17 @@ export function applyLiveChartSeriesEngineFrame(
268
273
  sv.extremaMaxValue.value = state.extremaMaxValue;
269
274
  sv.extremaMinTime.value = state.extremaMinTime;
270
275
  sv.extremaMaxTime.value = state.extremaMaxTime;
276
+ // Drawing must not react to replacement histories before the engine has
277
+ // calculated their range and tips. Publish from the same measured frame and
278
+ // the same input read above. Copy entries because producers can mutate live
279
+ // values in place; retain history arrays to avoid copying every point per frame.
280
+ if (sv.renderedSeries && input.canvasWidth !== 0 && input.canvasHeight !== 0) {
281
+ const rendered: SeriesConfig[] = [];
282
+ for (let i = 0; i < seriesSnap.length; i++) {
283
+ rendered.push({ ...seriesSnap[i] });
284
+ }
285
+ sv.renderedSeries.value = rendered;
286
+ }
271
287
  // Clear the one-shot snap so the next frame eases normally again — but only
272
288
  // once a real (measured) frame consumed it (the tick early-returns on a
273
289
  // zero-size canvas before applying the snap). See applyLiveChartEngineFrame.
@@ -288,6 +304,7 @@ export function useLiveChartSeriesEngine(
288
304
  const viewWindow = useSharedValue<number | null>(null);
289
305
  const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
290
306
  const smoothing = useDerivedValue(() => config.smoothing);
307
+ const rangeAnimationSV = useDerivedValue(() => config.rangeAnimation);
291
308
  const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
292
309
  const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
293
310
  const referenceValue = useDerivedValue(() => config.referenceValue);
@@ -328,6 +345,7 @@ export function useLiveChartSeriesEngine(
328
345
 
329
346
  const displaySeriesValues = useSharedValue<number[]>([]);
330
347
  const seriesOpacities = useSharedValue<number[]>([]);
348
+ const renderedSeries = useSharedValue<SeriesConfig[]>([]);
331
349
 
332
350
  // Live data extrema (value + time of the visible high / low across series).
333
351
  const extremaMinValue = useSharedValue(NaN);
@@ -363,6 +381,7 @@ export function useLiveChartSeriesEngine(
363
381
 
364
382
  const frameRefs: MultiEngineFrameRefs = {
365
383
  series,
384
+ renderedSeries,
366
385
  displaySeriesValues,
367
386
  seriesOpacities,
368
387
  displayMin,
@@ -373,6 +392,7 @@ export function useLiveChartSeriesEngine(
373
392
  canvasHeight,
374
393
  timeWindow,
375
394
  smoothing,
395
+ rangeAnimationSV,
376
396
  adaptiveSpeedBoostSV,
377
397
  exaggerateSV,
378
398
  referenceValue,
@@ -448,7 +468,7 @@ export function useLiveChartSeriesEngine(
448
468
  viewEnd,
449
469
  viewWindow,
450
470
  liveEdge,
451
- series,
471
+ series: renderedSeries,
452
472
  displaySeriesValues,
453
473
  seriesOpacities,
454
474
  extremaMinValue,
@@ -0,0 +1,38 @@
1
+ import type { SkPathBuilder } from "@shopify/react-native-skia";
2
+ import type { CandleRect } from "./candle";
3
+
4
+ /**
5
+ * Build one color's candle-body or volume-bar batch. Skia copies addRect's coordinates synchronously,
6
+ * so one plain rectangle can serve every sharp body in this rebuild. Keep it
7
+ * local to the worklet invocation: derived initializers also run on React's
8
+ * runtime, where mutating a previously serialized scratch object is unsafe.
9
+ */
10
+ export function buildCandleBodyPath(
11
+ builder: SkPathBuilder,
12
+ bodies: CandleRect[],
13
+ up: boolean,
14
+ radius: number,
15
+ ) {
16
+ "worklet";
17
+ const rect = { x: 0, y: 0, width: 0, height: 0 };
18
+ for (let i = 0; i < bodies.length; i++) {
19
+ const body = bodies[i];
20
+ if (body.up !== up) continue;
21
+ const rr = radius > 0 ? Math.min(radius, body.w / 2, body.h / 2) : 0;
22
+ if (rr > 0) {
23
+ builder.addRRect({
24
+ rect: { x: body.x, y: body.y, width: body.w, height: body.h },
25
+ rx: rr,
26
+ ry: rr,
27
+ });
28
+ } else {
29
+ rect.x = body.x;
30
+ rect.y = body.y;
31
+ rect.width = body.w;
32
+ rect.height = body.h;
33
+ builder.addRect(rect);
34
+ }
35
+ }
36
+ // A fresh immutable path both resets the builder and notifies subscribers.
37
+ return builder.detach();
38
+ }
@@ -0,0 +1,63 @@
1
+ import type { SkPathBuilder } from "@shopify/react-native-skia";
2
+ import { drawSpline, type SplineScratch } from "../math/spline";
3
+ import { sampleThresholdYAt } from "../math/threshold";
4
+
5
+ /** Emit each segment's curve math once into the line, fill and optional band.
6
+ * Builders remain separate so gaps and independent fill closures are preserved.
7
+ * Finalization is left to the caller; no extra intermediate native paths.
8
+ */
9
+ export function buildLineFillPaths(
10
+ line: SkPathBuilder,
11
+ fill: SkPathBuilder,
12
+ band: SkPathBuilder | undefined,
13
+ pts: number[],
14
+ ranges: number[],
15
+ scratch: SplineScratch,
16
+ linear: boolean,
17
+ bottom: number,
18
+ thresholdY: number,
19
+ samples: number[] | undefined,
20
+ x0: number,
21
+ x1: number,
22
+ /** A threshold-only update can retain the existing line and fill paths. */
23
+ bandOnly = false,
24
+ ) {
25
+ "worklet";
26
+ for (let range = 0; range < ranges.length; range += 2) {
27
+ const start = ranges[range],
28
+ end = ranges[range + 1];
29
+ const first = start * 2,
30
+ last = (end - 1) * 2;
31
+ const left = pts[first],
32
+ right = pts[last];
33
+ if (!bandOnly) {
34
+ line.moveTo(left, pts[first + 1]);
35
+ fill.moveTo(left, pts[first + 1]);
36
+ }
37
+ band?.moveTo(left, pts[first + 1]);
38
+ if (bandOnly) {
39
+ if (band) drawSpline(band, pts, scratch, linear, start, end);
40
+ } else {
41
+ drawSpline(line, pts, scratch, linear, start, end, fill, band);
42
+ fill.lineTo(right, bottom);
43
+ fill.lineTo(left, bottom);
44
+ fill.close();
45
+ }
46
+ if (band) {
47
+ if (samples && samples.length >= 2) {
48
+ const count = samples.length,
49
+ step = (x1 - x0) / (count - 1);
50
+ band.lineTo(right, sampleThresholdYAt(samples, x0, x1, right));
51
+ for (let i = count - 1; i >= 0; i--) {
52
+ const x = x0 + step * i;
53
+ if (x > left && x < right) band.lineTo(x, samples[i]);
54
+ }
55
+ band.lineTo(left, sampleThresholdYAt(samples, x0, x1, left));
56
+ } else {
57
+ band.lineTo(right, thresholdY);
58
+ band.lineTo(left, thresholdY);
59
+ }
60
+ band.close();
61
+ }
62
+ }
63
+ }