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
|
@@ -4,7 +4,13 @@ import {
|
|
|
4
4
|
Path,
|
|
5
5
|
type SkFont,
|
|
6
6
|
} from "@shopify/react-native-skia";
|
|
7
|
-
import {
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import {
|
|
9
|
+
useAnimatedReaction,
|
|
10
|
+
useDerivedValue,
|
|
11
|
+
type SharedValue,
|
|
12
|
+
} from "react-native-reanimated";
|
|
13
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
8
14
|
import { BADGE_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
|
|
9
15
|
import type { ResolvedGridStyleConfig } from "../core/resolveConfig";
|
|
10
16
|
import {
|
|
@@ -63,6 +69,7 @@ export function YAxisOverlay({
|
|
|
63
69
|
side = "right",
|
|
64
70
|
labelRightMargin,
|
|
65
71
|
gridEndGap = 0,
|
|
72
|
+
minGap = 36,
|
|
66
73
|
}: {
|
|
67
74
|
entries: SharedValue<YAxisEntry[]>;
|
|
68
75
|
engine: ChartEngineLayout;
|
|
@@ -104,6 +111,8 @@ export function YAxisOverlay({
|
|
|
104
111
|
labelRightMargin?: number;
|
|
105
112
|
/** Gap between the grid-line end and the shared label column. */
|
|
106
113
|
gridEndGap?: number;
|
|
114
|
+
/** The grid's `minGap`: bounds how many labelled lines the plot can hold. */
|
|
115
|
+
minGap?: number;
|
|
107
116
|
}) {
|
|
108
117
|
const gridColor = gridStyle?.color ?? palette.gridLine;
|
|
109
118
|
const gridWidth = gridStyle?.strokeWidth ?? 1;
|
|
@@ -190,6 +199,33 @@ export function YAxisOverlay({
|
|
|
190
199
|
return result;
|
|
191
200
|
});
|
|
192
201
|
|
|
202
|
+
// The label pool starts at MAX_Y_LABELS and grows to the number of labels the
|
|
203
|
+
// grid produces. A tall plot (or a small `minGap`) carries more labelled grid
|
|
204
|
+
// lines than a fixed pool held, and the extra lines were drawn without labels —
|
|
205
|
+
// which ones depended on key order. It grows no further than a settled grid
|
|
206
|
+
// can need: labelled lines stay at least half a `minGap` apart (the grid's
|
|
207
|
+
// hysteresis floor). Lines still fading out after a zoom can briefly outnumber
|
|
208
|
+
// that; for those frames some lines go unlabelled, as before. The pool never
|
|
209
|
+
// shrinks: a chart that fits keeps its cost, and zooming back and forth
|
|
210
|
+
// doesn't remount labels.
|
|
211
|
+
const [labelSlots, setLabelSlots] = useState(MAX_Y_LABELS);
|
|
212
|
+
const growLabelSlots = (slots: number) =>
|
|
213
|
+
setLabelSlots((current) => Math.max(current, slots));
|
|
214
|
+
useAnimatedReaction(
|
|
215
|
+
() => {
|
|
216
|
+
const plotHeight =
|
|
217
|
+
engine.canvasHeight.get() - padding.top - padding.bottom;
|
|
218
|
+
const settledMax = Math.floor(plotHeight / (minGap / 2)) + 1;
|
|
219
|
+
return Math.min(labelEntries.get().length, settledMax);
|
|
220
|
+
},
|
|
221
|
+
// Runs on every pass of the mapper: ask once per new size.
|
|
222
|
+
(slots, previous) => {
|
|
223
|
+
if (slots > labelSlots && slots !== previous) {
|
|
224
|
+
scheduleOnRN(growLabelSlots, slots);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
193
229
|
return (
|
|
194
230
|
<Group>
|
|
195
231
|
{variant !== "labels" && (
|
|
@@ -207,7 +243,7 @@ export function YAxisOverlay({
|
|
|
207
243
|
</Group>
|
|
208
244
|
)}
|
|
209
245
|
{variant !== "grid" &&
|
|
210
|
-
Array.from({ length:
|
|
246
|
+
Array.from({ length: labelSlots }, (_, i) => (
|
|
211
247
|
<AnimatedLabel
|
|
212
248
|
key={i}
|
|
213
249
|
entries={labelEntries}
|
package/src/constants.ts
CHANGED
|
@@ -10,9 +10,9 @@ import type {
|
|
|
10
10
|
export const MS_PER_FRAME_60FPS = 16.67;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* fixed-`count` mode (see {@link YAxisConfig.count}).
|
|
13
|
+
* Initial size of the Y-axis label pool. The pool grows when the dynamic
|
|
14
|
+
* (nice-interval) grid produces more labels (a tall plot or a small `minGap`);
|
|
15
|
+
* the fixed-`count` mode stays capped at this (see {@link YAxisConfig.count}).
|
|
16
16
|
*/
|
|
17
17
|
export const MAX_Y_LABELS = 15;
|
|
18
18
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CandlePoint, LiveChartPoint } from "../types";
|
|
2
|
+
|
|
3
|
+
/** A fresh wrapper per SharedValue notification, including same-array modify(). */
|
|
4
|
+
export interface HistoryRevision<T> {
|
|
5
|
+
data: T[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Per-engine, one-entry cache. Only committed history belongs here. */
|
|
9
|
+
export interface HistoryRangeCache {
|
|
10
|
+
revision: object | undefined;
|
|
11
|
+
data: LiveChartPoint[] | CandlePoint[] | undefined;
|
|
12
|
+
candle: boolean;
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
minTime: number;
|
|
18
|
+
maxTime: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeHistoryRangeCache(): HistoryRangeCache {
|
|
22
|
+
"worklet";
|
|
23
|
+
return {
|
|
24
|
+
revision: undefined,
|
|
25
|
+
data: undefined,
|
|
26
|
+
candle: false,
|
|
27
|
+
start: -1,
|
|
28
|
+
end: -1,
|
|
29
|
+
min: Infinity,
|
|
30
|
+
max: -Infinity,
|
|
31
|
+
minTime: 0,
|
|
32
|
+
maxTime: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Inclusive time bounds; the returned end index is exclusive. */
|
|
37
|
+
function timeBound(
|
|
38
|
+
data: { time: number }[],
|
|
39
|
+
time: number,
|
|
40
|
+
upper: boolean,
|
|
41
|
+
): number {
|
|
42
|
+
"worklet";
|
|
43
|
+
let lo = 0;
|
|
44
|
+
let hi = data.length;
|
|
45
|
+
while (lo < hi) {
|
|
46
|
+
const mid = (lo + hi) >>> 1;
|
|
47
|
+
if (upper ? data[mid].time <= time : data[mid].time < time) lo = mid + 1;
|
|
48
|
+
else hi = mid;
|
|
49
|
+
}
|
|
50
|
+
return lo;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reuse only when a trusted revision AND both visible index bounds match.
|
|
55
|
+
* Without a revision, always scan: identity/length cannot detect in-place edits.
|
|
56
|
+
* The caller owns the cache; live candles, references and animation stay outside.
|
|
57
|
+
*/
|
|
58
|
+
export function historyRange(
|
|
59
|
+
data: LiveChartPoint[] | CandlePoint[],
|
|
60
|
+
candle: boolean,
|
|
61
|
+
from: number,
|
|
62
|
+
to: number,
|
|
63
|
+
revision: object | undefined,
|
|
64
|
+
cache: HistoryRangeCache,
|
|
65
|
+
): HistoryRangeCache {
|
|
66
|
+
"worklet";
|
|
67
|
+
const start = timeBound(data, from, false);
|
|
68
|
+
const end = to === Infinity ? data.length : timeBound(data, to, true);
|
|
69
|
+
if (
|
|
70
|
+
revision !== undefined &&
|
|
71
|
+
cache.revision === revision &&
|
|
72
|
+
cache.data === data &&
|
|
73
|
+
cache.candle === candle &&
|
|
74
|
+
cache.start === start &&
|
|
75
|
+
cache.end === end
|
|
76
|
+
)
|
|
77
|
+
return cache;
|
|
78
|
+
|
|
79
|
+
cache.revision = revision;
|
|
80
|
+
cache.data = data;
|
|
81
|
+
cache.candle = candle;
|
|
82
|
+
cache.start = start;
|
|
83
|
+
cache.end = end;
|
|
84
|
+
let min = Infinity;
|
|
85
|
+
let max = -Infinity;
|
|
86
|
+
let minTime = 0;
|
|
87
|
+
let maxTime = 0;
|
|
88
|
+
for (let i = start; i < end; i++) {
|
|
89
|
+
const point = data[i];
|
|
90
|
+
const low = candle
|
|
91
|
+
? (point as CandlePoint).low
|
|
92
|
+
: (point as LiveChartPoint).value;
|
|
93
|
+
const high = candle ? (point as CandlePoint).high : low;
|
|
94
|
+
// Strict comparisons preserve the first occurrence when extrema tie.
|
|
95
|
+
if (low < min) {
|
|
96
|
+
min = low;
|
|
97
|
+
minTime = point.time;
|
|
98
|
+
}
|
|
99
|
+
if (high > max) {
|
|
100
|
+
max = high;
|
|
101
|
+
maxTime = point.time;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
cache.min = min;
|
|
105
|
+
cache.max = max;
|
|
106
|
+
cache.minTime = minTime;
|
|
107
|
+
cache.maxTime = maxTime;
|
|
108
|
+
return cache;
|
|
109
|
+
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CandleGap,
|
|
3
|
+
CandlePoint,
|
|
4
|
+
LiveChartPoint,
|
|
5
|
+
RangeAnimationConfig,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
historyRange,
|
|
10
|
+
makeHistoryRangeCache,
|
|
11
|
+
type HistoryRangeCache,
|
|
12
|
+
} from "./historyRangeCache";
|
|
2
13
|
|
|
3
14
|
import { MOTION_METRICS_DEFAULTS } from "../constants";
|
|
4
15
|
import { lerp } from "../math/lerp";
|
|
16
|
+
import { rangeAnimationSpeed } from "../math/rangeAnimation";
|
|
5
17
|
import { thresholdRangeMinMax } from "../math/threshold";
|
|
6
18
|
import { previousCandleCloseAtTime } from "../math/candleGaps";
|
|
7
19
|
|
|
@@ -53,6 +65,7 @@ export interface EngineTickInput {
|
|
|
53
65
|
canvasHeight: number;
|
|
54
66
|
timeWindow: number;
|
|
55
67
|
smoothing: number;
|
|
68
|
+
rangeAnimation?: RangeAnimationConfig;
|
|
56
69
|
exaggerate: boolean;
|
|
57
70
|
/** Extra catch-up speed added to `smoothing` when the live value lags. Default `0.12`. */
|
|
58
71
|
adaptiveSpeedBoost?: number;
|
|
@@ -79,6 +92,8 @@ export interface EngineTickInput {
|
|
|
79
92
|
yRangeScale?: number;
|
|
80
93
|
targetValue: number;
|
|
81
94
|
points: LiveChartPoint[];
|
|
95
|
+
/** Internal notification revision for the selected history; omit to disable reuse. */
|
|
96
|
+
historyRevision?: object;
|
|
82
97
|
/** Seconds since Unix epoch; defaults to `Date.now() / 1000` */
|
|
83
98
|
nowSeconds?: number;
|
|
84
99
|
/** Override the engine's "now" (unix seconds) — e.g. fill historical data edge-to-edge. */
|
|
@@ -187,6 +202,7 @@ export function advanceTimestampByPixel(
|
|
|
187
202
|
export function tickLiveChartEngineFrame(
|
|
188
203
|
state: EngineTickMutable,
|
|
189
204
|
input: EngineTickInput,
|
|
205
|
+
historyCache?: HistoryRangeCache,
|
|
190
206
|
): void {
|
|
191
207
|
"worklet";
|
|
192
208
|
const baseNow = input.nowOverride ?? input.nowSeconds ?? Date.now() / 1000;
|
|
@@ -301,39 +317,22 @@ export function tickLiveChartEngineFrame(
|
|
|
301
317
|
|
|
302
318
|
const winStart = state.timestamp - state.displayWindow;
|
|
303
319
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
320
|
+
const candleMode = input.mode === "candle";
|
|
321
|
+
const history = historyRange(
|
|
322
|
+
candleMode ? (input.candles ?? []) : input.points,
|
|
323
|
+
candleMode,
|
|
324
|
+
winStart,
|
|
325
|
+
// Line feeds may lead the local clock. Preserve their inclusive live tail.
|
|
326
|
+
candleMode || scrolledBack ? state.timestamp : Infinity,
|
|
327
|
+
input.historyRevision,
|
|
328
|
+
historyCache ?? makeHistoryRangeCache(),
|
|
329
|
+
);
|
|
330
|
+
let tMin = history.min;
|
|
331
|
+
let tMax = history.max;
|
|
332
|
+
let minTime = history.minTime;
|
|
333
|
+
let maxTime = history.maxTime;
|
|
312
334
|
|
|
313
|
-
if (
|
|
314
|
-
const candles = input.candles;
|
|
315
|
-
if (candles && candles.length > 0) {
|
|
316
|
-
let lo = 0;
|
|
317
|
-
let hi = candles.length;
|
|
318
|
-
while (lo < hi) {
|
|
319
|
-
const mid = (lo + hi) >> 1;
|
|
320
|
-
if (candles[mid].time < winStart) lo = mid + 1;
|
|
321
|
-
else hi = mid;
|
|
322
|
-
}
|
|
323
|
-
for (let i = lo; i < candles.length; i++) {
|
|
324
|
-
if (candles[i].time > state.timestamp) break;
|
|
325
|
-
/* istanbul ignore next -- trivial min/max */
|
|
326
|
-
if (candles[i].low < tMin) {
|
|
327
|
-
tMin = candles[i].low;
|
|
328
|
-
minTime = candles[i].time;
|
|
329
|
-
}
|
|
330
|
-
/* istanbul ignore next -- trivial min/max */
|
|
331
|
-
if (candles[i].high > tMax) {
|
|
332
|
-
tMax = candles[i].high;
|
|
333
|
-
maxTime = candles[i].time;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
335
|
+
if (candleMode) {
|
|
337
336
|
const lc = input.liveCandle;
|
|
338
337
|
// Fold the in-progress candle in only while its bucket is visible — when
|
|
339
338
|
// scrolled back past it, the live candle must not stretch the Y range.
|
|
@@ -349,31 +348,6 @@ export function tickLiveChartEngineFrame(
|
|
|
349
348
|
maxTime = lc.time;
|
|
350
349
|
}
|
|
351
350
|
}
|
|
352
|
-
} else {
|
|
353
|
-
const points = input.points;
|
|
354
|
-
let lo = 0;
|
|
355
|
-
let hi = points.length;
|
|
356
|
-
while (lo < hi) {
|
|
357
|
-
const mid = (lo + hi) >> 1;
|
|
358
|
-
if (points[mid].time < winStart) lo = mid + 1;
|
|
359
|
-
else hi = mid;
|
|
360
|
-
}
|
|
361
|
-
for (let i = lo; i < points.length; i++) {
|
|
362
|
-
// While scrolled back, stop at the frozen right edge (mirrors the candle
|
|
363
|
-
// scan) so newer points don't inflate the visible Y range. Following
|
|
364
|
-
// live, keep the tail inclusive — feed timestamps can run slightly ahead
|
|
365
|
-
// of the local clock and must not flicker out of the range.
|
|
366
|
-
if (scrolledBack && points[i].time > state.timestamp) break;
|
|
367
|
-
const v = points[i].value;
|
|
368
|
-
if (v < tMin) {
|
|
369
|
-
tMin = v;
|
|
370
|
-
minTime = points[i].time;
|
|
371
|
-
}
|
|
372
|
-
if (v > tMax) {
|
|
373
|
-
tMax = v;
|
|
374
|
-
maxTime = points[i].time;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
351
|
}
|
|
378
352
|
|
|
379
353
|
// Snapshot the raw data extrema (value + time) before the live value /
|
|
@@ -511,26 +485,30 @@ export function tickLiveChartEngineFrame(
|
|
|
511
485
|
const maxV = input.maxValue;
|
|
512
486
|
if (maxV !== undefined && tMax > maxV) tMax = maxV;
|
|
513
487
|
|
|
514
|
-
const
|
|
515
|
-
|
|
488
|
+
const disjoint = tMin >= state.displayMax || tMax <= state.displayMin;
|
|
489
|
+
// A subpixel settle must not jump across the other bound of a disjoint fit.
|
|
490
|
+
const rangeUnitsPerPixel = disjoint && input.rangeAnimation?.animateExpansion
|
|
491
|
+
? 0
|
|
492
|
+
: (tMax - tMin) / input.canvasHeight;
|
|
493
|
+
if (snap || yScaleDragging) {
|
|
516
494
|
state.displayMin = tMin;
|
|
517
495
|
} else {
|
|
518
496
|
state.displayMin = lerpAndSettle(
|
|
519
497
|
state.displayMin,
|
|
520
498
|
tMin,
|
|
521
|
-
speed,
|
|
499
|
+
rangeAnimationSpeed(speed, input.rangeAnimation, tMin < state.displayMin, disjoint),
|
|
522
500
|
input.dt,
|
|
523
501
|
rangeUnitsPerPixel,
|
|
524
502
|
);
|
|
525
503
|
}
|
|
526
504
|
|
|
527
|
-
if (snap || yScaleDragging
|
|
505
|
+
if (snap || yScaleDragging) {
|
|
528
506
|
state.displayMax = tMax;
|
|
529
507
|
} else {
|
|
530
508
|
state.displayMax = lerpAndSettle(
|
|
531
509
|
state.displayMax,
|
|
532
510
|
tMax,
|
|
533
|
-
speed,
|
|
511
|
+
rangeAnimationSpeed(speed, input.rangeAnimation, tMax > state.displayMax, disjoint),
|
|
534
512
|
input.dt,
|
|
535
513
|
rangeUnitsPerPixel,
|
|
536
514
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MOTION_METRICS_DEFAULTS } from "../constants";
|
|
2
2
|
import { lerp } from "../math/lerp";
|
|
3
|
-
import
|
|
3
|
+
import { rangeAnimationSpeed } from "../math/rangeAnimation";
|
|
4
|
+
import type { RangeAnimationConfig, SeriesConfig } from "../types";
|
|
4
5
|
|
|
5
6
|
export interface MultiEngineTickMutable {
|
|
6
7
|
displayMin: number;
|
|
@@ -35,6 +36,7 @@ export interface MultiEngineTickInput {
|
|
|
35
36
|
canvasHeight: number;
|
|
36
37
|
timeWindow: number;
|
|
37
38
|
smoothing: number;
|
|
39
|
+
rangeAnimation?: RangeAnimationConfig;
|
|
38
40
|
exaggerate: boolean;
|
|
39
41
|
/** Extra catch-up speed added to `smoothing` when a series tip lags. Default `0.12`. */
|
|
40
42
|
adaptiveSpeedBoost?: number;
|
|
@@ -332,16 +334,27 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
332
334
|
const maxV = input.maxValue;
|
|
333
335
|
if (maxV !== undefined && tMax > maxV) tMax = maxV;
|
|
334
336
|
|
|
335
|
-
|
|
337
|
+
const disjoint = tMin >= state.displayMax || tMax <= state.displayMin;
|
|
338
|
+
if (snap || yScaleDragging) {
|
|
336
339
|
state.displayMin = tMin;
|
|
337
340
|
} else {
|
|
338
|
-
state.displayMin = lerp(
|
|
341
|
+
state.displayMin = lerp(
|
|
342
|
+
state.displayMin,
|
|
343
|
+
tMin,
|
|
344
|
+
rangeAnimationSpeed(speed, input.rangeAnimation, tMin < state.displayMin, disjoint),
|
|
345
|
+
input.dt,
|
|
346
|
+
);
|
|
339
347
|
}
|
|
340
348
|
|
|
341
|
-
if (snap || yScaleDragging
|
|
349
|
+
if (snap || yScaleDragging) {
|
|
342
350
|
state.displayMax = tMax;
|
|
343
351
|
} else {
|
|
344
|
-
state.displayMax = lerp(
|
|
352
|
+
state.displayMax = lerp(
|
|
353
|
+
state.displayMax,
|
|
354
|
+
tMax,
|
|
355
|
+
rangeAnimationSpeed(speed, input.rangeAnimation, tMax > state.displayMax, disjoint),
|
|
356
|
+
input.dt,
|
|
357
|
+
);
|
|
345
358
|
}
|
|
346
359
|
}
|
|
347
360
|
}
|
|
@@ -545,7 +545,7 @@ const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
|
545
545
|
* Resolves `yAxis` prop to a fully-typed config or null (disabled).
|
|
546
546
|
* `true` → defaults, object → merged with defaults, falsy → null.
|
|
547
547
|
* `count` is normalized to a non-negative integer (the grid math clamps the
|
|
548
|
-
* upper bound to
|
|
548
|
+
* upper bound to `MAX_Y_LABELS`).
|
|
549
549
|
*/
|
|
550
550
|
export function resolveYAxis(
|
|
551
551
|
prop: boolean | YAxisConfig | undefined,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useSharedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
import { scheduleOnUI } from "react-native-worklets";
|
|
4
|
+
import type { HistoryRevision } from "./historyRangeCache";
|
|
5
|
+
|
|
6
|
+
// Reanimated's mapper IDs are positive. Separate IDs also let multiple charts
|
|
7
|
+
// subscribe to the same feed without replacing each other's listeners.
|
|
8
|
+
let nextHistoryListenerId = -1;
|
|
9
|
+
|
|
10
|
+
/** Invalidate synchronously: a frame can run before derived mappers flush. */
|
|
11
|
+
export function useHistoryRevision<T>(
|
|
12
|
+
data: SharedValue<T[]> | undefined,
|
|
13
|
+
source: SharedValue<T[]> | undefined = data,
|
|
14
|
+
): SharedValue<HistoryRevision<T> | undefined> {
|
|
15
|
+
const revision = useSharedValue<HistoryRevision<T> | undefined>(undefined);
|
|
16
|
+
const [listenerId] = useState(() => nextHistoryListenerId--);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
scheduleOnUI(() => {
|
|
19
|
+
"worklet";
|
|
20
|
+
const refresh = () => {
|
|
21
|
+
"worklet";
|
|
22
|
+
revision.set({ data: data?.get() ?? [] });
|
|
23
|
+
};
|
|
24
|
+
data?.addListener(listenerId, refresh);
|
|
25
|
+
if (source !== data) source?.addListener(listenerId, refresh);
|
|
26
|
+
refresh();
|
|
27
|
+
});
|
|
28
|
+
return () =>
|
|
29
|
+
scheduleOnUI(() => {
|
|
30
|
+
"worklet";
|
|
31
|
+
data?.removeListener(listenerId);
|
|
32
|
+
if (source !== data) source?.removeListener(listenerId);
|
|
33
|
+
});
|
|
34
|
+
}, [data, source, revision, listenerId]);
|
|
35
|
+
return revision;
|
|
36
|
+
}
|