react-native-livechart 2.0.1 → 3.1.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/README.md +9 -3
- package/dist/components/AxisLabelOverlay.d.ts +29 -0
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +12 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +15 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts +17 -0
- 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.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/SelectionDot.d.ts +32 -0
- package/dist/components/SelectionDot.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +46 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +6 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +39 -0
- package/dist/draw/particleAtlas.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +10 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +2 -2
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +11 -11
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartColors.d.ts +5 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +8 -7
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +3 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +3 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +9 -8
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePathBuilder.d.ts +27 -0
- package/dist/hooks/usePathBuilder.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +3 -1
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/spline.d.ts +10 -2
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AxisLabelOverlay.tsx +133 -0
- package/src/components/CrosshairLine.tsx +27 -0
- package/src/components/CrosshairOverlay.tsx +50 -18
- package/src/components/DegenParticlesOverlay.tsx +93 -109
- package/src/components/LiveChart.tsx +66 -10
- package/src/components/LiveChartSeries.tsx +40 -0
- package/src/components/LoadingOverlay.tsx +15 -29
- package/src/components/MarkerOverlay.tsx +23 -28
- package/src/components/MultiSeriesValueLines.tsx +20 -40
- package/src/components/ReferenceLineOverlay.tsx +65 -107
- package/src/components/SelectionDot.tsx +92 -0
- package/src/components/ValueLineOverlay.tsx +9 -28
- package/src/components/XAxisOverlay.tsx +9 -29
- package/src/components/YAxisOverlay.tsx +6 -22
- package/src/core/resolveConfig.ts +98 -0
- package/src/core/useLiveChartEngine.ts +78 -27
- package/src/draw/markerAtlas.ts +9 -9
- package/src/draw/particleAtlas.ts +95 -0
- package/src/hooks/crosshairShared.ts +26 -0
- package/src/hooks/useBadge.ts +17 -32
- package/src/hooks/useCandlePaths.ts +27 -64
- package/src/hooks/useChartColors.ts +19 -0
- package/src/hooks/useChartPaths.ts +27 -39
- package/src/hooks/useChartReveal.ts +7 -4
- package/src/hooks/useCrosshair.ts +46 -7
- package/src/hooks/useCrosshairSeries.ts +47 -8
- package/src/hooks/useDegen.ts +6 -0
- package/src/hooks/useMarkers.ts +3 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +26 -28
- package/src/hooks/usePathBuilder.ts +42 -0
- package/src/hooks/useTradeStream.ts +3 -0
- package/src/index.ts +4 -0
- package/src/math/spline.ts +22 -2
- package/src/types.ts +101 -0
package/src/hooks/useBadge.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useRef } from "react";
|
|
1
|
+
import { type SkFont } from "@shopify/react-native-skia";
|
|
3
2
|
import {
|
|
4
3
|
useDerivedValue,
|
|
5
4
|
useSharedValue,
|
|
@@ -25,6 +24,7 @@ import type {
|
|
|
25
24
|
LiveChartPalette,
|
|
26
25
|
Momentum,
|
|
27
26
|
} from "../types";
|
|
27
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
28
28
|
|
|
29
29
|
export function useBadge(
|
|
30
30
|
engine: ChartEngineWithLiveValue,
|
|
@@ -48,32 +48,17 @@ export function useBadge(
|
|
|
48
48
|
const downRgb = hexToRgb(palette.dotDown);
|
|
49
49
|
const accentRgb = hexToRgb(palette.badgeBg);
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
const cacheRef = useRef<{
|
|
55
|
-
a: SkPath;
|
|
56
|
-
b: SkPath;
|
|
57
|
-
tick: boolean;
|
|
58
|
-
} | null>(null);
|
|
59
|
-
if (cacheRef.current === null) {
|
|
60
|
-
cacheRef.current = {
|
|
61
|
-
a: Skia.Path.Make(),
|
|
62
|
-
b: Skia.Path.Make(),
|
|
63
|
-
tick: false,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
51
|
+
// Pill path is built into a reused PathBuilder and finalized with detach()
|
|
52
|
+
// each frame — a fresh immutable SkPath, no per-frame Skia.Path.Make().
|
|
53
|
+
const badgeBuilder = usePathBuilder();
|
|
66
54
|
|
|
67
55
|
const badge = useDerivedValue(() => {
|
|
68
|
-
const
|
|
56
|
+
const b = badgeBuilder.value;
|
|
69
57
|
const w = engine.canvasWidth.get();
|
|
70
58
|
const h = engine.canvasHeight.get();
|
|
71
|
-
cache.tick = !cache.tick;
|
|
72
|
-
const path = cache.tick ? cache.a : cache.b;
|
|
73
|
-
path.reset();
|
|
74
59
|
if (w === 0 || h === 0) {
|
|
75
60
|
return {
|
|
76
|
-
path,
|
|
61
|
+
path: b.detach(),
|
|
77
62
|
textX: 0,
|
|
78
63
|
textY: 0,
|
|
79
64
|
text: "",
|
|
@@ -108,7 +93,7 @@ export function useBadge(
|
|
|
108
93
|
const bodyLeft = Math.max(badgeMetrics.marginEdge, bodyRight - pillW);
|
|
109
94
|
const pillBodyW = bodyRight - bodyLeft;
|
|
110
95
|
textX = (bodyLeft + bodyRight - textW) / 2;
|
|
111
|
-
|
|
96
|
+
b.addRRect({
|
|
112
97
|
rect: { x: bodyLeft, y: badgeY, width: pillBodyW, height: pillH },
|
|
113
98
|
rx: r,
|
|
114
99
|
ry: r,
|
|
@@ -132,16 +117,16 @@ export function useBadge(
|
|
|
132
117
|
const badgeX = w - padding.right + badgeMetrics.dotGap;
|
|
133
118
|
const cx = tl + pillW - r;
|
|
134
119
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
b.moveTo(badgeX + tl, badgeY);
|
|
121
|
+
b.lineTo(badgeX + cx, badgeY);
|
|
122
|
+
b.arcToOval(
|
|
138
123
|
{ x: badgeX + cx - r, y: badgeY, width: r * 2, height: pillH },
|
|
139
124
|
-90,
|
|
140
125
|
180,
|
|
141
126
|
false,
|
|
142
127
|
);
|
|
143
|
-
|
|
144
|
-
|
|
128
|
+
b.lineTo(badgeX + tl, badgeY + pillH);
|
|
129
|
+
b.cubicTo(
|
|
145
130
|
badgeX + badgeMetrics.tailLength + 2,
|
|
146
131
|
badgeY + pillH,
|
|
147
132
|
badgeX + 3,
|
|
@@ -149,7 +134,7 @@ export function useBadge(
|
|
|
149
134
|
badgeX,
|
|
150
135
|
badgeY + r,
|
|
151
136
|
);
|
|
152
|
-
|
|
137
|
+
b.cubicTo(
|
|
153
138
|
badgeX + 3,
|
|
154
139
|
badgeY + r - badgeMetrics.tailSpread,
|
|
155
140
|
badgeX + badgeMetrics.tailLength + 2,
|
|
@@ -157,9 +142,9 @@ export function useBadge(
|
|
|
157
142
|
badgeX + tl,
|
|
158
143
|
badgeY,
|
|
159
144
|
);
|
|
160
|
-
|
|
145
|
+
b.close();
|
|
161
146
|
} else {
|
|
162
|
-
|
|
147
|
+
b.addRRect({
|
|
163
148
|
rect: { x: bodyLeft, y: badgeY, width: pillW, height: pillH },
|
|
164
149
|
rx: r,
|
|
165
150
|
ry: r,
|
|
@@ -199,7 +184,7 @@ export function useBadge(
|
|
|
199
184
|
const textColor =
|
|
200
185
|
variant === "minimal" ? "rgba(100,100,100,1)" : palette.badgeText;
|
|
201
186
|
|
|
202
|
-
return { path, textX, textY, text, bgColor, textColor };
|
|
187
|
+
return { path: b.detach(), textX, textY, text, bgColor, textColor };
|
|
203
188
|
});
|
|
204
189
|
|
|
205
190
|
return badge;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Skia
|
|
2
|
-
import { useRef } from "react";
|
|
1
|
+
import { Skia } from "@shopify/react-native-skia";
|
|
3
2
|
import {
|
|
4
3
|
useDerivedValue,
|
|
5
4
|
useFrameCallback,
|
|
@@ -12,15 +11,15 @@ import { buildCandleGeometry } from "../draw/candle";
|
|
|
12
11
|
import type { ChartPadding } from "../draw/line";
|
|
13
12
|
import { lerp } from "../math/lerp";
|
|
14
13
|
import type { CandleMetrics, CandlePoint } from "../types";
|
|
14
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
15
15
|
|
|
16
16
|
const CANDLE_WIDTH_LERP_SPEED = 0.08;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* so
|
|
22
|
-
*
|
|
23
|
-
* ticking).
|
|
19
|
+
* Candle paths (up/down bodies + up/down wicks). Each is built into a reused
|
|
20
|
+
* `Skia.PathBuilder` and finalized with `detach()` each frame — a fresh
|
|
21
|
+
* immutable `SkPath`, so Skia repaints without a per-curve ping-pong and no
|
|
22
|
+
* mutable `SkPath` is retained across frames.
|
|
24
23
|
*/
|
|
25
24
|
export function useCandlePaths(
|
|
26
25
|
engine: SingleEngineState,
|
|
@@ -30,40 +29,16 @@ export function useCandlePaths(
|
|
|
30
29
|
candleWidthSecs: number,
|
|
31
30
|
active: boolean,
|
|
32
31
|
candleMetrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
32
|
+
/** Static charts run no loops: register without starting. Default `true`. */
|
|
33
|
+
autostart = true,
|
|
33
34
|
) {
|
|
34
35
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
35
36
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
downBodiesB: SkPath;
|
|
42
|
-
upWicksA: SkPath;
|
|
43
|
-
upWicksB: SkPath;
|
|
44
|
-
downWicksA: SkPath;
|
|
45
|
-
downWicksB: SkPath;
|
|
46
|
-
ubTick: boolean;
|
|
47
|
-
dbTick: boolean;
|
|
48
|
-
uwTick: boolean;
|
|
49
|
-
dwTick: boolean;
|
|
50
|
-
} | null>(null);
|
|
51
|
-
if (cacheRef.current === null) {
|
|
52
|
-
cacheRef.current = {
|
|
53
|
-
upBodiesA: Skia.Path.Make(),
|
|
54
|
-
upBodiesB: Skia.Path.Make(),
|
|
55
|
-
downBodiesA: Skia.Path.Make(),
|
|
56
|
-
downBodiesB: Skia.Path.Make(),
|
|
57
|
-
upWicksA: Skia.Path.Make(),
|
|
58
|
-
upWicksB: Skia.Path.Make(),
|
|
59
|
-
downWicksA: Skia.Path.Make(),
|
|
60
|
-
downWicksB: Skia.Path.Make(),
|
|
61
|
-
ubTick: false,
|
|
62
|
-
dbTick: false,
|
|
63
|
-
uwTick: false,
|
|
64
|
-
dwTick: false,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
38
|
+
const upBodiesBuilder = usePathBuilder();
|
|
39
|
+
const downBodiesBuilder = usePathBuilder();
|
|
40
|
+
const upWicksBuilder = usePathBuilder();
|
|
41
|
+
const downWicksBuilder = usePathBuilder();
|
|
67
42
|
|
|
68
43
|
useFrameCallback((frameInfo) => {
|
|
69
44
|
"worklet";
|
|
@@ -77,7 +52,7 @@ export function useCandlePaths(
|
|
|
77
52
|
dt,
|
|
78
53
|
),
|
|
79
54
|
);
|
|
80
|
-
});
|
|
55
|
+
}, autostart);
|
|
81
56
|
|
|
82
57
|
/* istanbul ignore next -- worklet */
|
|
83
58
|
const geometry = useDerivedValue(() => {
|
|
@@ -99,68 +74,56 @@ export function useCandlePaths(
|
|
|
99
74
|
|
|
100
75
|
/* istanbul ignore next -- worklet */
|
|
101
76
|
const upBodiesPath = useDerivedValue(() => {
|
|
102
|
-
const
|
|
103
|
-
cache.ubTick = !cache.ubTick;
|
|
104
|
-
const path = cache.ubTick ? cache.upBodiesA : cache.upBodiesB;
|
|
105
|
-
path.reset();
|
|
77
|
+
const b = upBodiesBuilder.value;
|
|
106
78
|
const { bodies } = geometry.value;
|
|
107
79
|
for (let i = 0; i < bodies.length; i++) {
|
|
108
80
|
if (bodies[i].up) {
|
|
109
|
-
|
|
81
|
+
b.addRect(
|
|
110
82
|
Skia.XYWHRect(bodies[i].x, bodies[i].y, bodies[i].w, bodies[i].h),
|
|
111
83
|
);
|
|
112
84
|
}
|
|
113
85
|
}
|
|
114
|
-
return
|
|
86
|
+
return b.detach();
|
|
115
87
|
});
|
|
116
88
|
|
|
117
89
|
/* istanbul ignore next -- worklet */
|
|
118
90
|
const downBodiesPath = useDerivedValue(() => {
|
|
119
|
-
const
|
|
120
|
-
cache.dbTick = !cache.dbTick;
|
|
121
|
-
const path = cache.dbTick ? cache.downBodiesA : cache.downBodiesB;
|
|
122
|
-
path.reset();
|
|
91
|
+
const b = downBodiesBuilder.value;
|
|
123
92
|
const { bodies } = geometry.value;
|
|
124
93
|
for (let i = 0; i < bodies.length; i++) {
|
|
125
94
|
if (!bodies[i].up) {
|
|
126
|
-
|
|
95
|
+
b.addRect(
|
|
127
96
|
Skia.XYWHRect(bodies[i].x, bodies[i].y, bodies[i].w, bodies[i].h),
|
|
128
97
|
);
|
|
129
98
|
}
|
|
130
99
|
}
|
|
131
|
-
return
|
|
100
|
+
return b.detach();
|
|
132
101
|
});
|
|
133
102
|
|
|
134
103
|
/* istanbul ignore next -- worklet */
|
|
135
104
|
const upWicksPath = useDerivedValue(() => {
|
|
136
|
-
const
|
|
137
|
-
cache.uwTick = !cache.uwTick;
|
|
138
|
-
const path = cache.uwTick ? cache.upWicksA : cache.upWicksB;
|
|
139
|
-
path.reset();
|
|
105
|
+
const b = upWicksBuilder.value;
|
|
140
106
|
const { wicks } = geometry.value;
|
|
141
107
|
for (let i = 0; i < wicks.length; i++) {
|
|
142
108
|
if (wicks[i].up) {
|
|
143
|
-
|
|
144
|
-
|
|
109
|
+
b.moveTo(wicks[i].x, wicks[i].y1);
|
|
110
|
+
b.lineTo(wicks[i].x, wicks[i].y2);
|
|
145
111
|
}
|
|
146
112
|
}
|
|
147
|
-
return
|
|
113
|
+
return b.detach();
|
|
148
114
|
});
|
|
149
115
|
|
|
150
116
|
/* istanbul ignore next -- worklet */
|
|
151
117
|
const downWicksPath = useDerivedValue(() => {
|
|
152
|
-
const
|
|
153
|
-
cache.dwTick = !cache.dwTick;
|
|
154
|
-
const path = cache.dwTick ? cache.downWicksA : cache.downWicksB;
|
|
155
|
-
path.reset();
|
|
118
|
+
const b = downWicksBuilder.value;
|
|
156
119
|
const { wicks } = geometry.value;
|
|
157
120
|
for (let i = 0; i < wicks.length; i++) {
|
|
158
121
|
if (!wicks[i].up) {
|
|
159
|
-
|
|
160
|
-
|
|
122
|
+
b.moveTo(wicks[i].x, wicks[i].y1);
|
|
123
|
+
b.lineTo(wicks[i].x, wicks[i].y2);
|
|
161
124
|
}
|
|
162
125
|
}
|
|
163
|
-
return
|
|
126
|
+
return b.detach();
|
|
164
127
|
});
|
|
165
128
|
|
|
166
129
|
return { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } as const;
|
|
@@ -12,6 +12,11 @@ export interface ChartColors {
|
|
|
12
12
|
gradientTopColor: string;
|
|
13
13
|
/** Bottom gradient stop — custom opacity or palette default. */
|
|
14
14
|
gradientBottomColor: string;
|
|
15
|
+
/** Gradient color stops (top → bottom). Custom `colors` when provided, else the
|
|
16
|
+
* 2-stop `[gradientTopColor, gradientBottomColor]` fallback. */
|
|
17
|
+
gradientColors: string[];
|
|
18
|
+
/** Stop positions matching `gradientColors`, or undefined for even spacing. */
|
|
19
|
+
gradientPositions: number[] | undefined;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
@@ -39,10 +44,24 @@ export function useChartColors(
|
|
|
39
44
|
? `rgba(${r}, ${g}, ${b}, ${gradientCfg.bottomOpacity})`
|
|
40
45
|
: palette.fillBottom;
|
|
41
46
|
|
|
47
|
+
const customColors = gradientCfg?.colors;
|
|
48
|
+
const hasCustomColors = Array.isArray(customColors) && customColors.length >= 2;
|
|
49
|
+
const gradientColors = hasCustomColors
|
|
50
|
+
? customColors
|
|
51
|
+
: [gradientTopColor, gradientBottomColor];
|
|
52
|
+
const gradientPositions =
|
|
53
|
+
hasCustomColors &&
|
|
54
|
+
gradientCfg?.positions !== undefined &&
|
|
55
|
+
gradientCfg.positions.length === customColors.length
|
|
56
|
+
? gradientCfg.positions
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
42
59
|
return {
|
|
43
60
|
backgroundColor,
|
|
44
61
|
gradientEnd,
|
|
45
62
|
gradientTopColor,
|
|
46
63
|
gradientBottomColor,
|
|
64
|
+
gradientColors,
|
|
65
|
+
gradientPositions,
|
|
47
66
|
};
|
|
48
67
|
}
|
|
@@ -5,30 +5,29 @@ import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
|
5
5
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
6
6
|
import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
7
7
|
import { blendPtsY, squigglifyPts } from "../math/squiggly";
|
|
8
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Builds the `linePath` / `fillPath` with `Skia.PathBuilder`s reused across
|
|
12
|
+
* frames (one per curve, held in a SharedValue) and finalized with `detach()` —
|
|
13
|
+
* which returns a fresh immutable `SkPath` each frame and resets the builder.
|
|
14
|
+
* The fresh reference makes Reanimated notify subscribers (re-record + repaint)
|
|
15
|
+
* without the two-SkPath ping-pong the mutable-path pool needed.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* two paths per curve once and ping-pong the reference each frame so Reanimated's
|
|
16
|
-
* value-equality early-return still fires its subscribers (i.e. the Skia reanimated
|
|
17
|
-
* container re-records and repaints).
|
|
17
|
+
* The flat point buffer still ping-pongs (ptsA/ptsB) so the intermediate
|
|
18
|
+
* `flatPts` derived value changes reference each frame and re-runs linePath /
|
|
19
|
+
* fillPath.
|
|
18
20
|
*/
|
|
19
21
|
export function useChartPaths(
|
|
20
22
|
engine: SingleEngineState,
|
|
21
23
|
padding: ChartPadding,
|
|
22
24
|
morphT?: SharedValue<number>,
|
|
23
25
|
) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const lineBuilder = usePathBuilder();
|
|
27
|
+
const fillBuilder = usePathBuilder();
|
|
28
|
+
|
|
26
29
|
const cacheRef = useRef<{
|
|
27
|
-
|
|
28
|
-
lineB: SkPath;
|
|
29
|
-
fillA: SkPath;
|
|
30
|
-
fillB: SkPath;
|
|
31
|
-
tick: boolean;
|
|
30
|
+
emptyPath: SkPath;
|
|
32
31
|
ptsA: number[];
|
|
33
32
|
ptsB: number[];
|
|
34
33
|
ptsTick: boolean;
|
|
@@ -36,13 +35,7 @@ export function useChartPaths(
|
|
|
36
35
|
} | null>(null);
|
|
37
36
|
if (cacheRef.current === null) {
|
|
38
37
|
cacheRef.current = {
|
|
39
|
-
|
|
40
|
-
lineB: Skia.Path.Make(),
|
|
41
|
-
fillA: Skia.Path.Make(),
|
|
42
|
-
fillB: Skia.Path.Make(),
|
|
43
|
-
tick: false,
|
|
44
|
-
// Ping-pong point buffers: reused across frames but the returned reference
|
|
45
|
-
// still alternates, so Reanimated keeps notifying linePath/fillPath.
|
|
38
|
+
emptyPath: Skia.Path.Make(),
|
|
46
39
|
ptsA: [] as number[],
|
|
47
40
|
ptsB: [] as number[],
|
|
48
41
|
ptsTick: false,
|
|
@@ -89,32 +82,27 @@ export function useChartPaths(
|
|
|
89
82
|
const linePath = useDerivedValue(() => {
|
|
90
83
|
const cache = cacheRef.current!;
|
|
91
84
|
const pts = flatPts.get();
|
|
92
|
-
cache.tick = !cache.tick;
|
|
93
|
-
const path = cache.tick ? cache.lineA : cache.lineB;
|
|
94
|
-
path.reset();
|
|
95
85
|
const n = pts.length >> 1;
|
|
96
|
-
if (n < 2) return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
if (n < 2) return cache.emptyPath;
|
|
87
|
+
const b = lineBuilder.value;
|
|
88
|
+
b.moveTo(pts[0], pts[1]);
|
|
89
|
+
drawSpline(b, pts, cache.scratch);
|
|
90
|
+
return b.detach();
|
|
100
91
|
});
|
|
101
92
|
|
|
102
93
|
const fillPath = useDerivedValue(() => {
|
|
103
94
|
const cache = cacheRef.current!;
|
|
104
95
|
const pts = flatPts.get();
|
|
105
|
-
// Use the same tick flag flipped by linePath — flipping again here would
|
|
106
|
-
// keep both paths in sync, so just read the current parity.
|
|
107
|
-
const path = cache.tick ? cache.fillA : cache.fillB;
|
|
108
|
-
path.reset();
|
|
109
96
|
const n = pts.length >> 1;
|
|
110
|
-
if (n < 2) return
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
if (n < 2) return cache.emptyPath;
|
|
98
|
+
const b = fillBuilder.value;
|
|
99
|
+
b.moveTo(pts[0], pts[1]);
|
|
100
|
+
drawSpline(b, pts, cache.scratch);
|
|
113
101
|
const bottom = engine.canvasHeight.get() - padding.bottom;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
102
|
+
b.lineTo(pts[(n - 1) * 2], bottom);
|
|
103
|
+
b.lineTo(pts[0], bottom);
|
|
104
|
+
b.close();
|
|
105
|
+
return b.detach();
|
|
118
106
|
});
|
|
119
107
|
|
|
120
108
|
return { linePath, fillPath } as const;
|
|
@@ -58,6 +58,8 @@ export interface ChartRevealState {
|
|
|
58
58
|
export function useChartReveal(
|
|
59
59
|
loading: boolean,
|
|
60
60
|
hasData: SharedValue<boolean>,
|
|
61
|
+
/** Static charts skip the entry ramp: morphT snaps to its target, no `withTiming`. */
|
|
62
|
+
isStatic = false,
|
|
61
63
|
): ChartRevealState {
|
|
62
64
|
// Best-guess initial so the common case — a live chart that already has data
|
|
63
65
|
// and isn't loading — paints fully revealed with no flash. The reaction below
|
|
@@ -71,13 +73,14 @@ export function useChartReveal(
|
|
|
71
73
|
|
|
72
74
|
// The reaction reads presence/loading on the UI thread (a worklet, never during
|
|
73
75
|
// render — so no Reanimated "reading `value` during render" warning), seeds the
|
|
74
|
-
// exact reveal state on its first run, then animates on later changes.
|
|
76
|
+
// exact reveal state on its first run, then animates on later changes. Static
|
|
77
|
+
// charts always snap (no `withTiming` ramp) so no entry animation ever runs.
|
|
75
78
|
useAnimatedReaction(
|
|
76
79
|
() => !isLoading.get() && hasData.get(),
|
|
77
80
|
(chartVisible, prev) => {
|
|
78
81
|
"worklet";
|
|
79
|
-
if (prev === null || prev === undefined) {
|
|
80
|
-
//
|
|
82
|
+
if (isStatic || prev === null || prev === undefined) {
|
|
83
|
+
// Static, or first run: snap to the correct reveal state (no animation).
|
|
81
84
|
morphT.set(chartVisible ? 1 : 0);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
@@ -90,7 +93,7 @@ export function useChartReveal(
|
|
|
90
93
|
);
|
|
91
94
|
}
|
|
92
95
|
},
|
|
93
|
-
[hasData],
|
|
96
|
+
[hasData, isStatic],
|
|
94
97
|
);
|
|
95
98
|
|
|
96
99
|
const isEmpty = useDerivedValue(() => !isLoading.get() && !hasData.get());
|
|
@@ -16,6 +16,7 @@ import type { CandlePoint, LiveChartPalette, ScrubPoint } from "../types";
|
|
|
16
16
|
import {
|
|
17
17
|
computeCandleTooltipLayout,
|
|
18
18
|
computeCrosshairOpacity,
|
|
19
|
+
computeScrubDotY,
|
|
19
20
|
computeScrubTime,
|
|
20
21
|
deriveCrosshairTooltipSingle,
|
|
21
22
|
type CrosshairState,
|
|
@@ -57,9 +58,14 @@ export function useCrosshair(
|
|
|
57
58
|
candleOpts?: CrosshairCandleOpts,
|
|
58
59
|
/** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
|
|
59
60
|
panGestureDelay = 0,
|
|
61
|
+
onGestureStart?: () => void,
|
|
62
|
+
onGestureEnd?: () => void,
|
|
60
63
|
): CrosshairState {
|
|
61
64
|
const scrubX = useSharedValue(-1);
|
|
62
65
|
const scrubActive = useSharedValue(false);
|
|
66
|
+
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
67
|
+
// activates doesn't emit a spurious onGestureEnd.
|
|
68
|
+
const gestureStarted = useSharedValue(false);
|
|
63
69
|
|
|
64
70
|
const scrubTime = useDerivedValue(() =>
|
|
65
71
|
computeScrubTime(
|
|
@@ -112,6 +118,19 @@ export function useCrosshair(
|
|
|
112
118
|
),
|
|
113
119
|
);
|
|
114
120
|
|
|
121
|
+
// Y pixel of the scrub intersection — used by the selection dot. -1 when
|
|
122
|
+
// there's no value to mark.
|
|
123
|
+
const scrubDotY = useDerivedValue(() =>
|
|
124
|
+
computeScrubDotY(
|
|
125
|
+
scrubValue.get(),
|
|
126
|
+
engine.displayMin.get(),
|
|
127
|
+
engine.displayMax.get(),
|
|
128
|
+
engine.canvasHeight.get(),
|
|
129
|
+
padding.top,
|
|
130
|
+
padding.bottom,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
115
134
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
116
135
|
// the tooltip layout worklet can size text by character count instead of a
|
|
117
136
|
// per-frame Skia measureText.
|
|
@@ -165,7 +184,19 @@ export function useCrosshair(
|
|
|
165
184
|
onScrub?.(null);
|
|
166
185
|
}
|
|
167
186
|
|
|
187
|
+
/* istanbul ignore next */
|
|
188
|
+
function handleGestureStart() {
|
|
189
|
+
onGestureStart?.();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* istanbul ignore next */
|
|
193
|
+
function handleGestureEnd() {
|
|
194
|
+
onGestureEnd?.();
|
|
195
|
+
}
|
|
196
|
+
|
|
168
197
|
const hasOnScrub = onScrub != null;
|
|
198
|
+
const hasOnGestureStart = onGestureStart != null;
|
|
199
|
+
const hasOnGestureEnd = onGestureEnd != null;
|
|
169
200
|
|
|
170
201
|
useAnimatedReaction(
|
|
171
202
|
() => {
|
|
@@ -178,13 +209,14 @@ export function useCrosshair(
|
|
|
178
209
|
if (val === null || time < 0) return "__pending__";
|
|
179
210
|
const chartW = engine.canvasWidth.get() - padding.left - padding.right;
|
|
180
211
|
if (chartW <= 0) return "__pending__";
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
const dotY = computeScrubDotY(
|
|
213
|
+
val,
|
|
214
|
+
engine.displayMin.get(),
|
|
215
|
+
engine.displayMax.get(),
|
|
216
|
+
engine.canvasHeight.get(),
|
|
217
|
+
padding.top,
|
|
218
|
+
padding.bottom,
|
|
219
|
+
);
|
|
188
220
|
let candleJson: string | null = null;
|
|
189
221
|
if (isCandleMode) {
|
|
190
222
|
const c = scrubCandle.get();
|
|
@@ -229,6 +261,8 @@ export function useCrosshair(
|
|
|
229
261
|
if (!enabled) return;
|
|
230
262
|
scrubX.set(e.x);
|
|
231
263
|
scrubActive.set(true);
|
|
264
|
+
gestureStarted.set(true);
|
|
265
|
+
if (hasOnGestureStart) scheduleOnRN(handleGestureStart);
|
|
232
266
|
},
|
|
233
267
|
)
|
|
234
268
|
.onUpdate(
|
|
@@ -243,6 +277,10 @@ export function useCrosshair(
|
|
|
243
277
|
"worklet";
|
|
244
278
|
scrubActive.set(false);
|
|
245
279
|
if (hasOnScrub) scheduleOnRN(handleScrubEnd);
|
|
280
|
+
if (gestureStarted.get()) {
|
|
281
|
+
gestureStarted.set(false);
|
|
282
|
+
if (hasOnGestureEnd) scheduleOnRN(handleGestureEnd);
|
|
283
|
+
}
|
|
246
284
|
},
|
|
247
285
|
);
|
|
248
286
|
|
|
@@ -258,6 +296,7 @@ export function useCrosshair(
|
|
|
258
296
|
scrubValue,
|
|
259
297
|
crosshairOpacity,
|
|
260
298
|
tooltipLayout,
|
|
299
|
+
scrubDotY,
|
|
261
300
|
gesture,
|
|
262
301
|
};
|
|
263
302
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useDerivedValue,
|
|
6
6
|
useSharedValue,
|
|
7
7
|
} from "react-native-reanimated";
|
|
8
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
8
9
|
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
9
10
|
import { type ChartPadding } from "../draw/line";
|
|
10
11
|
import type { ScrubPointMulti } from "../types";
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from "./crosshairSeries";
|
|
15
16
|
import {
|
|
16
17
|
computeCrosshairOpacity,
|
|
18
|
+
computeScrubDotY,
|
|
17
19
|
computeScrubTime,
|
|
18
20
|
HIDDEN_TOOLTIP,
|
|
19
21
|
type CrosshairState,
|
|
@@ -30,9 +32,14 @@ export function useCrosshairSeries(
|
|
|
30
32
|
onScrub?: (point: ScrubPointMulti | null) => void,
|
|
31
33
|
/** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
|
|
32
34
|
panGestureDelay = 0,
|
|
35
|
+
onGestureStart?: () => void,
|
|
36
|
+
onGestureEnd?: () => void,
|
|
33
37
|
): CrosshairState {
|
|
34
38
|
const scrubX = useSharedValue(-1);
|
|
35
39
|
const scrubActive = useSharedValue(false);
|
|
40
|
+
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
41
|
+
// activates doesn't emit a spurious onGestureEnd.
|
|
42
|
+
const gestureStarted = useSharedValue(false);
|
|
36
43
|
|
|
37
44
|
const scrubTime = useDerivedValue(() =>
|
|
38
45
|
computeScrubTime(
|
|
@@ -62,9 +69,34 @@ export function useCrosshairSeries(
|
|
|
62
69
|
),
|
|
63
70
|
);
|
|
64
71
|
|
|
72
|
+
// Y pixel of the scrub intersection (leading-series value) — used by the
|
|
73
|
+
// selection dot. -1 when there's no value to mark.
|
|
74
|
+
const scrubDotY = useDerivedValue(() =>
|
|
75
|
+
computeScrubDotY(
|
|
76
|
+
scrubValue.get(),
|
|
77
|
+
engine.displayMin.get(),
|
|
78
|
+
engine.displayMax.get(),
|
|
79
|
+
engine.canvasHeight.get(),
|
|
80
|
+
padding.top,
|
|
81
|
+
padding.bottom,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
65
85
|
const tooltipLayout = useSharedValue(HIDDEN_TOOLTIP);
|
|
66
86
|
|
|
87
|
+
/* istanbul ignore next */
|
|
88
|
+
function handleGestureStart() {
|
|
89
|
+
onGestureStart?.();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* istanbul ignore next */
|
|
93
|
+
function handleGestureEnd() {
|
|
94
|
+
onGestureEnd?.();
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
const hasOnScrub = onScrub != null;
|
|
98
|
+
const hasOnGestureStart = onGestureStart != null;
|
|
99
|
+
const hasOnGestureEnd = onGestureEnd != null;
|
|
68
100
|
|
|
69
101
|
useAnimatedReaction(
|
|
70
102
|
() => {
|
|
@@ -77,14 +109,14 @@ export function useCrosshairSeries(
|
|
|
77
109
|
if (chartW <= 0) return "__pending__";
|
|
78
110
|
const r = interpolateSeriesAtTime(engine.series.get(), time);
|
|
79
111
|
if (r.primary === null) return "__pending__";
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
const dotY = computeScrubDotY(
|
|
113
|
+
r.primary,
|
|
114
|
+
engine.displayMin.get(),
|
|
115
|
+
engine.displayMax.get(),
|
|
116
|
+
engine.canvasHeight.get(),
|
|
117
|
+
padding.top,
|
|
118
|
+
padding.bottom,
|
|
119
|
+
);
|
|
88
120
|
return JSON.stringify({
|
|
89
121
|
time,
|
|
90
122
|
x,
|
|
@@ -136,6 +168,8 @@ export function useCrosshairSeries(
|
|
|
136
168
|
if (!enabled) return;
|
|
137
169
|
scrubX.set(e.x);
|
|
138
170
|
scrubActive.set(true);
|
|
171
|
+
gestureStarted.set(true);
|
|
172
|
+
if (hasOnGestureStart) scheduleOnRN(handleGestureStart);
|
|
139
173
|
},
|
|
140
174
|
)
|
|
141
175
|
.onUpdate(
|
|
@@ -149,6 +183,10 @@ export function useCrosshairSeries(
|
|
|
149
183
|
/* istanbul ignore next */ () => {
|
|
150
184
|
"worklet";
|
|
151
185
|
scrubActive.set(false);
|
|
186
|
+
if (gestureStarted.get()) {
|
|
187
|
+
gestureStarted.set(false);
|
|
188
|
+
if (hasOnGestureEnd) scheduleOnRN(handleGestureEnd);
|
|
189
|
+
}
|
|
152
190
|
},
|
|
153
191
|
);
|
|
154
192
|
|
|
@@ -164,6 +202,7 @@ export function useCrosshairSeries(
|
|
|
164
202
|
scrubValue,
|
|
165
203
|
crosshairOpacity,
|
|
166
204
|
tooltipLayout,
|
|
205
|
+
scrubDotY,
|
|
167
206
|
gesture,
|
|
168
207
|
};
|
|
169
208
|
}
|