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.
- package/dist/components/DegenParticlesOverlay.d.ts +12 -11
- 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 +4 -1
- package/dist/core/liveChartEngineTick.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 +17 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +2 -0
- 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/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/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/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/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 +11 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DegenParticlesOverlay.tsx +56 -35
- package/src/components/LiveChart.tsx +34 -4
- package/src/components/LiveChartSeries.tsx +14 -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 +24 -57
- package/src/core/resolveConfig.ts +1 -1
- package/src/core/useHistoryRevision.ts +36 -0
- package/src/core/useLiveChartEngine.ts +151 -64
- package/src/core/useLiveChartSeriesEngine.ts +16 -1
- 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/useCandlePaths.ts +68 -94
- package/src/hooks/useChartPaths.ts +87 -111
- 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/useVolumeRange.ts +40 -0
- package/src/hooks/useXAxis.ts +27 -3
- package/src/hooks/useYAxis.ts +18 -2
- package/src/math/spline.ts +16 -8
- package/src/math/squiggly.ts +12 -0
- package/src/types.ts +11 -2
|
@@ -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,6 +16,7 @@ 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,
|
|
@@ -30,7 +31,18 @@ import {
|
|
|
30
31
|
type EngineTickMutable,
|
|
31
32
|
} from "./liveChartEngineTick";
|
|
32
33
|
|
|
34
|
+
import {
|
|
35
|
+
makeHistoryRangeCache,
|
|
36
|
+
type HistoryRangeCache,
|
|
37
|
+
type HistoryRevision,
|
|
38
|
+
} from "./historyRangeCache";
|
|
39
|
+
|
|
40
|
+
import { useHistoryRevision } from "./useHistoryRevision";
|
|
41
|
+
|
|
33
42
|
export interface EngineConfig {
|
|
43
|
+
/** Original inputs before a reveal/stash bridge that may suppress same-array notifications. */
|
|
44
|
+
dataChangeSource?: SharedValue<LiveChartPoint[]>;
|
|
45
|
+
candlesChangeSource?: SharedValue<CandlePoint[]>;
|
|
34
46
|
data: SharedValue<LiveChartPoint[]>;
|
|
35
47
|
value: SharedValue<number>;
|
|
36
48
|
timeWindow: number;
|
|
@@ -96,6 +108,12 @@ export interface EngineConfig {
|
|
|
96
108
|
static?: boolean;
|
|
97
109
|
/** Runtime gate for the frame loop. Gestures and configuration stay mounted. */
|
|
98
110
|
isFrameLoopActive?: SharedValue<boolean>;
|
|
111
|
+
/** Experimental automatic idle scheduling, single-series only. */
|
|
112
|
+
autoSleep?: boolean;
|
|
113
|
+
/** Effects that require continuous engine work. */
|
|
114
|
+
keepAwake?: SharedValue<boolean>;
|
|
115
|
+
/** Appearance/reveal changes that should wake a settled engine. */
|
|
116
|
+
wakeSignal?: SharedValue<unknown>;
|
|
99
117
|
/** Optional development-only frame publication counters. */
|
|
100
118
|
debugFrameStats?: SharedValue<LiveChartFrameStats>;
|
|
101
119
|
/**
|
|
@@ -185,16 +203,21 @@ export interface ChartEngineEdge {
|
|
|
185
203
|
edgeValue: SharedValue<number>;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
export interface SingleEngineState
|
|
206
|
+
export interface SingleEngineState
|
|
207
|
+
extends ChartEngineLayout,
|
|
208
|
+
ChartEngineExtrema {
|
|
189
209
|
data: SharedValue<LiveChartPoint[]>;
|
|
190
210
|
value: SharedValue<number>;
|
|
191
211
|
displayValue: SharedValue<number>;
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
export interface MultiEngineState
|
|
214
|
+
export interface MultiEngineState
|
|
215
|
+
extends ChartEngineLayout,
|
|
216
|
+
ChartEngineExtrema {
|
|
195
217
|
data: SharedValue<LiveChartPoint[]>;
|
|
196
218
|
value: SharedValue<number>;
|
|
197
219
|
displayValue: SharedValue<number>;
|
|
220
|
+
/** Entries captured by the last measured engine frame; history arrays remain shared. */
|
|
198
221
|
series: SharedValue<SeriesConfig[]>;
|
|
199
222
|
displaySeriesValues: SharedValue<number[]>;
|
|
200
223
|
seriesOpacities: SharedValue<number[]>;
|
|
@@ -208,6 +231,8 @@ export type ChartEngineWithLiveValue = ChartEngineLayout & {
|
|
|
208
231
|
};
|
|
209
232
|
|
|
210
233
|
export interface EngineFrameRefs {
|
|
234
|
+
pointHistory?: SharedValue<HistoryRevision<LiveChartPoint> | undefined>;
|
|
235
|
+
candleHistory?: SharedValue<HistoryRevision<CandlePoint> | undefined>;
|
|
211
236
|
data: SharedValue<LiveChartPoint[]>;
|
|
212
237
|
value: SharedValue<number>;
|
|
213
238
|
displayValue: SharedValue<number>;
|
|
@@ -268,6 +293,7 @@ export interface EngineFrameRefs {
|
|
|
268
293
|
|
|
269
294
|
/** Stable state/input containers reused by the UI-thread frame callback. */
|
|
270
295
|
export interface EngineFrameScratch {
|
|
296
|
+
historyCache: HistoryRangeCache;
|
|
271
297
|
state: EngineTickMutable;
|
|
272
298
|
input: EngineTickInput;
|
|
273
299
|
}
|
|
@@ -275,6 +301,7 @@ export interface EngineFrameScratch {
|
|
|
275
301
|
/** Allocate one single-series frame scratch. Call once per engine instance. */
|
|
276
302
|
export function makeEngineFrameScratch(): EngineFrameScratch {
|
|
277
303
|
return {
|
|
304
|
+
historyCache: makeHistoryRangeCache(),
|
|
278
305
|
state: {
|
|
279
306
|
displayValue: 0,
|
|
280
307
|
displayMin: 0,
|
|
@@ -375,7 +402,9 @@ export function applyLiveChartEngineFrame(
|
|
|
375
402
|
input.nowOverride = sv.nowOverrideSV?.value;
|
|
376
403
|
input.windowBuffer = sv.windowBufferSV?.value ?? 0;
|
|
377
404
|
input.targetValue = sv.value.value;
|
|
378
|
-
|
|
405
|
+
const pointHistory = sv.pointHistory?.value;
|
|
406
|
+
const candleHistory = sv.candleHistory?.value;
|
|
407
|
+
input.points = pointHistory?.data ?? sv.data.value;
|
|
379
408
|
input.nowSeconds = Date.now() / 1000;
|
|
380
409
|
input.paused = sv.pausedSV.value;
|
|
381
410
|
input.snap = snap;
|
|
@@ -385,15 +414,16 @@ export function applyLiveChartEngineFrame(
|
|
|
385
414
|
input.returnFrom = sv.returnFromSV?.value;
|
|
386
415
|
input.viewWindow = sv.viewWindowSV?.value;
|
|
387
416
|
input.mode = sv.modeSV.value;
|
|
388
|
-
input.candles = sv.candles?.value;
|
|
417
|
+
input.candles = candleHistory?.data ?? sv.candles?.value;
|
|
418
|
+
input.historyRevision =
|
|
419
|
+
input.mode === "candle" ? candleHistory : pointHistory;
|
|
389
420
|
input.liveCandle = sv.liveCandle?.value;
|
|
390
421
|
input.candleGaps = sv.candleGapsSV?.value;
|
|
391
|
-
input.candleGapBridgeNoTrades =
|
|
392
|
-
sv.candleGapBridgeNoTradesSV?.value ?? false;
|
|
422
|
+
input.candleGapBridgeNoTrades = sv.candleGapBridgeNoTradesSV?.value ?? false;
|
|
393
423
|
input.candleGapBridgeUnavailable =
|
|
394
424
|
sv.candleGapBridgeUnavailableSV?.value ?? false;
|
|
395
425
|
input.candleGapBridgeUnknown = sv.candleGapBridgeUnknownSV?.value ?? false;
|
|
396
|
-
tickLiveChartEngineFrame(state, input);
|
|
426
|
+
tickLiveChartEngineFrame(state, input, scratch?.historyCache);
|
|
397
427
|
sv.displayValue.value = state.displayValue;
|
|
398
428
|
sv.displayMin.value = state.displayMin;
|
|
399
429
|
sv.displayMax.value = state.displayMax;
|
|
@@ -409,16 +439,22 @@ export function applyLiveChartEngineFrame(
|
|
|
409
439
|
// once a real (measured) frame consumed it. The tick early-returns on a
|
|
410
440
|
// zero-size canvas (before applying the snap), so keep the flag pending until
|
|
411
441
|
// the canvas has laid out, or a snap arriving pre-layout would be dropped.
|
|
412
|
-
if (
|
|
442
|
+
if (
|
|
443
|
+
snap &&
|
|
444
|
+
sv.snapSV &&
|
|
445
|
+
sv.canvasWidth.value !== 0 &&
|
|
446
|
+
sv.canvasHeight.value !== 0
|
|
447
|
+
) {
|
|
413
448
|
sv.snapSV.value = false;
|
|
414
449
|
}
|
|
415
450
|
}
|
|
416
451
|
|
|
417
452
|
export function useLiveChartEngine(
|
|
418
453
|
config: EngineConfig,
|
|
419
|
-
): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
|
|
454
|
+
): SingleEngineState & ChartEngineScroll & ChartEngineEdge & { wake: () => void } {
|
|
420
455
|
const {
|
|
421
456
|
data,
|
|
457
|
+
dataChangeSource = data,
|
|
422
458
|
value,
|
|
423
459
|
timeWindow: configuredTimeWindow,
|
|
424
460
|
smoothing: configuredSmoothing,
|
|
@@ -442,9 +478,13 @@ export function useLiveChartEngine(
|
|
|
442
478
|
static: isStatic,
|
|
443
479
|
isFrameLoopActive,
|
|
444
480
|
debugFrameStats,
|
|
481
|
+
autoSleep = false,
|
|
482
|
+
keepAwake,
|
|
483
|
+
wakeSignal,
|
|
445
484
|
snapKey,
|
|
446
485
|
mode,
|
|
447
486
|
candles,
|
|
487
|
+
candlesChangeSource = candles,
|
|
448
488
|
liveCandle,
|
|
449
489
|
candleGaps,
|
|
450
490
|
candleGapBridgeNoTrades,
|
|
@@ -472,9 +512,7 @@ export function useLiveChartEngine(
|
|
|
472
512
|
);
|
|
473
513
|
// Static charts snap to their target in one tick (smoothing=1), so the single
|
|
474
514
|
// settle reaction below produces the final state with no per-frame easing.
|
|
475
|
-
const smoothing = useDerivedValue(() =>
|
|
476
|
-
isStatic ? 1 : configuredSmoothing,
|
|
477
|
-
);
|
|
515
|
+
const smoothing = useDerivedValue(() => (isStatic ? 1 : configuredSmoothing));
|
|
478
516
|
const adaptiveSpeedBoostSV = useDerivedValue(() => adaptiveSpeedBoost);
|
|
479
517
|
const exaggerateSV = useDerivedValue(() => exaggerate ?? false);
|
|
480
518
|
const referenceValue = useDerivedValue(() => configuredReferenceValue);
|
|
@@ -597,8 +635,14 @@ export function useLiveChartEngine(
|
|
|
597
635
|
// React renders: Reanimated uses captured closure values as implicit reaction
|
|
598
636
|
// dependencies, and rebuilding this object would unregister/re-register the
|
|
599
637
|
// static settle mapper behind its path consumers. See #329.
|
|
638
|
+
// Observe original feeds as well as reveal bridges: same-array assignments
|
|
639
|
+
// may suppress bridge notifications. Live candle changes stay uncached.
|
|
640
|
+
const pointHistory = useHistoryRevision(data, dataChangeSource);
|
|
641
|
+
const candleHistory = useHistoryRevision(candles, candlesChangeSource);
|
|
600
642
|
const frameRefs = useMemo<EngineFrameRefs>(
|
|
601
643
|
() => ({
|
|
644
|
+
pointHistory,
|
|
645
|
+
candleHistory,
|
|
602
646
|
data,
|
|
603
647
|
value,
|
|
604
648
|
displayValue,
|
|
@@ -644,6 +688,8 @@ export function useLiveChartEngine(
|
|
|
644
688
|
extremaMaxTime,
|
|
645
689
|
}),
|
|
646
690
|
[
|
|
691
|
+
pointHistory,
|
|
692
|
+
candleHistory,
|
|
647
693
|
adaptiveSpeedBoostSV,
|
|
648
694
|
allowFutureViewEndSV,
|
|
649
695
|
candleGapBridgeNoTradesSV,
|
|
@@ -696,41 +742,78 @@ export function useLiveChartEngine(
|
|
|
696
742
|
frameScratchRef.current = makeEngineFrameScratch();
|
|
697
743
|
}
|
|
698
744
|
|
|
699
|
-
|
|
700
|
-
// loop is fully inert in static mode (the invariant that makes this worth it).
|
|
701
|
-
const engineFrameCallback = useFrameCallback((frameInfo) => {
|
|
745
|
+
const tickFrame = useCallback((dt: number): boolean => {
|
|
702
746
|
"worklet";
|
|
703
|
-
if (isFrameLoopActive?.get() === false) return;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
|
|
747
|
+
if (isFrameLoopActive?.get() === false) return false;
|
|
748
|
+
if (!autoSleep && !debugFrameStats) {
|
|
749
|
+
applyLiveChartEngineFrame({ timeSincePreviousFrame: dt }, frameRefs, frameScratchRef.current!);
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
const beforeValue = displayValue.get();
|
|
753
|
+
const beforeMin = displayMin.get();
|
|
754
|
+
const beforeMax = displayMax.get();
|
|
755
|
+
const beforeWindow = displayWindow.get();
|
|
756
|
+
const beforeTimestamp = timestamp.get();
|
|
757
|
+
const beforeLiveEdge = liveEdge.get();
|
|
758
|
+
const beforeEdgeValue = edgeValue.get();
|
|
759
|
+
applyLiveChartEngineFrame({ timeSincePreviousFrame: dt }, frameRefs, frameScratchRef.current!);
|
|
760
|
+
const changed = beforeValue !== displayValue.get() || beforeMin !== displayMin.get() ||
|
|
761
|
+
beforeMax !== displayMax.get() || beforeWindow !== displayWindow.get() ||
|
|
762
|
+
beforeTimestamp !== timestamp.get() || beforeEdgeValue !== edgeValue.get();
|
|
712
763
|
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();
|
|
764
|
+
const didPublish = changed || beforeLiveEdge !== liveEdge.get();
|
|
721
765
|
const previous = debugFrameStats.get();
|
|
722
|
-
debugFrameStats.set({
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
766
|
+
debugFrameStats.set({ frames: previous.frames + 1, published: previous.published + (didPublish ? 1 : 0), skipped: previous.skipped + (didPublish ? 0 : 1) });
|
|
767
|
+
}
|
|
768
|
+
// The liveEdge clock alone is not a visible animation while parked/paused.
|
|
769
|
+
// Do not sleep if a stranded viewEnd actually fell back to following live.
|
|
770
|
+
const parked = viewEnd.get() !== null && timestamp.get() === viewEnd.get();
|
|
771
|
+
const clockMoves = nowOverrideSV.get() === undefined && !pausedSV.get() && !parked;
|
|
772
|
+
return changed || clockMoves || keepAwake?.get() === true || returnT.get() < 1;
|
|
773
|
+
}, [autoSleep, isFrameLoopActive, displayValue, displayMin, displayMax, displayWindow, timestamp,
|
|
774
|
+
liveEdge, edgeValue, frameRefs, frameScratchRef, debugFrameStats, viewEnd, nowOverrideSV, pausedSV, keepAwake, returnT]);
|
|
775
|
+
|
|
776
|
+
const prepareWake = useCallback(() => {
|
|
777
|
+
"worklet";
|
|
778
|
+
// Read the arrays themselves, not a length/endpoints fingerprint: modify()
|
|
779
|
+
// can change an interior value without changing any of those quantities.
|
|
780
|
+
// History revisions also notify when a reveal bridge suppresses a same-array
|
|
781
|
+
// source update. A sleeping engine must wake to consume that invalidation.
|
|
782
|
+
return [pointHistory.get(), candleHistory.get(), data.get(), value.get(), candles?.get(), liveCandle?.get(), canvasWidth.get(),
|
|
783
|
+
canvasHeight.get(), timeWindow.get(), smoothing.get(), adaptiveSpeedBoostSV.get(),
|
|
784
|
+
exaggerateSV.get(), referenceValue.get(), referenceValues.get(), thresholdRangePoints.get(),
|
|
785
|
+
thresholdRangeExtendToStart.get(), thresholdRangeExtendToNow.get(), nonNegativeSV.get(),
|
|
786
|
+
maxValueSV.get(), yRangeScale?.get(), nowOverrideSV.get(), windowBufferSV.get(), pausedSV.get(),
|
|
787
|
+
viewEnd.get(), viewWindow.get(), allowFutureViewEndSV.get(), returnT.get(), returnFrom.get(),
|
|
788
|
+
snapSV.get(), modeSV.get(), candleGapsSV.get(), candleGapBridgeNoTradesSV.get(),
|
|
789
|
+
candleGapBridgeUnavailableSV.get(), candleGapBridgeUnknownSV.get(), isFrameLoopActive?.get(),
|
|
790
|
+
keepAwake?.get(), wakeSignal?.get()];
|
|
791
|
+
}, [pointHistory, candleHistory, data, value, candles, liveCandle, canvasWidth, canvasHeight, timeWindow, smoothing,
|
|
792
|
+
adaptiveSpeedBoostSV, exaggerateSV, referenceValue, referenceValues, thresholdRangePoints,
|
|
793
|
+
thresholdRangeExtendToStart, thresholdRangeExtendToNow, nonNegativeSV, maxValueSV, yRangeScale,
|
|
794
|
+
nowOverrideSV, windowBufferSV, pausedSV, viewEnd, viewWindow, allowFutureViewEndSV, returnT,
|
|
795
|
+
returnFrom, snapSV, modeSV, candleGapsSV, candleGapBridgeNoTradesSV, candleGapBridgeUnavailableSV,
|
|
796
|
+
candleGapBridgeUnknownSV, isFrameLoopActive, keepAwake, wakeSignal]);
|
|
797
|
+
|
|
798
|
+
const wakeLoop = useDemandFrameLoop(autoSleep && !isStatic, prepareWake, tickFrame);
|
|
799
|
+
const wake = useCallback(() => {
|
|
800
|
+
"worklet";
|
|
801
|
+
if (!autoSleep || isStatic) return;
|
|
802
|
+
// Gestures need a fresh live boundary BEFORE clamping their first delta.
|
|
803
|
+
if (isFrameLoopActive?.get() !== false) {
|
|
804
|
+
liveEdge.set((nowOverrideSV.get() ?? Date.now() / 1000) + windowBufferSV.get() * timeWindow.get());
|
|
727
805
|
}
|
|
728
|
-
|
|
806
|
+
wakeLoop();
|
|
807
|
+
}, [autoSleep, isStatic, isFrameLoopActive, liveEdge, nowOverrideSV, windowBufferSV, timeWindow, wakeLoop]);
|
|
808
|
+
|
|
809
|
+
const legacyActive = !isStatic && !autoSleep;
|
|
810
|
+
const engineFrameCallback = useFrameCallback((frameInfo) => {
|
|
811
|
+
"worklet";
|
|
812
|
+
tickFrame(frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS);
|
|
813
|
+
}, legacyActive);
|
|
729
814
|
useEffect(() => {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
engineFrameCallback.setActive(!isStatic);
|
|
733
|
-
}, [isStatic, engineFrameCallback]);
|
|
815
|
+
engineFrameCallback.setActive(legacyActive);
|
|
816
|
+
}, [legacyActive, engineFrameCallback]);
|
|
734
817
|
|
|
735
818
|
// When time-scroll is disabled while scrolled back, return the window to the
|
|
736
819
|
// live edge. With a positive duration this glides: snapshot the frozen edge into
|
|
@@ -769,29 +852,32 @@ export function useLiveChartEngine(
|
|
|
769
852
|
// constant so it never fires.
|
|
770
853
|
useAnimatedReaction(
|
|
771
854
|
() => {
|
|
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(",");
|
|
855
|
+
if (!isStaticSV.get()) return null; // inert when not static
|
|
856
|
+
return {
|
|
857
|
+
revision:
|
|
858
|
+
modeSV.get() === "candle" ? candleHistory.get() : pointHistory.get(),
|
|
859
|
+
fingerprint: [
|
|
860
|
+
value.get(),
|
|
861
|
+
canvasWidth.get(),
|
|
862
|
+
canvasHeight.get(),
|
|
863
|
+
timeWindow.get(),
|
|
864
|
+
// Range props can arrive after the data shared value. Re-settle when
|
|
865
|
+
// those bounds change too, rather than retaining the previous scale
|
|
866
|
+
// until another data/layout update (static charts have no frame loop).
|
|
867
|
+
maxValueSV.get(),
|
|
868
|
+
referenceValue.get(),
|
|
869
|
+
...(referenceValues.get() ?? []),
|
|
870
|
+
].join(","),
|
|
871
|
+
};
|
|
792
872
|
},
|
|
793
873
|
(curr, prev) => {
|
|
794
|
-
if (
|
|
874
|
+
if (
|
|
875
|
+
!isStaticSV.get() ||
|
|
876
|
+
curr === null ||
|
|
877
|
+
(curr.revision === prev?.revision &&
|
|
878
|
+
curr.fingerprint === prev?.fingerprint)
|
|
879
|
+
)
|
|
880
|
+
return;
|
|
795
881
|
applyLiveChartEngineFrame(
|
|
796
882
|
{ timeSincePreviousFrame: MS_PER_FRAME_60FPS },
|
|
797
883
|
frameRefs,
|
|
@@ -805,6 +891,7 @@ export function useLiveChartEngine(
|
|
|
805
891
|
);
|
|
806
892
|
|
|
807
893
|
return {
|
|
894
|
+
wake,
|
|
808
895
|
data,
|
|
809
896
|
value,
|
|
810
897
|
displayValue,
|
|
@@ -64,6 +64,8 @@ export interface MultiSeriesEngineConfig {
|
|
|
64
64
|
|
|
65
65
|
export interface MultiEngineFrameRefs {
|
|
66
66
|
series: SharedValue<SeriesConfig[]>;
|
|
67
|
+
/** Drawing input published with this frame's range/tips. Optional for standalone callers. */
|
|
68
|
+
renderedSeries?: SharedValue<SeriesConfig[]>;
|
|
67
69
|
displaySeriesValues: SharedValue<number[]>;
|
|
68
70
|
seriesOpacities: SharedValue<number[]>;
|
|
69
71
|
displayMin: SharedValue<number>;
|
|
@@ -268,6 +270,17 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
268
270
|
sv.extremaMaxValue.value = state.extremaMaxValue;
|
|
269
271
|
sv.extremaMinTime.value = state.extremaMinTime;
|
|
270
272
|
sv.extremaMaxTime.value = state.extremaMaxTime;
|
|
273
|
+
// Drawing must not react to replacement histories before the engine has
|
|
274
|
+
// calculated their range and tips. Publish from the same measured frame and
|
|
275
|
+
// the same input read above. Copy entries because producers can mutate live
|
|
276
|
+
// values in place; retain history arrays to avoid copying every point per frame.
|
|
277
|
+
if (sv.renderedSeries && input.canvasWidth !== 0 && input.canvasHeight !== 0) {
|
|
278
|
+
const rendered: SeriesConfig[] = [];
|
|
279
|
+
for (let i = 0; i < seriesSnap.length; i++) {
|
|
280
|
+
rendered.push({ ...seriesSnap[i] });
|
|
281
|
+
}
|
|
282
|
+
sv.renderedSeries.value = rendered;
|
|
283
|
+
}
|
|
271
284
|
// Clear the one-shot snap so the next frame eases normally again — but only
|
|
272
285
|
// once a real (measured) frame consumed it (the tick early-returns on a
|
|
273
286
|
// zero-size canvas before applying the snap). See applyLiveChartEngineFrame.
|
|
@@ -328,6 +341,7 @@ export function useLiveChartSeriesEngine(
|
|
|
328
341
|
|
|
329
342
|
const displaySeriesValues = useSharedValue<number[]>([]);
|
|
330
343
|
const seriesOpacities = useSharedValue<number[]>([]);
|
|
344
|
+
const renderedSeries = useSharedValue<SeriesConfig[]>([]);
|
|
331
345
|
|
|
332
346
|
// Live data extrema (value + time of the visible high / low across series).
|
|
333
347
|
const extremaMinValue = useSharedValue(NaN);
|
|
@@ -363,6 +377,7 @@ export function useLiveChartSeriesEngine(
|
|
|
363
377
|
|
|
364
378
|
const frameRefs: MultiEngineFrameRefs = {
|
|
365
379
|
series,
|
|
380
|
+
renderedSeries,
|
|
366
381
|
displaySeriesValues,
|
|
367
382
|
seriesOpacities,
|
|
368
383
|
displayMin,
|
|
@@ -448,7 +463,7 @@ export function useLiveChartSeriesEngine(
|
|
|
448
463
|
viewEnd,
|
|
449
464
|
viewWindow,
|
|
450
465
|
liveEdge,
|
|
451
|
-
series,
|
|
466
|
+
series: renderedSeries,
|
|
452
467
|
displaySeriesValues,
|
|
453
468
|
seriesOpacities,
|
|
454
469
|
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
|
+
}
|
package/src/draw/volume.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
volumeStartIndex,
|
|
3
|
+
volumeEndIndex,
|
|
4
|
+
visibleMaxVolume,
|
|
5
|
+
type VolumeRange,
|
|
6
|
+
} from "./volumeRange";
|
|
1
7
|
import { CANDLE_METRICS_DEFAULTS } from "../constants";
|
|
2
8
|
import type { CandleMetrics, CandlePoint } from "../types";
|
|
3
9
|
import type { ChartPadding } from "./line";
|
|
@@ -105,6 +111,8 @@ export function buildVolumeGeometry(
|
|
|
105
111
|
bandHeight: number,
|
|
106
112
|
candleWidthSecs: number,
|
|
107
113
|
metrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
114
|
+
/** Internal cached history range. Live volume is always folded in below. */
|
|
115
|
+
history?: VolumeRange,
|
|
108
116
|
): VolumeGeometry {
|
|
109
117
|
"worklet";
|
|
110
118
|
const chartW = canvasW - padding.left - padding.right;
|
|
@@ -126,25 +134,14 @@ export function buildVolumeGeometry(
|
|
|
126
134
|
const padLeft = padding.left;
|
|
127
135
|
const baseline = canvasH - padding.bottom + bandHeight;
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Pass 1 — largest visible volume (the bar that fills the band). The live
|
|
139
|
-
// candle counts too so a tall in-progress bar doesn't overflow the band.
|
|
140
|
-
let maxVol = 0;
|
|
141
|
-
for (let i = lo; i < candles.length; i++) {
|
|
142
|
-
/* istanbul ignore next -- past visible window */
|
|
143
|
-
if (candles[i].time > winEnd) break;
|
|
144
|
-
const v = candles[i].volume ?? 0;
|
|
145
|
-
if (v > maxVol) maxVol = v;
|
|
146
|
-
}
|
|
147
|
-
if (liveCandle && candleVisible(liveCandle, winStart, winEnd, candleWidthSecs)) {
|
|
137
|
+
const lo =
|
|
138
|
+
history?.start ?? volumeStartIndex(candles, winStart, candleWidthSecs);
|
|
139
|
+
const end = history?.end ?? volumeEndIndex(candles, winEnd);
|
|
140
|
+
let maxVol = history?.max ?? visibleMaxVolume(candles, lo, end);
|
|
141
|
+
if (
|
|
142
|
+
liveCandle &&
|
|
143
|
+
candleVisible(liveCandle, winStart, winEnd, candleWidthSecs)
|
|
144
|
+
) {
|
|
148
145
|
const v = liveCandle.volume ?? 0;
|
|
149
146
|
if (v > maxVol) maxVol = v;
|
|
150
147
|
}
|
|
@@ -153,9 +150,7 @@ export function buildVolumeGeometry(
|
|
|
153
150
|
|
|
154
151
|
// Pass 2 — build the bars normalized to maxVol.
|
|
155
152
|
const bars: VolumeBar[] = [];
|
|
156
|
-
for (let i = lo; i <
|
|
157
|
-
/* istanbul ignore next -- past visible window */
|
|
158
|
-
if (candles[i].time > winEnd) break;
|
|
153
|
+
for (let i = lo; i < end; i++) {
|
|
159
154
|
appendVolumeBar(
|
|
160
155
|
candles[i],
|
|
161
156
|
winStart,
|
|
@@ -172,7 +167,10 @@ export function buildVolumeGeometry(
|
|
|
172
167
|
bars,
|
|
173
168
|
);
|
|
174
169
|
}
|
|
175
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
liveCandle &&
|
|
172
|
+
candleVisible(liveCandle, winStart, winEnd, candleWidthSecs)
|
|
173
|
+
) {
|
|
176
174
|
appendVolumeBar(
|
|
177
175
|
liveCandle,
|
|
178
176
|
winStart,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CandlePoint } from "../types";
|
|
2
|
+
|
|
3
|
+
/** First bucket touching the left edge (inclusive). */
|
|
4
|
+
export function volumeStartIndex(
|
|
5
|
+
candles: CandlePoint[],
|
|
6
|
+
start: number,
|
|
7
|
+
width: number,
|
|
8
|
+
): number {
|
|
9
|
+
"worklet";
|
|
10
|
+
let lo = 0,
|
|
11
|
+
hi = candles.length;
|
|
12
|
+
while (lo < hi) {
|
|
13
|
+
const mid = (lo + hi) >> 1;
|
|
14
|
+
if (candles[mid].time + width < start) lo = mid + 1;
|
|
15
|
+
else hi = mid;
|
|
16
|
+
}
|
|
17
|
+
return lo;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** First bucket beyond the right edge (exclusive). */
|
|
21
|
+
export function volumeEndIndex(candles: CandlePoint[], end: number): number {
|
|
22
|
+
"worklet";
|
|
23
|
+
let lo = 0,
|
|
24
|
+
hi = candles.length;
|
|
25
|
+
while (lo < hi) {
|
|
26
|
+
const mid = (lo + hi) >> 1;
|
|
27
|
+
if (candles[mid].time <= end) lo = mid + 1;
|
|
28
|
+
else hi = mid;
|
|
29
|
+
}
|
|
30
|
+
return lo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function visibleMaxVolume(
|
|
34
|
+
candles: CandlePoint[],
|
|
35
|
+
start: number,
|
|
36
|
+
end: number,
|
|
37
|
+
): number {
|
|
38
|
+
"worklet";
|
|
39
|
+
let max = 0;
|
|
40
|
+
for (let i = start; i < end; i++) {
|
|
41
|
+
const volume = candles[i].volume ?? 0;
|
|
42
|
+
if (volume > max) max = volume;
|
|
43
|
+
}
|
|
44
|
+
return max;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface VolumeRange {
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
max: number;
|
|
51
|
+
}
|