Skip to content

Commit cdf73d7

Browse files
committed
feat: add pan and hover gesture handlers, with point resolution
1 parent be93f88 commit cdf73d7

File tree

6 files changed

+185
-30
lines changed

6 files changed

+185
-30
lines changed

example/app/index.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,28 @@ import { type AxisLabelComponentProps, LineChart } from "@codeherence/react-nati
22
import { useCallback, useState } from "react";
33
import { Button, StyleSheet, Text, View } from "react-native";
44
import { useSafeAreaInsets } from "react-native-safe-area-context";
5+
import { LineChartProps } from "src/components/LineChart";
56

67
const generateRandomData = (): [number, number][] => {
78
return Array.from({ length: 30 }, (_, i) => [i, Math.random() * 2000]);
89
};
910

11+
type FilterUndefined<T> = T extends undefined ? never : T;
12+
13+
const onGestureChangeWorklet: FilterUndefined<LineChartProps["onPanGestureChange"]> = ({
14+
point,
15+
}) => {
16+
"worklet";
17+
console.log(point);
18+
};
19+
20+
const onHoverChangeWorklet: FilterUndefined<LineChartProps["onHoverGestureChange"]> = ({
21+
point,
22+
}) => {
23+
"worklet";
24+
console.log(point);
25+
};
26+
1027
const formatter = new Intl.NumberFormat("en-US", {
1128
style: "currency",
1229
currency: "USD",
@@ -20,9 +37,8 @@ export default () => {
2037
const { top, bottom } = useSafeAreaInsets();
2138
const [data, setData] = useState<[number, number][]>(generateRandomData());
2239

23-
const handlePress = useCallback(() => {
24-
setData(generateRandomData());
25-
}, []);
40+
// Randomize the data
41+
const handlePress = useCallback(() => setData(generateRandomData()), []);
2642

2743
return (
2844
<View style={[styles.container, { paddingTop: top, paddingBottom: bottom }]}>
@@ -32,6 +48,8 @@ export default () => {
3248
style={styles.chart}
3349
TopAxisLabel={AxisLabel}
3450
BottomAxisLabel={AxisLabel}
51+
onPanGestureChange={onGestureChangeWorklet}
52+
onHoverGestureChange={onHoverChangeWorklet}
3553
/>
3654
</View>
3755
);

example/components/Banner.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { BannerComponentProps } from "@codeherence/react-native-graph";
2-
import { Text, useFont } from "@shopify/react-native-skia";
1+
// import { BannerComponentProps } from "@codeherence/react-native-graph";
2+
// import { Text, useFont } from "@shopify/react-native-skia";
33

4-
const robotoMedium = require("../public/fonts/Roboto/Roboto-Medium.ttf");
4+
// const robotoMedium = require("../public/fonts/Roboto/Roboto-Medium.ttf");
55

6-
export const Banner: React.FC<BannerComponentProps> = ({ text }) => {
7-
const font = useFont(robotoMedium, 24);
8-
return <Text x={0} y={0} font={font} text={text} />;
9-
};
6+
// export const Banner: React.FC<BannerComponentProps> = ({ text }) => {
7+
// const font = useFont(robotoMedium, 24);
8+
// return <Text x={0} y={0} font={font} text={text} />;
9+
// };

src/components/LineChart/Math.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ export interface GetYForXProps {
5050
precision?: number;
5151
}
5252

53-
export const getYForX = ({ path, x, precision = 2 }: GetYForXProps): number | undefined => {
53+
export const getYForX = ({ path, x, precision = 2 }: GetYForXProps): number => {
5454
"worklet";
55-
5655
return linearYForX({ path, x, precision });
5756
};
5857

src/components/LineChart/index.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ import { Canvas, Path } from "@shopify/react-native-skia";
22
import { useCallback, useMemo, useState } from "react";
33
import { LayoutChangeEvent, StyleSheet, View, ViewProps } from "react-native";
44
import { GestureDetector } from "react-native-gesture-handler";
5-
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
5+
import { useSharedValue } from "react-native-reanimated";
66

77
import { AxisLabelComponentProps, AxisLabelContainer } from "./AxisLabel";
88
import { Cursor } from "./Cursor";
9-
import { computePath, getYForX, type ComputePathProps, computeGraphData } from "./Math";
9+
import { computePath, type ComputePathProps, computeGraphData } from "./Math";
1010
import {
1111
DEFAULT_CURSOR_RADIUS,
1212
DEFAULT_CURVE_TYPE,
1313
DEFAULT_FORMATTER,
1414
DEFAULT_STROKE_WIDTH,
1515
} from "./constants";
16-
import { useGestures } from "./useGestures";
16+
import { useGestures, type UseGestureProps } from "./useGestures";
17+
18+
type FilterNull<T> = T extends null ? never : T;
1719

1820
export type LineChartProps = ViewProps & {
1921
/** Array of [x, y] points for the chart */
@@ -25,6 +27,13 @@ export type LineChartProps = ViewProps & {
2527
formatter?: (price: number) => string;
2628
TopAxisLabel?: React.FC<AxisLabelComponentProps>;
2729
BottomAxisLabel?: React.FC<AxisLabelComponentProps>;
30+
/** Callback when the pan gesture begins. This function must be a worklet function. */
31+
onPanGestureBegin?: FilterNull<UseGestureProps["onPanGestureBegin"]>;
32+
onPanGestureChange?: FilterNull<UseGestureProps["onPanGestureChange"]>;
33+
onPanGestureEnd?: FilterNull<UseGestureProps["onPanGestureEnd"]>;
34+
onHoverGestureBegin?: FilterNull<UseGestureProps["onHoverGestureBegin"]>;
35+
onHoverGestureChange?: FilterNull<UseGestureProps["onHoverGestureChange"]>;
36+
onHoverGestureEnd?: FilterNull<UseGestureProps["onHoverGestureEnd"]>;
2837
};
2938

3039
export const LineChart: React.FC<LineChartProps> = ({
@@ -35,14 +44,20 @@ export const LineChart: React.FC<LineChartProps> = ({
3544
formatter = DEFAULT_FORMATTER,
3645
TopAxisLabel = null,
3746
BottomAxisLabel = null,
47+
onPanGestureBegin = null,
48+
onPanGestureChange = null,
49+
onPanGestureEnd = null,
50+
onHoverGestureBegin = null,
51+
onHoverGestureChange = null,
52+
onHoverGestureEnd = null,
3853
...viewProps
3954
}) => {
4055
const [width, setWidth] = useState(0);
4156
const [height, setHeight] = useState(0);
4257

4358
// Initially -cursorRadius so that the cursor is offscreen
4459
const x = useSharedValue(-cursorRadius);
45-
const gestures = useGestures({ x, cursorRadius });
60+
const y = useSharedValue(0);
4661

4762
// We separate the computation of the data from the rendering. This is so that these values are
4863
// not recomputed when the width or height of the chart changes, but only when the points change.
@@ -52,9 +67,21 @@ export const LineChart: React.FC<LineChartProps> = ({
5267
return computePath({ ...data, width, height, cursorRadius, curveType });
5368
}, [data, width, height, cursorRadius, curveType]);
5469

55-
const y = useDerivedValue(() => {
56-
return path ? getYForX({ path, x: x.value }) ?? 0 : 0;
57-
}, [path]);
70+
const gestures = useGestures({
71+
x,
72+
y,
73+
path,
74+
height,
75+
minValue: data.minValue,
76+
maxValue: data.maxValue,
77+
cursorRadius,
78+
onPanGestureBegin,
79+
onPanGestureChange,
80+
onPanGestureEnd,
81+
onHoverGestureBegin,
82+
onHoverGestureChange,
83+
onHoverGestureEnd,
84+
});
5885

5986
const onLayout = useCallback((e: LayoutChangeEvent) => {
6087
setWidth(e.nativeEvent.layout.width);
Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,133 @@
1-
import { Gesture } from "react-native-gesture-handler";
2-
import { SharedValue } from "react-native-reanimated";
1+
import { SkPath } from "@shopify/react-native-skia";
2+
import {
3+
Gesture,
4+
type PanGestureHandlerEventPayload,
5+
type PanGestureChangeEventPayload,
6+
} from "react-native-gesture-handler";
7+
import { SharedValue, interpolate } from "react-native-reanimated";
38

4-
interface UseGestureProps {
9+
import { getYForX } from "./Math";
10+
11+
type PanGestureHandlerOnBeginEventPayload = {
12+
point: number;
13+
event: PanGestureHandlerEventPayload;
14+
};
15+
type PanGestureHandlerOnChangeEventPayload = {
16+
point: number;
17+
event: PanGestureHandlerEventPayload & PanGestureChangeEventPayload;
18+
};
19+
20+
// Extract Hover Gesture onBegin args since it isn't exported by rngh
21+
type HoverGestureOnBegin = ReturnType<typeof Gesture.Hover>["onBegin"];
22+
type HoverGestureOnBeginCallBack = Parameters<HoverGestureOnBegin>[0];
23+
type HoverGestureHandlerOnBeginEventPayload = {
24+
point: number;
25+
event: Parameters<HoverGestureOnBeginCallBack>[0];
26+
};
27+
28+
// Extract Hover Gesture onChange args since it isn't exported by rngh
29+
type HoverGestureOnChange = ReturnType<typeof Gesture.Hover>["onChange"];
30+
type HoverGestureOnChangeCallBack = Parameters<HoverGestureOnChange>[0];
31+
type HoverGestureHandlerOnChangeEventPayload = {
32+
point: number;
33+
event: Parameters<HoverGestureOnChangeCallBack>[0];
34+
};
35+
36+
// Extract Hover Gesture onEnd args since it isn't exported by rngh
37+
type HoverGestureOnEnd = ReturnType<typeof Gesture.Hover>["onEnd"];
38+
type HoverGestureOnEndCallBack = Parameters<HoverGestureOnEnd>[0];
39+
type HoverGestureHandlerOnEndEventPayload = Parameters<HoverGestureOnEndCallBack>[0];
40+
41+
export interface UseGestureProps {
542
x: SharedValue<number>;
43+
y: SharedValue<number>;
44+
path: SkPath;
45+
height: number;
46+
minValue: number;
47+
maxValue: number;
648
cursorRadius: number;
49+
/** Callback when the pan gesture begins. This function must be a worklet function. */
50+
onPanGestureBegin: ((payload: PanGestureHandlerOnBeginEventPayload) => void) | null;
51+
onPanGestureChange: ((payload: PanGestureHandlerOnChangeEventPayload) => void) | null;
52+
onPanGestureEnd: ((payload: PanGestureHandlerEventPayload) => void) | null;
53+
onHoverGestureBegin: ((payload: HoverGestureHandlerOnBeginEventPayload) => void) | null;
54+
onHoverGestureChange: ((payload: HoverGestureHandlerOnChangeEventPayload) => void) | null;
55+
onHoverGestureEnd: ((payload: HoverGestureHandlerOnEndEventPayload) => void) | null;
756
}
857

9-
export const useGestures = ({ x, cursorRadius }: UseGestureProps) => {
58+
/**
59+
* Returns the gesture handlers for the LineChart component.
60+
* @param param0 - The props to allow the gesture handlers to interact with the LineChart component.
61+
* @returns The gesture handlers for the LineChart component.
62+
*/
63+
export const useGestures = ({
64+
x,
65+
y,
66+
path,
67+
height,
68+
minValue,
69+
maxValue,
70+
cursorRadius,
71+
onPanGestureBegin,
72+
onPanGestureChange,
73+
onPanGestureEnd,
74+
onHoverGestureBegin,
75+
onHoverGestureChange,
76+
onHoverGestureEnd,
77+
}: UseGestureProps) => {
1078
const panGesture = Gesture.Pan()
11-
.onBegin((evt) => (x.value = evt.x))
12-
.onChange((evt) => (x.value = evt.x))
13-
.onEnd(() => (x.value = -cursorRadius));
79+
.onBegin((event) => {
80+
x.value = event.x;
81+
y.value = getYForX({ path, x: event.x });
82+
const point = interpolate(
83+
y.value,
84+
[cursorRadius, height - cursorRadius],
85+
[maxValue, minValue]
86+
);
87+
if (onPanGestureBegin) onPanGestureBegin({ event, point });
88+
})
89+
.onChange((event) => {
90+
x.value = event.x;
91+
y.value = getYForX({ path, x: event.x });
92+
const point = interpolate(
93+
y.value,
94+
[cursorRadius, height - cursorRadius],
95+
[maxValue, minValue]
96+
);
97+
if (onPanGestureChange) onPanGestureChange({ event, point });
98+
})
99+
.onEnd((event) => {
100+
x.value = -cursorRadius;
101+
if (onPanGestureEnd) onPanGestureEnd(event);
102+
});
14103

15104
const hoverGesture = Gesture.Hover()
16-
.onBegin((evt) => (x.value = evt.x))
17-
.onChange((evt) => (x.value = evt.x))
18-
.onEnd(() => (x.value = -cursorRadius));
105+
.onBegin((event) => {
106+
x.value = event.x;
107+
y.value = getYForX({ path, x: event.x });
108+
const point = interpolate(
109+
y.value,
110+
[cursorRadius, height - cursorRadius],
111+
[maxValue, minValue]
112+
);
113+
if (onHoverGestureBegin) onHoverGestureBegin({ event, point });
114+
})
115+
.onChange((event) => {
116+
x.value = event.x;
117+
y.value = getYForX({ path, x: event.x });
118+
const point = interpolate(
119+
y.value,
120+
[cursorRadius, height - cursorRadius],
121+
[maxValue, minValue]
122+
);
123+
if (onHoverGestureChange) onHoverGestureChange({ event, point });
124+
})
125+
.onEnd((event) => {
126+
x.value = -cursorRadius;
127+
if (onHoverGestureEnd) onHoverGestureEnd(event);
128+
});
19129

130+
// We return a composed gesture that listens to both pan and hover gestures. This is to
131+
// allow the chart component to work on both touch and mouse devices.
20132
return Gesture.Race(hoverGesture, panGesture);
21133
};

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export { LineChart } from "./components/SkiaComponents";
2-
export { type BannerComponentProps } from "./components/LineChart/Banner";
32
export { type AxisLabelComponentProps } from "./components/LineChart/AxisLabel";

0 commit comments

Comments
 (0)