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
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
|
+
}
|
|
@@ -244,6 +244,35 @@ export function computeScrubDotY(
|
|
|
244
244
|
return padTop + ((displayMax - value) / valRange) * chartH;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* A reference value's canvas Y pinned to the plot: a value off the visible range
|
|
249
|
+
* sits on the nearest plot edge. `-1` when there is no plot height (not laid out,
|
|
250
|
+
* or shorter than its padding) — decided from the layout, not from the projected
|
|
251
|
+
* Y: {@link computeScrubDotY} returns a negative Y for a value far above the range
|
|
252
|
+
* too, and that one must pin to the top edge as a value below pins to the bottom.
|
|
253
|
+
*/
|
|
254
|
+
export function pinnedPlotY(
|
|
255
|
+
value: number,
|
|
256
|
+
displayMin: number,
|
|
257
|
+
displayMax: number,
|
|
258
|
+
canvasHeight: number,
|
|
259
|
+
padTop: number,
|
|
260
|
+
padBottom: number,
|
|
261
|
+
): number {
|
|
262
|
+
"worklet";
|
|
263
|
+
const bottom = canvasHeight - padBottom;
|
|
264
|
+
if (bottom - padTop <= 0) return -1;
|
|
265
|
+
const y = computeScrubDotY(
|
|
266
|
+
value,
|
|
267
|
+
displayMin,
|
|
268
|
+
displayMax,
|
|
269
|
+
canvasHeight,
|
|
270
|
+
padTop,
|
|
271
|
+
padBottom,
|
|
272
|
+
);
|
|
273
|
+
return Math.min(bottom, Math.max(padTop, y));
|
|
274
|
+
}
|
|
275
|
+
|
|
247
276
|
/**
|
|
248
277
|
* Inverse of {@link computeScrubDotY}: maps a canvas Y pixel back to a value (a
|
|
249
278
|
* free price *level*, used by scrub-action mode where the reticle Y is the chosen
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
3
2
|
import {
|
|
4
3
|
useDerivedValue,
|
|
5
4
|
useFrameCallback,
|
|
@@ -8,12 +7,15 @@ import {
|
|
|
8
7
|
} from "react-native-reanimated";
|
|
9
8
|
import { CANDLE_METRICS_DEFAULTS, MS_PER_FRAME_60FPS } from "../constants";
|
|
10
9
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
10
|
+
import { buildCandleBodyPath } from "../draw/candleBodyPath";
|
|
11
11
|
import { buildCandleGeometry } from "../draw/candle";
|
|
12
12
|
import { buildVolumeGeometry } from "../draw/volume";
|
|
13
13
|
import type { ChartPadding } from "../draw/line";
|
|
14
14
|
import { lerp } from "../math/lerp";
|
|
15
15
|
import type { CandleMetrics, CandlePoint } from "../types";
|
|
16
|
+
import { useVolumeRange } from "./useVolumeRange";
|
|
16
17
|
import { usePathBuilder } from "./usePathBuilder";
|
|
18
|
+
import { useDemandFrameLoop } from "./useDemandFrameLoop";
|
|
17
19
|
|
|
18
20
|
const CANDLE_WIDTH_LERP_SPEED = 0.08;
|
|
19
21
|
|
|
@@ -40,6 +42,7 @@ export function useCandleWidthLerp(
|
|
|
40
42
|
active: boolean,
|
|
41
43
|
/** Runtime gate shared with the chart engine. */
|
|
42
44
|
isFrameLoopActive?: SharedValue<boolean>,
|
|
45
|
+
autoSleep = false,
|
|
43
46
|
): SharedValue<number> {
|
|
44
47
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
45
48
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
@@ -51,10 +54,10 @@ export function useCandleWidthLerp(
|
|
|
51
54
|
() => candleLerpSpeed ?? CANDLE_WIDTH_LERP_SPEED,
|
|
52
55
|
);
|
|
53
56
|
|
|
54
|
-
const
|
|
57
|
+
const tickWidth = useCallback((dt: number) => {
|
|
55
58
|
"worklet";
|
|
56
|
-
if (!active || isFrameLoopActive?.get() === false) return;
|
|
57
|
-
const
|
|
59
|
+
if (!active || isFrameLoopActive?.get() === false) return false;
|
|
60
|
+
const previous = displayCandleWidth.get();
|
|
58
61
|
const target = targetCandleWidth.get();
|
|
59
62
|
const next = lerp(
|
|
60
63
|
displayCandleWidth.get(),
|
|
@@ -62,12 +65,24 @@ export function useCandleWidthLerp(
|
|
|
62
65
|
widthLerpSpeed.get(),
|
|
63
66
|
dt,
|
|
64
67
|
);
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const settled = Math.abs(target - next) < 0.01 ? target : next;
|
|
69
|
+
displayCandleWidth.set(settled);
|
|
70
|
+
return previous !== settled;
|
|
71
|
+
}, [active, isFrameLoopActive, displayCandleWidth, targetCandleWidth, widthLerpSpeed]);
|
|
72
|
+
const prepareWidth = useCallback(() => {
|
|
73
|
+
"worklet";
|
|
74
|
+
return [targetCandleWidth.get(), widthLerpSpeed.get(), isFrameLoopActive?.get()];
|
|
75
|
+
}, [targetCandleWidth, widthLerpSpeed, isFrameLoopActive]);
|
|
76
|
+
useDemandFrameLoop(autoSleep && autostart && active, prepareWidth, tickWidth);
|
|
77
|
+
const legacyActive = autostart && !autoSleep;
|
|
78
|
+
const widthFrameCallback = useFrameCallback((frameInfo) => {
|
|
79
|
+
"worklet";
|
|
80
|
+
tickWidth(frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS);
|
|
81
|
+
}, legacyActive);
|
|
67
82
|
useEffect(() => {
|
|
68
83
|
// `autostart` is an initial seed in Reanimated, not a reactive switch.
|
|
69
|
-
widthFrameCallback.setActive(
|
|
70
|
-
}, [
|
|
84
|
+
widthFrameCallback.setActive(legacyActive);
|
|
85
|
+
}, [legacyActive, widthFrameCallback]);
|
|
71
86
|
|
|
72
87
|
return displayCandleWidth;
|
|
73
88
|
}
|
|
@@ -118,50 +133,24 @@ export function useCandlePaths(
|
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
/* istanbul ignore next -- worklet */
|
|
121
|
-
const upBodiesPath = useDerivedValue(() =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (rr > 0) {
|
|
130
|
-
b.addRRect({
|
|
131
|
-
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
132
|
-
rx: rr,
|
|
133
|
-
ry: rr,
|
|
134
|
-
});
|
|
135
|
-
} else {
|
|
136
|
-
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return b.detach();
|
|
141
|
-
});
|
|
136
|
+
const upBodiesPath = useDerivedValue(() =>
|
|
137
|
+
buildCandleBodyPath(
|
|
138
|
+
upBodiesBuilder.get(),
|
|
139
|
+
geometry.get().bodies,
|
|
140
|
+
true,
|
|
141
|
+
candleMetrics.bodyRadius,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
142
144
|
|
|
143
145
|
/* istanbul ignore next -- worklet */
|
|
144
|
-
const downBodiesPath = useDerivedValue(() =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (rr > 0) {
|
|
153
|
-
b.addRRect({
|
|
154
|
-
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
155
|
-
rx: rr,
|
|
156
|
-
ry: rr,
|
|
157
|
-
});
|
|
158
|
-
} else {
|
|
159
|
-
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return b.detach();
|
|
164
|
-
});
|
|
146
|
+
const downBodiesPath = useDerivedValue(() =>
|
|
147
|
+
buildCandleBodyPath(
|
|
148
|
+
downBodiesBuilder.get(),
|
|
149
|
+
geometry.get().bodies,
|
|
150
|
+
false,
|
|
151
|
+
candleMetrics.bodyRadius,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
165
154
|
|
|
166
155
|
/* istanbul ignore next -- worklet */
|
|
167
156
|
const upWicksPath = useDerivedValue(() => {
|
|
@@ -192,6 +181,13 @@ export function useCandlePaths(
|
|
|
192
181
|
// Volume bars share the candle window + (lerped) candle width so each bar sits
|
|
193
182
|
// directly under its candle body. Empty unless a volume band is reserved.
|
|
194
183
|
/* istanbul ignore next -- worklet */
|
|
184
|
+
const volumeRange = useVolumeRange(
|
|
185
|
+
candles,
|
|
186
|
+
engine.timestamp,
|
|
187
|
+
engine.displayWindow,
|
|
188
|
+
displayCandleWidth,
|
|
189
|
+
active && volumeBandHeight > 0,
|
|
190
|
+
);
|
|
195
191
|
const volumeGeometry = useDerivedValue(() => {
|
|
196
192
|
if (!active || !candles || volumeBandHeight <= 0) return { bars: [] };
|
|
197
193
|
return buildVolumeGeometry(
|
|
@@ -205,54 +201,32 @@ export function useCandlePaths(
|
|
|
205
201
|
volumeBandHeight,
|
|
206
202
|
displayCandleWidth.get(),
|
|
207
203
|
candleMetrics,
|
|
204
|
+
{
|
|
205
|
+
start: volumeRange.start.get(),
|
|
206
|
+
end: volumeRange.end.get(),
|
|
207
|
+
max: volumeRange.max.get(),
|
|
208
|
+
},
|
|
208
209
|
);
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
/* istanbul ignore next -- worklet */
|
|
212
|
-
const upBarsPath = useDerivedValue(() =>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (rr > 0) {
|
|
221
|
-
b.addRRect({
|
|
222
|
-
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
223
|
-
rx: rr,
|
|
224
|
-
ry: rr,
|
|
225
|
-
});
|
|
226
|
-
} else {
|
|
227
|
-
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return b.detach();
|
|
232
|
-
});
|
|
233
|
-
|
|
213
|
+
const upBarsPath = useDerivedValue(() =>
|
|
214
|
+
buildCandleBodyPath(
|
|
215
|
+
upBarsBuilder.get(),
|
|
216
|
+
volumeGeometry.get().bars,
|
|
217
|
+
true,
|
|
218
|
+
volumeRadius,
|
|
219
|
+
),
|
|
220
|
+
);
|
|
234
221
|
/* istanbul ignore next -- worklet */
|
|
235
|
-
const downBarsPath = useDerivedValue(() =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (rr > 0) {
|
|
244
|
-
b.addRRect({
|
|
245
|
-
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
246
|
-
rx: rr,
|
|
247
|
-
ry: rr,
|
|
248
|
-
});
|
|
249
|
-
} else {
|
|
250
|
-
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return b.detach();
|
|
255
|
-
});
|
|
222
|
+
const downBarsPath = useDerivedValue(() =>
|
|
223
|
+
buildCandleBodyPath(
|
|
224
|
+
downBarsBuilder.get(),
|
|
225
|
+
volumeGeometry.get().bars,
|
|
226
|
+
false,
|
|
227
|
+
volumeRadius,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
256
230
|
|
|
257
231
|
return {
|
|
258
232
|
upBodiesPath,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildLineFillPaths } from "../draw/lineFillPaths";
|
|
1
2
|
import { Skia, type SkPath } from "@shopify/react-native-skia";
|
|
2
3
|
import { useRef } from "react";
|
|
3
4
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
@@ -8,13 +9,14 @@ import type {
|
|
|
8
9
|
} from "../core/useLiveChartEngine";
|
|
9
10
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
10
11
|
import { buildLineGapSegmentRanges } from "../draw/lineGap";
|
|
12
|
+
import { makeLineSimplifyScratch, simplifyLinePoints } from "../math/simplify";
|
|
13
|
+
import { makeSplineScratch } from "../math/spline";
|
|
14
|
+
import { thresholdSampleSpanX } from "../math/threshold";
|
|
11
15
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { sampleThresholdYAt, thresholdSampleSpanX } from "../math/threshold";
|
|
17
|
-
import { blendPtsY, squigglifyPts } from "../math/squiggly";
|
|
16
|
+
blendPtsY,
|
|
17
|
+
squiggleClockSeconds,
|
|
18
|
+
squigglifyPts,
|
|
19
|
+
} from "../math/squiggly";
|
|
18
20
|
import { usePathBuilder } from "./usePathBuilder";
|
|
19
21
|
import type { CandleGap } from "../types";
|
|
20
22
|
|
|
@@ -31,7 +33,7 @@ export function resolveLineTipValue(
|
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Builds the `linePath` / `fillPath` with `Skia.PathBuilder`s reused across
|
|
34
|
-
*
|
|
36
|
+
* geometry updates (one per curve, held in a SharedValue) and finalized with `detach()` —
|
|
35
37
|
* which returns a fresh immutable `SkPath` each frame and resets the builder.
|
|
36
38
|
* The fresh reference makes Reanimated notify subscribers (re-record + repaint)
|
|
37
39
|
* without the two-SkPath ping-pong the mutable-path pool needed.
|
|
@@ -69,6 +71,12 @@ export function useChartPaths(
|
|
|
69
71
|
|
|
70
72
|
const cacheRef = useRef<{
|
|
71
73
|
emptyPath: SkPath;
|
|
74
|
+
lastPts: number[] | null;
|
|
75
|
+
lastRanges: number[] | null;
|
|
76
|
+
lastLinear: boolean;
|
|
77
|
+
lastBottom: number;
|
|
78
|
+
lineOutput: SkPath | null;
|
|
79
|
+
fillOutput: SkPath | null;
|
|
72
80
|
ptsA: number[];
|
|
73
81
|
ptsB: number[];
|
|
74
82
|
rawPts: number[];
|
|
@@ -87,6 +95,12 @@ export function useChartPaths(
|
|
|
87
95
|
if (cacheRef.current === null) {
|
|
88
96
|
cacheRef.current = {
|
|
89
97
|
emptyPath: Skia.Path.Make(),
|
|
98
|
+
lastPts: null,
|
|
99
|
+
lastRanges: null,
|
|
100
|
+
lastLinear: false,
|
|
101
|
+
lastBottom: NaN,
|
|
102
|
+
lineOutput: null,
|
|
103
|
+
fillOutput: null,
|
|
90
104
|
ptsA: [] as number[],
|
|
91
105
|
ptsB: [] as number[],
|
|
92
106
|
rawPts: [] as number[],
|
|
@@ -160,9 +174,11 @@ export function useChartPaths(
|
|
|
160
174
|
// Compute squiggly Y values at the same X positions as the real line
|
|
161
175
|
const centerY =
|
|
162
176
|
(engine.canvasHeight.get() - padding.bottom + padding.top) / 2;
|
|
177
|
+
// Same wall clock as the loading shell's squiggle, so the wave stays
|
|
178
|
+
// continuous where the shell hands over to the morph.
|
|
163
179
|
const squigglyPts = squigglifyPts(
|
|
164
180
|
renderPts,
|
|
165
|
-
|
|
181
|
+
squiggleClockSeconds(),
|
|
166
182
|
centerY,
|
|
167
183
|
squiggleAmplitude,
|
|
168
184
|
squiggleSpeed,
|
|
@@ -200,117 +216,77 @@ export function useChartPaths(
|
|
|
200
216
|
);
|
|
201
217
|
});
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
// One derived rebuild keeps curve inputs and all outputs in the same revision.
|
|
220
|
+
// The spline solver emits to separate builders without extra command buffers.
|
|
221
|
+
const paths = useDerivedValue(() => {
|
|
204
222
|
const cache = cacheRef.current!;
|
|
205
223
|
const pts = flatPts.get();
|
|
206
|
-
const n = pts.length >> 1;
|
|
207
|
-
if (n < 2) return cache.emptyPath;
|
|
208
|
-
const b = lineBuilder.value;
|
|
209
224
|
const ranges = segmentRanges.get();
|
|
210
|
-
if (ranges.length === 0)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
if (pts.length < 4 || ranges.length === 0) {
|
|
226
|
+
cache.lastPts = null;
|
|
227
|
+
return {
|
|
228
|
+
line: cache.emptyPath,
|
|
229
|
+
fill: cache.emptyPath,
|
|
230
|
+
band: cache.emptyPath,
|
|
231
|
+
};
|
|
216
232
|
}
|
|
217
|
-
return b.detach();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const fillPath = useDerivedValue(() => {
|
|
221
|
-
const cache = cacheRef.current!;
|
|
222
|
-
const pts = flatPts.get();
|
|
223
|
-
const n = pts.length >> 1;
|
|
224
|
-
if (n < 2) return cache.emptyPath;
|
|
225
|
-
const b = fillBuilder.value;
|
|
226
|
-
const ranges = segmentRanges.get();
|
|
227
|
-
if (ranges.length === 0) return cache.emptyPath;
|
|
228
233
|
const bottom = engine.canvasHeight.get() - padding.bottom;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// The samples live on the time-anchored, gliding grid — interpolate them
|
|
268
|
-
// across that span (same as the shader), not the static plot edges.
|
|
269
|
-
const [x0, x1] = thresholdSampleSpanX(
|
|
270
|
-
engine.timestamp.get(),
|
|
271
|
-
engine.displayWindow.get(),
|
|
272
|
-
padding.left,
|
|
273
|
-
engine.canvasWidth.get() - padding.right,
|
|
274
|
-
count,
|
|
234
|
+
// Buffers alternate on each geometry update. Identical references here
|
|
235
|
+
// therefore mean only the threshold changed, not the line geometry.
|
|
236
|
+
const bandOnly =
|
|
237
|
+
cache.lastPts === pts &&
|
|
238
|
+
cache.lastRanges === ranges &&
|
|
239
|
+
cache.lastLinear === linear &&
|
|
240
|
+
cache.lastBottom === bottom;
|
|
241
|
+
const samples = thresholdSamples?.get();
|
|
242
|
+
const sampled = samples != null && samples.length >= 2;
|
|
243
|
+
const y = thresholdY?.get() ?? NaN;
|
|
244
|
+
const hasBand = sampled || Number.isFinite(y);
|
|
245
|
+
const span = sampled
|
|
246
|
+
? thresholdSampleSpanX(
|
|
247
|
+
engine.timestamp.get(),
|
|
248
|
+
engine.displayWindow.get(),
|
|
249
|
+
padding.left,
|
|
250
|
+
engine.canvasWidth.get() - padding.right,
|
|
251
|
+
samples.length,
|
|
252
|
+
)
|
|
253
|
+
: [0, 0];
|
|
254
|
+
const line = lineBuilder.get(),
|
|
255
|
+
fill = fillBuilder.get();
|
|
256
|
+
const band = hasBand ? thresholdFillBuilder.get() : undefined;
|
|
257
|
+
if (!bandOnly || band)
|
|
258
|
+
buildLineFillPaths(
|
|
259
|
+
line,
|
|
260
|
+
fill,
|
|
261
|
+
band,
|
|
262
|
+
pts,
|
|
263
|
+
ranges,
|
|
264
|
+
cache.scratch,
|
|
265
|
+
linear,
|
|
266
|
+
bottom,
|
|
267
|
+
y,
|
|
268
|
+
samples,
|
|
269
|
+
span[0],
|
|
270
|
+
span[1],
|
|
271
|
+
bandOnly,
|
|
275
272
|
);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const end = ranges[range + 1];
|
|
280
|
-
const first = start * 2;
|
|
281
|
-
const last = (end - 1) * 2;
|
|
282
|
-
const leftX = pts[first];
|
|
283
|
-
const rightX = pts[last];
|
|
284
|
-
b.moveTo(leftX, pts[first + 1]);
|
|
285
|
-
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
286
|
-
b.lineTo(rightX, sampleThresholdYAt(tsamples, x0, x1, rightX));
|
|
287
|
-
for (let i = count - 1; i >= 0; i--) {
|
|
288
|
-
const sx = x0 + step * i;
|
|
289
|
-
if (sx > leftX && sx < rightX) b.lineTo(sx, tsamples[i]);
|
|
290
|
-
}
|
|
291
|
-
b.lineTo(leftX, sampleThresholdYAt(tsamples, x0, x1, leftX));
|
|
292
|
-
b.close();
|
|
293
|
-
}
|
|
294
|
-
return b.detach();
|
|
273
|
+
if (!bandOnly) {
|
|
274
|
+
cache.lineOutput = line.detach();
|
|
275
|
+
cache.fillOutput = fill.detach();
|
|
295
276
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const last = (end - 1) * 2;
|
|
306
|
-
b.moveTo(pts[first], pts[first + 1]);
|
|
307
|
-
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
308
|
-
b.lineTo(pts[last], yT);
|
|
309
|
-
b.lineTo(pts[first], yT);
|
|
310
|
-
b.close();
|
|
311
|
-
}
|
|
312
|
-
return b.detach();
|
|
277
|
+
cache.lastPts = pts;
|
|
278
|
+
cache.lastRanges = ranges;
|
|
279
|
+
cache.lastLinear = linear;
|
|
280
|
+
cache.lastBottom = bottom;
|
|
281
|
+
return {
|
|
282
|
+
line: cache.lineOutput!,
|
|
283
|
+
fill: cache.fillOutput!,
|
|
284
|
+
band: band?.detach() ?? cache.emptyPath,
|
|
285
|
+
};
|
|
313
286
|
});
|
|
287
|
+
const linePath = useDerivedValue(() => paths.get().line);
|
|
288
|
+
const fillPath = useDerivedValue(() => paths.get().fill);
|
|
289
|
+
const thresholdFillPath = useDerivedValue(() => paths.get().band);
|
|
314
290
|
|
|
315
291
|
return { linePath, fillPath, thresholdFillPath } as const;
|
|
316
292
|
}
|