react-native-livechart 3.12.0 → 4.0.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/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +16 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.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/MarkerOverlay.d.ts +6 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +56 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +9 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +10 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +505 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +150 -57
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +549 -32
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +92 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +39 -11
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +35 -3
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +525 -21
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { CANDLE_METRICS_DEFAULTS, MS_PER_FRAME_60FPS } from "../constants";
|
|
9
9
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
10
10
|
import { buildCandleGeometry } from "../draw/candle";
|
|
11
|
+
import { buildVolumeGeometry } from "../draw/volume";
|
|
11
12
|
import type { ChartPadding } from "../draw/line";
|
|
12
13
|
import { lerp } from "../math/lerp";
|
|
13
14
|
import type { CandleMetrics, CandlePoint } from "../types";
|
|
@@ -29,6 +30,10 @@ export function useCandlePaths(
|
|
|
29
30
|
candleWidthSecs: number,
|
|
30
31
|
active: boolean,
|
|
31
32
|
candleMetrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
33
|
+
/** Reserved volume-band height (px). `0` = no volume bars. */
|
|
34
|
+
volumeBandHeight = 0,
|
|
35
|
+
/** Corner radius (px) of the volume bars. */
|
|
36
|
+
volumeRadius = 0,
|
|
32
37
|
/** Static charts run no loops: register without starting. Default `true`. */
|
|
33
38
|
autostart = true,
|
|
34
39
|
) {
|
|
@@ -39,6 +44,8 @@ export function useCandlePaths(
|
|
|
39
44
|
const downBodiesBuilder = usePathBuilder();
|
|
40
45
|
const upWicksBuilder = usePathBuilder();
|
|
41
46
|
const downWicksBuilder = usePathBuilder();
|
|
47
|
+
const upBarsBuilder = usePathBuilder();
|
|
48
|
+
const downBarsBuilder = usePathBuilder();
|
|
42
49
|
|
|
43
50
|
useFrameCallback((frameInfo) => {
|
|
44
51
|
"worklet";
|
|
@@ -76,11 +83,20 @@ export function useCandlePaths(
|
|
|
76
83
|
const upBodiesPath = useDerivedValue(() => {
|
|
77
84
|
const b = upBodiesBuilder.value;
|
|
78
85
|
const { bodies } = geometry.value;
|
|
86
|
+
const radius = candleMetrics.bodyRadius;
|
|
79
87
|
for (let i = 0; i < bodies.length; i++) {
|
|
80
88
|
if (bodies[i].up) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
89
|
+
const bd = bodies[i];
|
|
90
|
+
const rr = radius > 0 ? Math.min(radius, bd.w / 2, bd.h / 2) : 0;
|
|
91
|
+
if (rr > 0) {
|
|
92
|
+
b.addRRect({
|
|
93
|
+
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
94
|
+
rx: rr,
|
|
95
|
+
ry: rr,
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
return b.detach();
|
|
@@ -90,11 +106,20 @@ export function useCandlePaths(
|
|
|
90
106
|
const downBodiesPath = useDerivedValue(() => {
|
|
91
107
|
const b = downBodiesBuilder.value;
|
|
92
108
|
const { bodies } = geometry.value;
|
|
109
|
+
const radius = candleMetrics.bodyRadius;
|
|
93
110
|
for (let i = 0; i < bodies.length; i++) {
|
|
94
111
|
if (!bodies[i].up) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
112
|
+
const bd = bodies[i];
|
|
113
|
+
const rr = radius > 0 ? Math.min(radius, bd.w / 2, bd.h / 2) : 0;
|
|
114
|
+
if (rr > 0) {
|
|
115
|
+
b.addRRect({
|
|
116
|
+
rect: { x: bd.x, y: bd.y, width: bd.w, height: bd.h },
|
|
117
|
+
rx: rr,
|
|
118
|
+
ry: rr,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
b.addRect(Skia.XYWHRect(bd.x, bd.y, bd.w, bd.h));
|
|
122
|
+
}
|
|
98
123
|
}
|
|
99
124
|
}
|
|
100
125
|
return b.detach();
|
|
@@ -126,5 +151,77 @@ export function useCandlePaths(
|
|
|
126
151
|
return b.detach();
|
|
127
152
|
});
|
|
128
153
|
|
|
129
|
-
|
|
154
|
+
// Volume bars share the candle window + (lerped) candle width so each bar sits
|
|
155
|
+
// directly under its candle body. Empty unless a volume band is reserved.
|
|
156
|
+
/* istanbul ignore next -- worklet */
|
|
157
|
+
const volumeGeometry = useDerivedValue(() => {
|
|
158
|
+
if (!active || !candles || volumeBandHeight <= 0) return { bars: [] };
|
|
159
|
+
return buildVolumeGeometry(
|
|
160
|
+
candles.value,
|
|
161
|
+
liveCandle?.value ?? null,
|
|
162
|
+
padding,
|
|
163
|
+
engine.canvasWidth.value,
|
|
164
|
+
engine.canvasHeight.value,
|
|
165
|
+
engine.timestamp.value - engine.displayWindow.value,
|
|
166
|
+
engine.displayWindow.value,
|
|
167
|
+
volumeBandHeight,
|
|
168
|
+
displayCandleWidth.get(),
|
|
169
|
+
candleMetrics,
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/* istanbul ignore next -- worklet */
|
|
174
|
+
const upBarsPath = useDerivedValue(() => {
|
|
175
|
+
const b = upBarsBuilder.value;
|
|
176
|
+
const { bars } = volumeGeometry.value;
|
|
177
|
+
for (let i = 0; i < bars.length; i++) {
|
|
178
|
+
if (bars[i].up) {
|
|
179
|
+
const bar = bars[i];
|
|
180
|
+
const rr =
|
|
181
|
+
volumeRadius > 0 ? Math.min(volumeRadius, bar.w / 2, bar.h / 2) : 0;
|
|
182
|
+
if (rr > 0) {
|
|
183
|
+
b.addRRect({
|
|
184
|
+
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
185
|
+
rx: rr,
|
|
186
|
+
ry: rr,
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return b.detach();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/* istanbul ignore next -- worklet */
|
|
197
|
+
const downBarsPath = useDerivedValue(() => {
|
|
198
|
+
const b = downBarsBuilder.value;
|
|
199
|
+
const { bars } = volumeGeometry.value;
|
|
200
|
+
for (let i = 0; i < bars.length; i++) {
|
|
201
|
+
if (!bars[i].up) {
|
|
202
|
+
const bar = bars[i];
|
|
203
|
+
const rr =
|
|
204
|
+
volumeRadius > 0 ? Math.min(volumeRadius, bar.w / 2, bar.h / 2) : 0;
|
|
205
|
+
if (rr > 0) {
|
|
206
|
+
b.addRRect({
|
|
207
|
+
rect: { x: bar.x, y: bar.y, width: bar.w, height: bar.h },
|
|
208
|
+
rx: rr,
|
|
209
|
+
ry: rr,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
b.addRect(Skia.XYWHRect(bar.x, bar.y, bar.w, bar.h));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return b.detach();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
upBodiesPath,
|
|
221
|
+
downBodiesPath,
|
|
222
|
+
upWicksPath,
|
|
223
|
+
downWicksPath,
|
|
224
|
+
upBarsPath,
|
|
225
|
+
downBarsPath,
|
|
226
|
+
} as const;
|
|
130
227
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import type { ChartOverlayContext, ChartScale } from "../types";
|
|
7
|
+
import { priceToY, timeToX, xToTime, yToPrice } from "./overlayScale";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the {@link ChartOverlayContext} handed to a custom
|
|
11
|
+
* {@link LiveChartProps.renderOverlay}: a single per-frame {@link ChartScale}
|
|
12
|
+
* SharedValue plus the pure price↔pixel / time↔pixel mapping worklets.
|
|
13
|
+
*
|
|
14
|
+
* The `scale` derived value reads the engine SharedValues (range, window, "now")
|
|
15
|
+
* and the padding each frame, so a consumer's `useAnimatedStyle` becomes reactive
|
|
16
|
+
* just by reading `scale.get()` — that read subscribes it to per-frame updates
|
|
17
|
+
* (Reanimated only observes SharedValues found directly in a worklet's closure,
|
|
18
|
+
* not those hidden behind a function call). The mappings stay pure, taking the
|
|
19
|
+
* snapshot as an argument.
|
|
20
|
+
*/
|
|
21
|
+
export function useChartOverlayContext(
|
|
22
|
+
engine: ChartEngineLayout,
|
|
23
|
+
padding: ChartPadding,
|
|
24
|
+
): ChartOverlayContext {
|
|
25
|
+
const { top: padTop, bottom: padBottom, left: padLeft, right: padRight } = padding;
|
|
26
|
+
|
|
27
|
+
const scale = useDerivedValue<ChartScale>(() => {
|
|
28
|
+
const width = engine.canvasWidth.get();
|
|
29
|
+
const height = engine.canvasHeight.get();
|
|
30
|
+
return {
|
|
31
|
+
min: engine.displayMin.get(),
|
|
32
|
+
max: engine.displayMax.get(),
|
|
33
|
+
window: engine.displayWindow.get(),
|
|
34
|
+
now: engine.timestamp.get(),
|
|
35
|
+
plot: {
|
|
36
|
+
left: padLeft,
|
|
37
|
+
top: padTop,
|
|
38
|
+
right: width - padRight,
|
|
39
|
+
bottom: height - padBottom,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}, [engine, padLeft, padTop, padRight, padBottom]);
|
|
45
|
+
|
|
46
|
+
return useMemo<ChartOverlayContext>(
|
|
47
|
+
() => ({ scale, priceToY, yToPrice, timeToX, xToTime }),
|
|
48
|
+
[scale],
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reactive Y for a price: projects `price` to its canvas Y pixel and returns it as
|
|
54
|
+
* a `SharedValue<number>` that tracks the live axis on the UI thread. The
|
|
55
|
+
* **recommended** way to place something at a price in a {@link LiveChartProps.renderOverlay}
|
|
56
|
+
* — read the returned value in your `useAnimatedStyle` like any SharedValue; the
|
|
57
|
+
* subscription to the chart's scale is handled here, so it can't be forgotten.
|
|
58
|
+
*
|
|
59
|
+
* ```tsx
|
|
60
|
+
* function PriceLevel({ ctx, price }) {
|
|
61
|
+
* const y = usePriceY(ctx, price);
|
|
62
|
+
* const style = useAnimatedStyle(() => ({ transform: [{ translateY: y.get() }] }));
|
|
63
|
+
* return <Animated.View style={style} />;
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* It's a hook, so call it once per level (render one component per price). For
|
|
68
|
+
* one-off math off the render path (e.g. a gesture handler), use the pure
|
|
69
|
+
* {@link ChartOverlayContext.priceToY} with `ctx.scale.get()` instead.
|
|
70
|
+
*/
|
|
71
|
+
export function usePriceY(
|
|
72
|
+
ctx: ChartOverlayContext,
|
|
73
|
+
price: number,
|
|
74
|
+
): SharedValue<number> {
|
|
75
|
+
const { scale, priceToY: toY } = ctx;
|
|
76
|
+
// Destructure `scale` so it sits directly in the worklet's closure (Reanimated
|
|
77
|
+
// observes top-level closure SharedValues, not ones reached via `ctx.scale`).
|
|
78
|
+
return useDerivedValue(() => toY(price, scale.get()), [scale, toY, price]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reactive X for a timestamp — the time↔pixel sibling of {@link usePriceY}.
|
|
83
|
+
* Projects `time` (unix seconds) to its canvas X pixel as a `SharedValue<number>`
|
|
84
|
+
* that tracks the scrolling window on the UI thread. Read it in your
|
|
85
|
+
* `useAnimatedStyle`; call once per element.
|
|
86
|
+
*/
|
|
87
|
+
export function useTimeX(
|
|
88
|
+
ctx: ChartOverlayContext,
|
|
89
|
+
time: number,
|
|
90
|
+
): SharedValue<number> {
|
|
91
|
+
const { scale, timeToX: toX } = ctx;
|
|
92
|
+
return useDerivedValue(() => toX(time, scale.get()), [scale, toX, time]);
|
|
93
|
+
}
|
|
@@ -27,6 +27,14 @@ export function useChartPaths(
|
|
|
27
27
|
thresholdY?: SharedValue<number>,
|
|
28
28
|
/** Draw the line/fill as a straight polyline instead of the monotone cubic. */
|
|
29
29
|
linear = false,
|
|
30
|
+
/**
|
|
31
|
+
* Value at the visible window's right edge (engine `edgeValue`). With
|
|
32
|
+
* `followViewEdge`, the line's right-edge tip uses this instead of the live
|
|
33
|
+
* `displayValue` — so while time-scrolled the line ends at the last visible
|
|
34
|
+
* price rather than dropping to the (off-screen) live value.
|
|
35
|
+
*/
|
|
36
|
+
edgeValue?: SharedValue<number>,
|
|
37
|
+
followViewEdge = false,
|
|
30
38
|
) {
|
|
31
39
|
const lineBuilder = usePathBuilder();
|
|
32
40
|
const fillBuilder = usePathBuilder();
|
|
@@ -53,9 +61,14 @@ export function useChartPaths(
|
|
|
53
61
|
const cache = cacheRef.current!;
|
|
54
62
|
cache.ptsTick = !cache.ptsTick;
|
|
55
63
|
const buf = cache.ptsTick ? cache.ptsA : cache.ptsB;
|
|
64
|
+
// While scrolled back with `followViewEdge`, tip the line at the view-edge
|
|
65
|
+
// price (engine `edgeValue`) instead of the live value — otherwise the line
|
|
66
|
+
// drops from the last visible point to the off-screen live value at the edge.
|
|
67
|
+
const tipValue =
|
|
68
|
+
followViewEdge && edgeValue ? edgeValue.get() : engine.displayValue.get();
|
|
56
69
|
const realPts = buildLinePoints(
|
|
57
70
|
engine.data.get(),
|
|
58
|
-
|
|
71
|
+
tipValue,
|
|
59
72
|
engine.timestamp.get(),
|
|
60
73
|
engine.displayWindow.get(),
|
|
61
74
|
engine.displayMin.get(),
|
|
@@ -130,13 +130,20 @@ export function useCrosshair(
|
|
|
130
130
|
*/
|
|
131
131
|
deferTapHit?: (x: number, y: number) => boolean,
|
|
132
132
|
/** Where the tooltip pill sits relative to the scrub line. Default `"side"`. */
|
|
133
|
-
tooltipPlacement: "side" | "top" | "bottom" = "side",
|
|
133
|
+
tooltipPlacement: "side" | "top" | "bottom" | "point" = "side",
|
|
134
134
|
/** Render the value row in the default tooltip. Default `true`. */
|
|
135
135
|
tooltipShowValue = true,
|
|
136
136
|
/** Render the time row in the default tooltip. Default `true`. */
|
|
137
137
|
tooltipShowTime = true,
|
|
138
138
|
/** Gap (px) between the tooltip and the plot edge it's pinned to. Default `8`. */
|
|
139
139
|
tooltipMargin = 8,
|
|
140
|
+
/**
|
|
141
|
+
* Height (px) of a bottom band to exclude from scrub recognition — the
|
|
142
|
+
* axis-drag "grab the time ruler" strip, so a drag there scrolls instead of
|
|
143
|
+
* scrubbing (and a vertical drag falls through to the parent). `0` = no
|
|
144
|
+
* exclusion (the default; scrub covers the whole plot).
|
|
145
|
+
*/
|
|
146
|
+
scrubBottomExclude = 0,
|
|
140
147
|
): CrosshairState {
|
|
141
148
|
const scrubX = useSharedValue(-1);
|
|
142
149
|
const scrubActive = useSharedValue(false);
|
|
@@ -264,6 +271,7 @@ export function useCrosshair(
|
|
|
264
271
|
tooltipShowTime,
|
|
265
272
|
engine.canvasHeight.get(),
|
|
266
273
|
tooltipMargin,
|
|
274
|
+
scrubDotY.get(),
|
|
267
275
|
);
|
|
268
276
|
});
|
|
269
277
|
|
|
@@ -506,6 +514,12 @@ export function useCrosshair(
|
|
|
506
514
|
}
|
|
507
515
|
return;
|
|
508
516
|
}
|
|
517
|
+
// Defer to a marker / pressable badge under the finger: don't start a
|
|
518
|
+
// live scrub (or drop a crosshair) where the press is routed to an
|
|
519
|
+
// overlay tap. `scrubActive` is only set here, so bailing in `onStart`
|
|
520
|
+
// also keeps a follow-on drag from showing a crosshair — no `onUpdate`
|
|
521
|
+
// guard needed. (Plain-scrub counterpart of the scrub-action tap defer.)
|
|
522
|
+
if (deferTapHit !== undefined && deferTapHit(e.x, e.y)) return;
|
|
509
523
|
scrubX.set(e.x);
|
|
510
524
|
scrubActive.set(true);
|
|
511
525
|
gestureStarted.set(true);
|
|
@@ -562,6 +576,14 @@ export function useCrosshair(
|
|
|
562
576
|
gesture = gesture.activeOffsetX([-25, 25]).failOffsetY([-25, 25]);
|
|
563
577
|
}
|
|
564
578
|
|
|
579
|
+
// Axis-drag time-scroll: carve the bottom "time ruler" band out of the scrub's
|
|
580
|
+
// hit area so a drag starting there scrolls (the pan-scroll gesture owns it)
|
|
581
|
+
// and never trips the crosshair. `shouldCancelWhenOutside(false)` above keeps a
|
|
582
|
+
// scrub that *started* in the plot tracking on into the band.
|
|
583
|
+
if (scrubBottomExclude > 0) {
|
|
584
|
+
gesture = gesture.hitSlop({ bottom: -scrubBottomExclude });
|
|
585
|
+
}
|
|
586
|
+
|
|
565
587
|
// Tap: place/move the reticle, press the action badge, or dismiss the lock.
|
|
566
588
|
// Composed ahead of the pan by the controller, so a tap is never swallowed.
|
|
567
589
|
// Built only in scrub-action mode (the plain-scrub path never constructs a Tap).
|
|
@@ -626,12 +648,17 @@ export function useCrosshair(
|
|
|
626
648
|
lockActive.set(true);
|
|
627
649
|
};
|
|
628
650
|
|
|
629
|
-
|
|
651
|
+
let tapGesture = hasScrubAction
|
|
630
652
|
? Gesture.Tap()
|
|
631
653
|
.maxDuration(250)
|
|
632
654
|
.maxDistance(SCRUB_ACTION_TAP_SLOP)
|
|
633
655
|
.onEnd(handleActionTap)
|
|
634
656
|
: undefined;
|
|
657
|
+
// Keep the axis-drag band scroll-only for taps too — a tap there shouldn't
|
|
658
|
+
// drop an order-ticket reticle.
|
|
659
|
+
if (tapGesture && scrubBottomExclude > 0) {
|
|
660
|
+
tapGesture = tapGesture.hitSlop({ bottom: -scrubBottomExclude });
|
|
661
|
+
}
|
|
635
662
|
|
|
636
663
|
return {
|
|
637
664
|
scrubX,
|
package/src/hooks/useLiveDot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useDerivedValue } from "react-native-reanimated";
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
2
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
3
3
|
import type { ChartPadding } from "../draw/line";
|
|
4
4
|
|
|
@@ -6,8 +6,17 @@ import type { ChartPadding } from "../draw/line";
|
|
|
6
6
|
* Derive the live dot position (right edge of the chart, mapped to current value).
|
|
7
7
|
* Returns `{ dotX, dotY }` as shared values. Coordinates are set to `-100`
|
|
8
8
|
* (off-screen sentinel) when canvas dimensions are unavailable.
|
|
9
|
+
*
|
|
10
|
+
* With `followViewEdge` + `edgeValue`, the dot (and the value line that shares
|
|
11
|
+
* `dotY`) tracks the visible window's right-edge price while scrolled back, so it
|
|
12
|
+
* stays aligned with a `followViewEdge` badge instead of marking the live value.
|
|
9
13
|
*/
|
|
10
|
-
export function useLiveDot(
|
|
14
|
+
export function useLiveDot(
|
|
15
|
+
engine: SingleEngineState,
|
|
16
|
+
padding: ChartPadding,
|
|
17
|
+
edgeValue?: SharedValue<number>,
|
|
18
|
+
followViewEdge = false,
|
|
19
|
+
) {
|
|
11
20
|
const dotX = useDerivedValue(() => {
|
|
12
21
|
const w = engine.canvasWidth.value;
|
|
13
22
|
if (w === 0) return -100;
|
|
@@ -22,9 +31,11 @@ export function useLiveDot(engine: SingleEngineState, padding: ChartPadding) {
|
|
|
22
31
|
const dMax = engine.displayMax.value;
|
|
23
32
|
const valRange = dMax - dMin;
|
|
24
33
|
if (valRange === 0) return padding.top + chartH / 2;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
const v =
|
|
35
|
+
followViewEdge && edgeValue
|
|
36
|
+
? edgeValue.value
|
|
37
|
+
: engine.displayValue.value;
|
|
38
|
+
return padding.top + ((dMax - v) / valRange) * chartH;
|
|
28
39
|
});
|
|
29
40
|
|
|
30
41
|
return { dotX, dotY } as const;
|
package/src/hooks/useMarkers.ts
CHANGED
|
@@ -10,16 +10,30 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
|
10
10
|
import type { ChartPadding } from "../draw/line";
|
|
11
11
|
import { projectMarkers, type ProjectedMarker } from "../math/markers";
|
|
12
12
|
import { nearestMarkerIndex } from "../math/markers";
|
|
13
|
+
import {
|
|
14
|
+
clusterMarkers,
|
|
15
|
+
clusterMembers,
|
|
16
|
+
type ResolvedMarkerCluster,
|
|
17
|
+
} from "../math/markerCluster";
|
|
13
18
|
import type {
|
|
14
19
|
LiveChartPoint,
|
|
15
20
|
Marker,
|
|
16
|
-
|
|
21
|
+
MarkerPressEvent,
|
|
17
22
|
SeriesConfig,
|
|
18
23
|
} from "../types";
|
|
19
24
|
|
|
25
|
+
const ANCHORED_CLUSTER: ResolvedMarkerCluster = {
|
|
26
|
+
mode: "anchored",
|
|
27
|
+
overlap: 0.75,
|
|
28
|
+
gap: 2,
|
|
29
|
+
maxBeforeGroup: 5,
|
|
30
|
+
};
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
33
|
* Projects markers to screen positions each frame and builds a tap gesture that
|
|
22
|
-
* hit-tests against them, firing `
|
|
34
|
+
* hit-tests against them, firing `onMarkerPress` (or `null` on a miss). Applies
|
|
35
|
+
* the {@link clusterMarkers} collision pass each frame so hit-testing matches the
|
|
36
|
+
* drawn (stacked / collapsed) positions.
|
|
23
37
|
*/
|
|
24
38
|
export function useMarkers(
|
|
25
39
|
engine: ChartEngineLayout,
|
|
@@ -27,7 +41,7 @@ export function useMarkers(
|
|
|
27
41
|
markers: SharedValue<Marker[]>,
|
|
28
42
|
active: boolean,
|
|
29
43
|
hitRadius: number,
|
|
30
|
-
|
|
44
|
+
onMarkerPress?: (event: MarkerPressEvent | null) => void,
|
|
31
45
|
seriesSV?: SharedValue<SeriesConfig[]>,
|
|
32
46
|
lineData?: SharedValue<LiveChartPoint[]>,
|
|
33
47
|
/** Static charts run no loops: register without starting. Default `true`. */
|
|
@@ -35,6 +49,8 @@ export function useMarkers(
|
|
|
35
49
|
/** Single-series line is drawn linear (`line.curve === "linear"`) — anchor
|
|
36
50
|
* `lineData` markers on the straight chord rather than the spline. */
|
|
37
51
|
lineLinear = false,
|
|
52
|
+
/** Collision config; default `"anchored"` (no stacking). */
|
|
53
|
+
cluster: ResolvedMarkerCluster = ANCHORED_CLUSTER,
|
|
38
54
|
): {
|
|
39
55
|
projected: SharedValue<ProjectedMarker[]>;
|
|
40
56
|
tapGesture: ReturnType<typeof Gesture.Tap>;
|
|
@@ -51,10 +67,10 @@ export function useMarkers(
|
|
|
51
67
|
cacheRef.current = { a: [] as ProjectedMarker[], b: [] as ProjectedMarker[], tick: false };
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
const
|
|
70
|
+
const emitPress =
|
|
55
71
|
/* istanbul ignore next -- invoked only from the UI-thread tap worklet */
|
|
56
|
-
(event:
|
|
57
|
-
|
|
72
|
+
(event: MarkerPressEvent | null) => {
|
|
73
|
+
onMarkerPress?.(event);
|
|
58
74
|
};
|
|
59
75
|
|
|
60
76
|
useFrameCallback(
|
|
@@ -82,6 +98,7 @@ export function useMarkers(
|
|
|
82
98
|
lineData: lineData?.get(),
|
|
83
99
|
lineLinear,
|
|
84
100
|
});
|
|
101
|
+
clusterMarkers(markers.get(), buf, { config: cluster });
|
|
85
102
|
projected.set(buf);
|
|
86
103
|
},
|
|
87
104
|
autostart,
|
|
@@ -92,14 +109,25 @@ export function useMarkers(
|
|
|
92
109
|
e,
|
|
93
110
|
) => {
|
|
94
111
|
"worklet";
|
|
95
|
-
const
|
|
112
|
+
const proj = projected.get();
|
|
113
|
+
const idx = nearestMarkerIndex(proj, e.x, e.y, hitRadius);
|
|
96
114
|
if (idx < 0) {
|
|
97
|
-
runOnJS(
|
|
115
|
+
runOnJS(emitPress)(null);
|
|
98
116
|
return;
|
|
99
117
|
}
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
118
|
+
const ms = markers.get();
|
|
119
|
+
const m = ms[idx];
|
|
120
|
+
const p = proj[idx];
|
|
121
|
+
const isGrouped = p.isGrouped;
|
|
122
|
+
// Collapsed cluster: surface the whole bucket so the consumer can list it.
|
|
123
|
+
const members = isGrouped ? clusterMembers(ms, proj, idx) : undefined;
|
|
124
|
+
runOnJS(emitPress)({
|
|
125
|
+
marker: m,
|
|
126
|
+
point: { x: p.x, y: p.y },
|
|
127
|
+
index: idx,
|
|
128
|
+
isGrouped,
|
|
129
|
+
members,
|
|
130
|
+
});
|
|
103
131
|
},
|
|
104
132
|
);
|
|
105
133
|
|