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
|
@@ -214,8 +214,8 @@ export function useCrosshair(
|
|
|
214
214
|
// lifecycle helpers are also used by LiveChartSeries and unit-tested directly.
|
|
215
215
|
const fingerDown = useSharedValue(false);
|
|
216
216
|
const panActivated = useSharedValue(false);
|
|
217
|
-
// Where and when the current touch went down, for
|
|
218
|
-
// stale-timer guards (see delayedPanGuard).
|
|
217
|
+
// Where and when the current touch went down, for overlay arbitration and
|
|
218
|
+
// the stationary-hold / stale-timer guards (see delayedPanGuard).
|
|
219
219
|
const downX = useSharedValue(0);
|
|
220
220
|
const downY = useSharedValue(0);
|
|
221
221
|
const downAtMs = useSharedValue(0);
|
|
@@ -661,6 +661,14 @@ export function useCrosshair(
|
|
|
661
661
|
.onTouchesDown(
|
|
662
662
|
/* istanbul ignore next */ (e) => {
|
|
663
663
|
"worklet";
|
|
664
|
+
// Overlay drags decide ownership at touch-down. Preserve that point
|
|
665
|
+
// even without a hold: the finger can enter a grabRange before the
|
|
666
|
+
// scrub reaches its horizontal activation threshold.
|
|
667
|
+
const touch = e.changedTouches[0];
|
|
668
|
+
if (touch) {
|
|
669
|
+
downX.set(touch.x);
|
|
670
|
+
downY.set(touch.y);
|
|
671
|
+
}
|
|
664
672
|
delayedPanTouchDown(
|
|
665
673
|
longPressMs,
|
|
666
674
|
e,
|
|
@@ -751,7 +759,11 @@ export function useCrosshair(
|
|
|
751
759
|
// overlay tap. `scrubActive` is only set here, so bailing in `onStart`
|
|
752
760
|
// also keeps a follow-on drag from showing a crosshair — no `onUpdate`
|
|
753
761
|
// guard needed. (Plain-scrub counterpart of the scrub-action tap defer.)
|
|
754
|
-
if (
|
|
762
|
+
if (
|
|
763
|
+
deferTapHit !== undefined &&
|
|
764
|
+
deferTapHit(downX.get(), downY.get())
|
|
765
|
+
)
|
|
766
|
+
return;
|
|
755
767
|
startPlainScrub(
|
|
756
768
|
snapCandleX(e.x),
|
|
757
769
|
padding,
|
package/src/hooks/useDegen.ts
CHANGED
|
@@ -50,6 +50,7 @@ export function useDegen(
|
|
|
50
50
|
): {
|
|
51
51
|
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
52
52
|
packRevision: SharedValue<number>;
|
|
53
|
+
particleTimestamp: SharedValue<number>;
|
|
53
54
|
shakeTransform: DerivedValue<
|
|
54
55
|
[{ translateX: number }, { translateY: number }]
|
|
55
56
|
>;
|
|
@@ -57,6 +58,7 @@ export function useDegen(
|
|
|
57
58
|
const MAX_SLOTS = 80;
|
|
58
59
|
const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
|
|
59
60
|
const packRevision = useSharedValue(0);
|
|
61
|
+
const particleTimestamp = useSharedValue(0);
|
|
60
62
|
const prevM = useSharedValue<Momentum>("flat");
|
|
61
63
|
const writeRot = useSharedValue(0);
|
|
62
64
|
const enabledSV = useSharedValue(0);
|
|
@@ -185,10 +187,12 @@ export function useDegen(
|
|
|
185
187
|
// ChartWithDegen only mounts while the resolved effect is enabled; static
|
|
186
188
|
// charts force that config to null and unmount this callback entirely.
|
|
187
189
|
useFrameCallback(
|
|
188
|
-
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
190
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (frame) => {
|
|
189
191
|
"worklet";
|
|
190
192
|
if (isFrameLoopActive?.get() === false) return;
|
|
191
|
-
|
|
193
|
+
// The viewport clock advances in half-pixel steps and may be pinned by
|
|
194
|
+
// nowOverride or pan. Effects need the continuous display-frame clock.
|
|
195
|
+
const now = frame.timestamp / 1000;
|
|
192
196
|
const buf = pack.get();
|
|
193
197
|
const slots = slotCountSV.get();
|
|
194
198
|
|
|
@@ -273,10 +277,10 @@ export function useDegen(
|
|
|
273
277
|
);
|
|
274
278
|
|
|
275
279
|
// Repaint only while particles are alive (or on the frame they all expire,
|
|
276
|
-
// so the overlay clears).
|
|
277
|
-
//
|
|
278
|
-
// worklet churn for an idle particle system.
|
|
280
|
+
// so the overlay clears). Publish the same clock for Atlas size/opacity,
|
|
281
|
+
// only while particles are alive, to keep an idle field from repainting.
|
|
279
282
|
if (activeCount > 0 || prevActiveCount.get() > 0) {
|
|
283
|
+
particleTimestamp.set(now);
|
|
280
284
|
packRevision.set(packRevision.get() + 1);
|
|
281
285
|
}
|
|
282
286
|
prevActiveCount.set(activeCount);
|
|
@@ -291,5 +295,5 @@ export function useDegen(
|
|
|
291
295
|
];
|
|
292
296
|
});
|
|
293
297
|
|
|
294
|
-
return { pack, packRevision, shakeTransform };
|
|
298
|
+
return { pack, packRevision, particleTimestamp, shakeTransform };
|
|
295
299
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
|
3
|
+
import { scheduleOnUI } from "react-native-worklets";
|
|
4
|
+
|
|
5
|
+
/** Wait through several unchanged frames, rather than mistaking one quiet frame for rest. */
|
|
6
|
+
export const IDLE_SETTLE_MS = 160;
|
|
7
|
+
|
|
8
|
+
function noInputs() { "worklet"; return null; }
|
|
9
|
+
function noReaction() { "worklet"; }
|
|
10
|
+
|
|
11
|
+
export function nextQuietTime(previous: number, changed: boolean, dt: number): number {
|
|
12
|
+
"worklet";
|
|
13
|
+
return changed ? 0 : previous + Math.min(Math.max(dt, 0), 34);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Opt-in UI-runtime RAF loop. Inputs wake it without a JS-thread round trip.
|
|
18
|
+
* `tick` returns true while work changed or a continuous effect needs frames.
|
|
19
|
+
* `prepare` must read INPUTS only: subscribing to tick outputs causes feedback.
|
|
20
|
+
* A generation token makes queued frames harmless after replacement/unmount.
|
|
21
|
+
*/
|
|
22
|
+
export function useDemandFrameLoop(
|
|
23
|
+
enabled: boolean,
|
|
24
|
+
prepare: () => unknown,
|
|
25
|
+
tick: (dt: number) => boolean,
|
|
26
|
+
): () => void {
|
|
27
|
+
const mounted = useSharedValue(false);
|
|
28
|
+
const running = useSharedValue(false);
|
|
29
|
+
const generation = useSharedValue(0);
|
|
30
|
+
const quietTime = useSharedValue(0);
|
|
31
|
+
|
|
32
|
+
const wake = useCallback(() => {
|
|
33
|
+
"worklet";
|
|
34
|
+
if (!enabled || !mounted.get()) return;
|
|
35
|
+
quietTime.set(0);
|
|
36
|
+
if (running.get()) return;
|
|
37
|
+
running.set(true);
|
|
38
|
+
const token = generation.get();
|
|
39
|
+
let previous: number | null = null;
|
|
40
|
+
function step(timestamp: number) {
|
|
41
|
+
"worklet";
|
|
42
|
+
if (!mounted.get() || generation.get() !== token) return;
|
|
43
|
+
// Resuming never integrates the entire sleeping interval as a single frame.
|
|
44
|
+
const dt = previous === null ? 1000 / 60 : Math.min(timestamp - previous, 34);
|
|
45
|
+
previous = timestamp;
|
|
46
|
+
const quiet = nextQuietTime(quietTime.get(), tick(dt), dt);
|
|
47
|
+
quietTime.set(quiet);
|
|
48
|
+
if (quiet < IDLE_SETTLE_MS) requestAnimationFrame(step);
|
|
49
|
+
else running.set(false);
|
|
50
|
+
}
|
|
51
|
+
requestAnimationFrame(step);
|
|
52
|
+
}, [enabled, mounted, running, generation, quietTime, tick]);
|
|
53
|
+
|
|
54
|
+
useAnimatedReaction(
|
|
55
|
+
// Pass the worklet itself: Reanimated extracts SharedValue subscriptions
|
|
56
|
+
// from its closure. Wrapping it in () => prepare() hides those inputs.
|
|
57
|
+
enabled ? prepare : noInputs,
|
|
58
|
+
enabled ? wake : noReaction,
|
|
59
|
+
[enabled, enabled ? prepare : noInputs, enabled ? wake : noReaction],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!enabled) return;
|
|
64
|
+
scheduleOnUI(() => {
|
|
65
|
+
"worklet";
|
|
66
|
+
generation.set(generation.get() + 1);
|
|
67
|
+
running.set(false);
|
|
68
|
+
mounted.set(true);
|
|
69
|
+
wake();
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
scheduleOnUI(() => {
|
|
73
|
+
"worklet";
|
|
74
|
+
mounted.set(false);
|
|
75
|
+
generation.set(generation.get() + 1);
|
|
76
|
+
running.set(false);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}, [enabled, generation, mounted, running, wake]);
|
|
80
|
+
|
|
81
|
+
return wake;
|
|
82
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ResolvedLoadingConfig } from "../core/resolveConfig";
|
|
4
|
+
|
|
5
|
+
/** Field by field, with `Object.is` (a `NaN` must equal itself, or the render
|
|
6
|
+
* below would never settle), over every field the config has. */
|
|
7
|
+
function sameLook(a: ResolvedLoadingConfig, b: ResolvedLoadingConfig | null) {
|
|
8
|
+
if (b === null) return false;
|
|
9
|
+
const keys = Object.keys(a) as (keyof ResolvedLoadingConfig)[];
|
|
10
|
+
return keys.every((key) => Object.is(a[key], b[key]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The loading shell's look: the live `loading` config while loading, and the
|
|
15
|
+
* last one after. The shell fades out and the reveal morph melts its squiggle
|
|
16
|
+
* into the line only once `loading` has turned off, when `resolveLoading`
|
|
17
|
+
* already returns `null` — so they would otherwise snap to the default color,
|
|
18
|
+
* stroke, wave and Y-axis placeholders just as the data arrives. The empty
|
|
19
|
+
* shell ("No data") shown later keeps it too; before any loading, it has none
|
|
20
|
+
* and the defaults apply.
|
|
21
|
+
*/
|
|
22
|
+
export function useLoadingLook(
|
|
23
|
+
loadingCfg: ResolvedLoadingConfig | null,
|
|
24
|
+
): ResolvedLoadingConfig | null {
|
|
25
|
+
const [last, setLast] = useState(loadingCfg);
|
|
26
|
+
// Stored from render (React's "previous props" pattern), compared by value:
|
|
27
|
+
// `resolveLoading` returns a fresh object for an inline config every render.
|
|
28
|
+
if (loadingCfg !== null && !sameLook(loadingCfg, last)) setLast(loadingCfg);
|
|
29
|
+
return loadingCfg ?? last;
|
|
30
|
+
}
|
package/src/hooks/useMarkers.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import { Gesture } from "react-native-gesture-handler";
|
|
3
3
|
import {
|
|
4
|
+
useAnimatedReaction,
|
|
4
5
|
useFrameCallback,
|
|
5
6
|
useSharedValue,
|
|
6
7
|
type SharedValue,
|
|
@@ -57,6 +58,8 @@ export function useMarkers(
|
|
|
57
58
|
cluster: ResolvedMarkerCluster = ANCHORED_CLUSTER,
|
|
58
59
|
/** Runtime gate shared with the chart engine. */
|
|
59
60
|
isFrameLoopActive?: SharedValue<boolean>,
|
|
61
|
+
/** Experimental input-driven projection instead of continuous frames; defaults to false. */
|
|
62
|
+
autoSleep = false,
|
|
60
63
|
): {
|
|
61
64
|
projected: SharedValue<ProjectedMarker[]>;
|
|
62
65
|
tapGesture: ReturnType<typeof Gesture.Tap>;
|
|
@@ -79,7 +82,7 @@ export function useMarkers(
|
|
|
79
82
|
onMarkerPress?.(event);
|
|
80
83
|
};
|
|
81
84
|
|
|
82
|
-
const
|
|
85
|
+
const project =
|
|
83
86
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
84
87
|
"worklet";
|
|
85
88
|
if (isFrameLoopActive?.get() === false) return;
|
|
@@ -111,13 +114,24 @@ export function useMarkers(
|
|
|
111
114
|
maxY: engine.canvasHeight.get(),
|
|
112
115
|
});
|
|
113
116
|
projected.set(buf);
|
|
117
|
+
};
|
|
118
|
+
const legacyActive = autostart && !autoSleep;
|
|
119
|
+
const markerFrameCallback = useFrameCallback(project, legacyActive);
|
|
120
|
+
useAnimatedReaction(
|
|
121
|
+
() => {
|
|
122
|
+
if (!autoSleep || !autostart) return null;
|
|
123
|
+
return [active, markers.get(), engine.canvasWidth.get(), engine.canvasHeight.get(),
|
|
124
|
+
engine.timestamp.get(), engine.displayWindow.get(), engine.displayMin.get(),
|
|
125
|
+
engine.displayMax.get(), seriesSV?.get(), lineData?.get(), isFrameLoopActive?.get()];
|
|
126
|
+
},
|
|
127
|
+
(inputs) => {
|
|
128
|
+
if (inputs !== null) project();
|
|
114
129
|
},
|
|
115
|
-
autostart,
|
|
116
130
|
);
|
|
117
131
|
useEffect(() => {
|
|
118
132
|
// Reanimated does not reactively apply later `autostart` values.
|
|
119
|
-
markerFrameCallback.setActive(
|
|
120
|
-
}, [
|
|
133
|
+
markerFrameCallback.setActive(legacyActive);
|
|
134
|
+
}, [legacyActive, markerFrameCallback]);
|
|
121
135
|
|
|
122
136
|
const tapGesture = Gesture.Tap().onEnd(
|
|
123
137
|
/* istanbul ignore next -- gesture worklet runs on UI thread, not in Jest */ (
|
|
@@ -30,6 +30,7 @@ export function useMultiSeriesDegen(
|
|
|
30
30
|
): {
|
|
31
31
|
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
32
32
|
packRevision: SharedValue<number>;
|
|
33
|
+
particleTimestamp: SharedValue<number>;
|
|
33
34
|
shakeTransform: DerivedValue<
|
|
34
35
|
[{ translateX: number }, { translateY: number }]
|
|
35
36
|
>;
|
|
@@ -37,6 +38,7 @@ export function useMultiSeriesDegen(
|
|
|
37
38
|
const MAX_SLOTS = 80;
|
|
38
39
|
const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
|
|
39
40
|
const packRevision = useSharedValue(0);
|
|
41
|
+
const particleTimestamp = useSharedValue(0);
|
|
40
42
|
// Per-series previous momentum (0 = flat, 1 = up, 2 = down), index-aligned to series.
|
|
41
43
|
const prevMoms = useSharedValue<number[]>([]);
|
|
42
44
|
const writeRot = useSharedValue(0);
|
|
@@ -131,9 +133,10 @@ export function useMultiSeriesDegen(
|
|
|
131
133
|
]);
|
|
132
134
|
|
|
133
135
|
useFrameCallback(
|
|
134
|
-
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
136
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (frame) => {
|
|
135
137
|
"worklet";
|
|
136
|
-
|
|
138
|
+
// Effect timing is independent of the quantized/pinned viewport clock.
|
|
139
|
+
const now = frame.timestamp / 1000;
|
|
137
140
|
const buf = pack.get();
|
|
138
141
|
const slots = slotCountSV.get();
|
|
139
142
|
|
|
@@ -242,6 +245,7 @@ export function useMultiSeriesDegen(
|
|
|
242
245
|
);
|
|
243
246
|
|
|
244
247
|
if (activeCount > 0 || prevActiveCount.get() > 0) {
|
|
248
|
+
particleTimestamp.set(now);
|
|
245
249
|
packRevision.set(packRevision.get() + 1);
|
|
246
250
|
}
|
|
247
251
|
prevActiveCount.set(activeCount);
|
|
@@ -256,5 +260,5 @@ export function useMultiSeriesDegen(
|
|
|
256
260
|
];
|
|
257
261
|
});
|
|
258
262
|
|
|
259
|
-
return { pack, packRevision, shakeTransform };
|
|
263
|
+
return { pack, packRevision, particleTimestamp, shakeTransform };
|
|
260
264
|
}
|
|
@@ -22,6 +22,8 @@ export type PanScrollGestureMode = "holdToScrub" | "axisDrag";
|
|
|
22
22
|
|
|
23
23
|
/** Engine SharedValues the pan-scroll gesture reads/writes (subset of the engine state). */
|
|
24
24
|
export interface PanScrollEngineRefs {
|
|
25
|
+
/** Wake a demand-driven engine before reading the gesture bounds. */
|
|
26
|
+
wake?: () => void;
|
|
25
27
|
/** Absolute right-edge time to freeze at, or `null` to follow the live edge. */
|
|
26
28
|
viewEnd: SharedValue<number | null>;
|
|
27
29
|
/** Right-edge time the engine would use if following live (advances each frame). */
|
|
@@ -263,10 +265,13 @@ export function usePanScroll({
|
|
|
263
265
|
},
|
|
264
266
|
);
|
|
265
267
|
|
|
268
|
+
const wakeEngine = engine.wake;
|
|
269
|
+
|
|
266
270
|
const onStart =
|
|
267
271
|
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
268
272
|
() => {
|
|
269
273
|
"worklet";
|
|
274
|
+
wakeEngine?.();
|
|
270
275
|
// Stop an in-flight fling FIRST. `withDecay` keeps writing `viewEnd` after
|
|
271
276
|
// the finger lifts, and `Gesture.Race` declares no relation between the two
|
|
272
277
|
// pans, so this can run while a scrub is already engaged. Bailing out below
|
|
@@ -10,6 +10,8 @@ import { FOLLOW_SNAP, panLowerBound, panUpperBound } from "./usePanScroll";
|
|
|
10
10
|
|
|
11
11
|
/** Engine SharedValues the pinch-zoom gesture reads/writes. */
|
|
12
12
|
export interface PinchZoomEngineRefs {
|
|
13
|
+
/** Wake a demand-driven engine before reading the gesture bounds. */
|
|
14
|
+
wake?: () => void;
|
|
13
15
|
/** Visible-window width override in seconds, or `null` to follow `timeWindow`. */
|
|
14
16
|
viewWindow: SharedValue<number | null>;
|
|
15
17
|
/** Absolute right-edge time, or `null` to follow the live edge (shared with pan). */
|
|
@@ -170,10 +172,13 @@ export function usePinchZoom({
|
|
|
170
172
|
const startWindow = useSharedValue(timeWindow);
|
|
171
173
|
const startViewEnd = useSharedValue(0);
|
|
172
174
|
|
|
175
|
+
const wakeEngine = engine.wake;
|
|
176
|
+
|
|
173
177
|
const onStart =
|
|
174
178
|
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
175
179
|
() => {
|
|
176
180
|
"worklet";
|
|
181
|
+
wakeEngine?.();
|
|
177
182
|
cancelAnimation(viewEnd);
|
|
178
183
|
cancelAnimation(viewWindow);
|
|
179
184
|
const edge = liveEdge.get();
|
|
@@ -18,11 +18,7 @@ import {
|
|
|
18
18
|
} from "../math/referenceDrag";
|
|
19
19
|
import { referenceLineForm } from "../math/referenceLines";
|
|
20
20
|
import type { ReferenceLine } from "../types";
|
|
21
|
-
import {
|
|
22
|
-
computeScrubDotY,
|
|
23
|
-
computeValueAtY,
|
|
24
|
-
snapPrice,
|
|
25
|
-
} from "./crosshairShared";
|
|
21
|
+
import { computeValueAtY, pinnedPlotY, snapPrice } from "./crosshairShared";
|
|
26
22
|
|
|
27
23
|
/** Vertical reach (px) around a line within which a touch grabs it. */
|
|
28
24
|
const GRAB_SLOP = 14;
|
|
@@ -37,12 +33,13 @@ const EMPTY: never[] = [];
|
|
|
37
33
|
* Builds the per-line **drag** gesture for draggable Form-A reference lines: grab a
|
|
38
34
|
* line near its value-Y and drag vertically to set a new value, with optional
|
|
39
35
|
* `snap` + `bounds` clamp. Mirrors the order-ticket reticle in {@link useCrosshair}
|
|
40
|
-
* (value↔Y via `computeValueAtY` / `
|
|
36
|
+
* (value↔Y via `computeValueAtY` / `pinnedPlotY`, frozen value re-projected
|
|
41
37
|
* each frame) but per line, writing into the shared `dragValues` array the layout
|
|
42
38
|
* and overlays read.
|
|
43
39
|
*
|
|
44
40
|
* The pan uses `manualActivation`: it grabs only when a touch starts within
|
|
45
|
-
* {@link GRAB_SLOP} of a draggable line
|
|
41
|
+
* {@link GRAB_SLOP} of a draggable line (and inside its `grabRange`, when it has
|
|
42
|
+
* one), then **owns** that touch — any drag past
|
|
46
43
|
* {@link DRAG_ACTIVATE_PX} (in either axis) drags the line. A touch off every line
|
|
47
44
|
* fails fast so the chart's other gestures (scrub / scroll) run everywhere else
|
|
48
45
|
* (compose this ahead of them via `Gesture.Exclusive`). Crucially it no longer
|
|
@@ -86,6 +83,7 @@ export function useReferenceDrag(
|
|
|
86
83
|
const dMax = engine.displayMax.get();
|
|
87
84
|
const top = padding.top;
|
|
88
85
|
const bottom = ch - padding.bottom;
|
|
86
|
+
if (bottom <= top) return EMPTY;
|
|
89
87
|
const out: number[] = [];
|
|
90
88
|
for (let i = 0; i < lines.length; i++) {
|
|
91
89
|
const l = lines[i];
|
|
@@ -98,12 +96,16 @@ export function useReferenceDrag(
|
|
|
98
96
|
continue;
|
|
99
97
|
}
|
|
100
98
|
const v = dragValues.get()[i] ?? l.value;
|
|
101
|
-
|
|
102
|
-
out.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
99
|
+
out.push(pinnedPlotY(v, dMin, dMax, ch, top, padding.bottom));
|
|
103
100
|
}
|
|
104
101
|
return out;
|
|
105
102
|
});
|
|
106
103
|
|
|
104
|
+
// Per-line grab x-ranges (`ReferenceLine.grabRange`), index-aligned with
|
|
105
|
+
// `lines`; null = anywhere along the line. Read by the gesture worklets below,
|
|
106
|
+
// which close over this render's `lines` as the callbacks do.
|
|
107
|
+
const grabRanges = lines.map((l) => l.grabRange ?? null);
|
|
108
|
+
|
|
107
109
|
const dragIndex = useSharedValue(-1);
|
|
108
110
|
const startX = useSharedValue(0);
|
|
109
111
|
const startY = useSharedValue(0);
|
|
@@ -175,7 +177,10 @@ export function useReferenceDrag(
|
|
|
175
177
|
"worklet";
|
|
176
178
|
const t = e.changedTouches[0];
|
|
177
179
|
if (!t) return;
|
|
178
|
-
const i = nearestDraggableIndex(handleYs.get(), t.y, GRAB_SLOP
|
|
180
|
+
const i = nearestDraggableIndex(handleYs.get(), t.y, GRAB_SLOP, {
|
|
181
|
+
x: t.x,
|
|
182
|
+
ranges: grabRanges,
|
|
183
|
+
});
|
|
179
184
|
if (i < 0) {
|
|
180
185
|
manager.fail();
|
|
181
186
|
return;
|
|
@@ -266,13 +271,22 @@ export function useReferenceDrag(
|
|
|
266
271
|
// is grabbed the answer is yes regardless of position: the scrub asks with its
|
|
267
272
|
// touch-DOWN point, which a longer drag has carried the line away from, so the
|
|
268
273
|
// geometric test alone said "no line here" and a crosshair opened mid-drag.
|
|
269
|
-
// Otherwise it is the y-reach around the handles
|
|
270
|
-
//
|
|
274
|
+
// Otherwise it is the y-reach around the handles, along the whole line — or,
|
|
275
|
+
// for a line with a `grabRange`, only inside it.
|
|
271
276
|
/* istanbul ignore next -- worklet, runs on the UI thread */
|
|
272
|
-
const hitTest = (
|
|
277
|
+
const hitTest = (x: number, y: number): boolean => {
|
|
273
278
|
"worklet";
|
|
274
279
|
if (!anyDraggable) return false;
|
|
275
|
-
return referenceDragOwnsTouch(
|
|
280
|
+
return referenceDragOwnsTouch(
|
|
281
|
+
dragIndex.get(),
|
|
282
|
+
handleYs.get(),
|
|
283
|
+
y,
|
|
284
|
+
GRAB_SLOP,
|
|
285
|
+
{
|
|
286
|
+
x,
|
|
287
|
+
ranges: grabRanges,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
276
290
|
};
|
|
277
291
|
|
|
278
292
|
const gesture = Gesture.Pan()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
4
|
+
import type { ChartPadding } from "../draw/line";
|
|
5
|
+
import { groupReferenceLines, type ReferenceGrouping } from "../math/referenceGroup";
|
|
6
|
+
import { referenceLineForm } from "../math/referenceLines";
|
|
7
|
+
import type { ReferenceLine } from "../types";
|
|
8
|
+
import { pinnedPlotY } from "./crosshairShared";
|
|
9
|
+
|
|
10
|
+
/** Stable result while grouping is disabled. */
|
|
11
|
+
const EMPTY_GROUPING: ReferenceGrouping = { hidden: [], groups: [] };
|
|
12
|
+
|
|
13
|
+
/** Cluster the built-in tags that can actually render in the current plot. */
|
|
14
|
+
export function useReferenceLineGrouping({
|
|
15
|
+
radius,
|
|
16
|
+
engine,
|
|
17
|
+
padding,
|
|
18
|
+
lines,
|
|
19
|
+
custom,
|
|
20
|
+
offAxisCustom,
|
|
21
|
+
dragValues,
|
|
22
|
+
}: {
|
|
23
|
+
radius: number | null;
|
|
24
|
+
engine: ChartEngineLayout;
|
|
25
|
+
padding: ChartPadding;
|
|
26
|
+
lines: ReferenceLine[];
|
|
27
|
+
custom: boolean[];
|
|
28
|
+
offAxisCustom: boolean[];
|
|
29
|
+
dragValues: SharedValue<number[]>;
|
|
30
|
+
}) {
|
|
31
|
+
const hasOffAxisBadge = lines.map((line) => Boolean(line.badge || line.offAxisBadge));
|
|
32
|
+
const result = useDerivedValue<ReferenceGrouping>(() => {
|
|
33
|
+
if (radius == null) return EMPTY_GROUPING;
|
|
34
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
35
|
+
const displayMin = engine.displayMin.get();
|
|
36
|
+
const displayMax = engine.displayMax.get();
|
|
37
|
+
const top = padding.top;
|
|
38
|
+
const yPositions: number[] = [];
|
|
39
|
+
for (let index = 0; index < lines.length; index++) {
|
|
40
|
+
const line = lines[index];
|
|
41
|
+
if (
|
|
42
|
+
referenceLineForm(line) !== "line" ||
|
|
43
|
+
line.value === undefined ||
|
|
44
|
+
custom[index] ||
|
|
45
|
+
offAxisCustom[index]
|
|
46
|
+
) {
|
|
47
|
+
yPositions.push(-1);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const value = dragValues.get()[index] ?? line.value;
|
|
51
|
+
// Plain off-axis lines are culled by useReferenceLine. Only a configured
|
|
52
|
+
// badge can contribute a visible tag to the edge's count pill.
|
|
53
|
+
if ((value < displayMin || value > displayMax) && !hasOffAxisBadge[index]) {
|
|
54
|
+
yPositions.push(-1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
yPositions.push(
|
|
58
|
+
pinnedPlotY(
|
|
59
|
+
value,
|
|
60
|
+
displayMin,
|
|
61
|
+
displayMax,
|
|
62
|
+
canvasHeight,
|
|
63
|
+
top,
|
|
64
|
+
padding.bottom,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return groupReferenceLines(yPositions, radius);
|
|
69
|
+
});
|
|
70
|
+
const hidden = useDerivedValue<boolean[]>(() => result.get().hidden);
|
|
71
|
+
return { refGroupResult: result, groupHidden: hidden };
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { CandlePoint } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
visibleMaxVolume,
|
|
5
|
+
volumeEndIndex,
|
|
6
|
+
volumeStartIndex,
|
|
7
|
+
} from "../draw/volumeRange";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Primitive index outputs suppress downstream scans while the window glides
|
|
11
|
+
* within the same buckets. The maximum also depends directly on candles, so
|
|
12
|
+
* every notified data revision (including same-length replacement or modify)
|
|
13
|
+
* invalidates it. Array length and object identity are never cache keys.
|
|
14
|
+
*/
|
|
15
|
+
export function useVolumeRange(
|
|
16
|
+
candles: SharedValue<CandlePoint[]> | undefined,
|
|
17
|
+
timestamp: SharedValue<number>,
|
|
18
|
+
window: SharedValue<number>,
|
|
19
|
+
width: SharedValue<number>,
|
|
20
|
+
active: boolean,
|
|
21
|
+
) {
|
|
22
|
+
const start = useDerivedValue(() =>
|
|
23
|
+
!active || !candles
|
|
24
|
+
? 0
|
|
25
|
+
: volumeStartIndex(
|
|
26
|
+
candles.get(),
|
|
27
|
+
timestamp.get() - window.get(),
|
|
28
|
+
width.get(),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
const end = useDerivedValue(() =>
|
|
32
|
+
!active || !candles ? 0 : volumeEndIndex(candles.get(), timestamp.get()),
|
|
33
|
+
);
|
|
34
|
+
const max = useDerivedValue(() =>
|
|
35
|
+
!active || !candles
|
|
36
|
+
? 0
|
|
37
|
+
: visibleMaxVolume(candles.get(), start.get(), end.get()),
|
|
38
|
+
);
|
|
39
|
+
return { start, end, max };
|
|
40
|
+
}
|
package/src/hooks/useXAxis.ts
CHANGED
|
@@ -167,11 +167,16 @@ export function useXAxis(
|
|
|
167
167
|
const edgeAlpha =
|
|
168
168
|
fromEdge >= fadeZone ? 1 : fromEdge <= 0 ? 0 : fromEdge / fadeZone;
|
|
169
169
|
|
|
170
|
-
const
|
|
170
|
+
const isTarget = xAxisKeyIsTarget(key, targetKeys);
|
|
171
|
+
const target = isTarget ? edgeAlpha : 0;
|
|
171
172
|
let next = lerp(label.alpha, target, FADE, MS_PER_FRAME_60FPS);
|
|
172
173
|
if (Math.abs(next - target) < 0.02) next = target;
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
// Only a key that left the target set is deleted once it has faded out. A
|
|
176
|
+
// target key can sit at alpha 0 (the set spans one interval past each edge,
|
|
177
|
+
// outside the plot); deleting it would re-create it on the next run and
|
|
178
|
+
// write `labelAlphas`, this mapper's own input, every frame.
|
|
179
|
+
if (next < 0.01 && !isTarget) {
|
|
175
180
|
delete alphas[key];
|
|
176
181
|
cacheChanged = true;
|
|
177
182
|
} else {
|
|
@@ -182,7 +187,12 @@ export function useXAxis(
|
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
|
|
190
|
+
// Publish only a changed final cache so this derived value cannot keep
|
|
191
|
+
// waking itself after the engine sleeps. Off-screen target keys are retained
|
|
192
|
+
// above; this equality check also guards against other no-op cache updates.
|
|
193
|
+
if (cacheChanged && !sameXAxisLabels(previousAlphas, alphas)) {
|
|
194
|
+
labelAlphas.set(alphas);
|
|
195
|
+
}
|
|
186
196
|
|
|
187
197
|
// Collect visible labels
|
|
188
198
|
const raw: XAxisEntry[] = [];
|
|
@@ -226,3 +236,17 @@ export function useXAxis(
|
|
|
226
236
|
|
|
227
237
|
return { xAxisEntries, font };
|
|
228
238
|
}
|
|
239
|
+
|
|
240
|
+
export function sameXAxisLabels(
|
|
241
|
+
previous: Record<number, { alpha: number; text: string }>,
|
|
242
|
+
next: Record<number, { alpha: number; text: string }>,
|
|
243
|
+
): boolean {
|
|
244
|
+
"worklet";
|
|
245
|
+
const keys = Object.keys(next);
|
|
246
|
+
if (keys.length !== Object.keys(previous).length) return false;
|
|
247
|
+
for (let i = 0; i < keys.length; i++) {
|
|
248
|
+
const key = Number(keys[i]);
|
|
249
|
+
if (!previous[key] || previous[key].alpha !== next[key].alpha || previous[key].text !== next[key].text) return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
package/src/hooks/useYAxis.ts
CHANGED
|
@@ -28,7 +28,15 @@ export function useYAxis(
|
|
|
28
28
|
const yAxisEntries = useDerivedValue(() => {
|
|
29
29
|
const dt = MS_PER_FRAME_60FPS;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// SharedValue payloads may be frozen after crossing the JS/UI boundary.
|
|
32
|
+
// Copy before computeGridEntries adds, updates, or removes label keys.
|
|
33
|
+
const previousAlphas = labelAlphas.get();
|
|
34
|
+
const alphas: Record<number, number> = {};
|
|
35
|
+
const previousKeys = Object.keys(previousAlphas);
|
|
36
|
+
for (let i = 0; i < previousKeys.length; i++) {
|
|
37
|
+
const key = Number(previousKeys[i]);
|
|
38
|
+
alphas[key] = previousAlphas[key];
|
|
39
|
+
}
|
|
32
40
|
const result = computeGridEntries(
|
|
33
41
|
engine.displayMin.get(),
|
|
34
42
|
engine.displayMax.get(),
|
|
@@ -46,7 +54,15 @@ export function useYAxis(
|
|
|
46
54
|
);
|
|
47
55
|
|
|
48
56
|
prevInterval.set(result.interval);
|
|
49
|
-
|
|
57
|
+
// This derived value also reads the cache. Only publish actual changes so
|
|
58
|
+
// a settled axis doesn't keep scheduling itself with fresh object identities.
|
|
59
|
+
const nextKeys = Object.keys(alphas);
|
|
60
|
+
let cacheChanged = nextKeys.length !== previousKeys.length;
|
|
61
|
+
for (let i = 0; i < nextKeys.length && !cacheChanged; i++) {
|
|
62
|
+
const key = Number(nextKeys[i]);
|
|
63
|
+
if (alphas[key] !== previousAlphas[key]) cacheChanged = true;
|
|
64
|
+
}
|
|
65
|
+
if (cacheChanged) labelAlphas.set(alphas);
|
|
50
66
|
|
|
51
67
|
return result.entries;
|
|
52
68
|
});
|