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.
- package/dist/components/DegenParticlesOverlay.d.ts +2 -2
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +5 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +3 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/core/historyRangeCache.d.ts +25 -0
- package/dist/core/historyRangeCache.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -2
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +2 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +1 -1
- package/dist/core/useHistoryRevision.d.ts +5 -0
- package/dist/core/useHistoryRevision.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +20 -2
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +5 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candleBodyPath.d.ts +10 -0
- package/dist/draw/candleBodyPath.d.ts.map +1 -0
- package/dist/draw/lineFillPaths.d.ts +10 -0
- package/dist/draw/lineFillPaths.d.ts.map +1 -0
- package/dist/draw/volume.d.ts +4 -1
- package/dist/draw/volume.d.ts.map +1 -1
- package/dist/draw/volumeRange.d.ts +12 -0
- package/dist/draw/volumeRange.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +8 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +1 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +1 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +1 -0
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useDemandFrameLoop.d.ts +11 -0
- package/dist/hooks/useDemandFrameLoop.d.ts.map +1 -0
- package/dist/hooks/useLoadingLook.d.ts +12 -0
- package/dist/hooks/useLoadingLook.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesDegen.d.ts +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +2 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts +2 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -1
- package/dist/hooks/useReferenceDrag.d.ts +3 -2
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineGrouping.d.ts +19 -0
- package/dist/hooks/useReferenceLineGrouping.d.ts.map +1 -0
- package/dist/hooks/useVolumeRange.d.ts +14 -0
- package/dist/hooks/useVolumeRange.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +7 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/rangeAnimation.d.ts +4 -0
- package/dist/math/rangeAnimation.d.ts.map +1 -0
- package/dist/math/referenceDrag.d.ts +17 -5
- package/dist/math/referenceDrag.d.ts.map +1 -1
- package/dist/math/referenceGroup.d.ts +2 -2
- package/dist/math/spline.d.ts +3 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +8 -0
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/types.d.ts +52 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DegenParticlesOverlay.tsx +5 -5
- package/src/components/LiveChart.tsx +37 -66
- package/src/components/LiveChartSeries.tsx +18 -6
- package/src/components/LoadingOverlay.tsx +53 -12
- package/src/components/YAxisOverlay.tsx +38 -2
- package/src/constants.ts +3 -3
- package/src/core/historyRangeCache.ts +109 -0
- package/src/core/liveChartEngineTick.ts +41 -63
- package/src/core/liveChartSeriesEngineTick.ts +18 -5
- package/src/core/resolveConfig.ts +1 -1
- package/src/core/useHistoryRevision.ts +36 -0
- package/src/core/useLiveChartEngine.ts +159 -64
- package/src/core/useLiveChartSeriesEngine.ts +22 -2
- package/src/draw/candleBodyPath.ts +38 -0
- package/src/draw/lineFillPaths.ts +63 -0
- package/src/draw/volume.ts +21 -23
- package/src/draw/volumeRange.ts +51 -0
- package/src/hooks/crosshairShared.ts +29 -0
- package/src/hooks/useCandlePaths.ts +68 -94
- package/src/hooks/useChartPaths.ts +87 -111
- package/src/hooks/useCrosshair.ts +15 -3
- package/src/hooks/useDegen.ts +10 -6
- package/src/hooks/useDemandFrameLoop.ts +82 -0
- package/src/hooks/useLoadingLook.ts +30 -0
- package/src/hooks/useMarkers.ts +18 -4
- package/src/hooks/useMultiSeriesDegen.ts +7 -3
- package/src/hooks/usePanScroll.ts +5 -0
- package/src/hooks/usePinchZoom.ts +5 -0
- package/src/hooks/useReferenceDrag.ts +28 -14
- package/src/hooks/useReferenceLineGrouping.ts +72 -0
- package/src/hooks/useVolumeRange.ts +40 -0
- package/src/hooks/useXAxis.ts +27 -3
- package/src/hooks/useYAxis.ts +18 -2
- package/src/index.ts +1 -0
- package/src/math/rangeAnimation.ts +28 -0
- package/src/math/referenceDrag.ts +21 -4
- package/src/math/referenceGroup.ts +2 -2
- package/src/math/spline.ts +16 -8
- package/src/math/squiggly.ts +12 -0
- 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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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 (
|
|
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
|
+
}
|