Skip to content

Commit

Permalink
Add ability to use custom gestures (#417)
Browse files Browse the repository at this point in the history
  • Loading branch information
keithluchtel authored Nov 20, 2024
1 parent 29c6ed2 commit bd8c92d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-mugs-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"victory-native": minor
---

Add ability to pass use custom gesture definitions.
5 changes: 5 additions & 0 deletions example/app/consts/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ export const ChartRoutes: {
description: "This is an example of pan zoom functionality",
path: "/pan-zoom",
},
{
title: "Custom Gesture",
description: "Basic chart example with a custom tap gesture.",
path: "/custom-gesture",
},
];

if (__DEV__) {
Expand Down
79 changes: 79 additions & 0 deletions example/app/custom-gesture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from "react";
import { StyleSheet, View, SafeAreaView } from "react-native";
import {
type CartesianActionsHandle,
CartesianChart,
Line,
useChartPressState,
} from "victory-native";
import { Circle, useFont } from "@shopify/react-native-skia";
import type { SharedValue } from "react-native-reanimated";
import { Gesture } from "react-native-gesture-handler";
import { useRef } from "react";
import { InfoCard } from "example/components/InfoCard";
import { appColors } from "./consts/colors";
import inter from "../assets/inter-medium.ttf";

export default function CustomGestureScreen() {
const font = useFont(inter, 12);
const { state, isActive } = useChartPressState({ x: 0, y: { highTmp: 0 } });
const ref = useRef<CartesianActionsHandle<typeof state>>(null);

const tapGesture = Gesture.Tap().onStart((e) => {
state.isActive.value = true;
ref.current?.handleTouch(state, e.x, e.y);
});
const composed = Gesture.Race(tapGesture);

return (
<SafeAreaView style={styles.safeView}>
<View style={{ flex: 1, maxHeight: 400, padding: 32 }}>
<CartesianChart
data={DATA}
xKey="day"
yKeys={["highTmp"]}
axisOptions={{
font,
}}
customGestures={composed}
actionsRef={ref}
>
{({ points }) => (
<>
<Line points={points.highTmp} color="red" strokeWidth={3} />
{isActive && (
<ToolTip x={state.x.position} y={state.y.highTmp.position} />
)}
</>
)}
</CartesianChart>
</View>
<View style={{ paddingHorizontal: 20 }}>
<InfoCard>
The tap gesture for selecting a point on the chart is a custom gesture
implemented on the page and utilizes a chart action to handle point
matching
</InfoCard>
</View>
</SafeAreaView>
);
}

function ToolTip({ x, y }: { x: SharedValue<number>; y: SharedValue<number> }) {
return <Circle cx={x} cy={y} r={8} color="black" />;
}

const DATA = Array.from({ length: 31 }, (_, i) => ({
day: i,
highTmp: 40 + 30 * Math.random(),
}));

const styles = StyleSheet.create({
safeView: {
flex: 1,
backgroundColor: appColors.viewBackground.light,
$dark: {
backgroundColor: appColors.viewBackground.dark,
},
},
});
33 changes: 30 additions & 3 deletions lib/src/cartesian/CartesianChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { type LayoutChangeEvent } from "react-native";
import { Canvas, Group, rect } from "@shopify/react-native-skia";
import { useSharedValue } from "react-native-reanimated";
import {
type ComposedGesture,
Gesture,
GestureHandlerRootView,
type TouchData,
} from "react-native-gesture-handler";

import { type MutableRefObject } from "react";
import { ZoomTransform } from "d3-zoom";
import type {
AxisProps,
Expand All @@ -26,7 +27,10 @@ import { transformInputData } from "./utils/transformInputData";
import { findClosestPoint } from "../utils/findClosestPoint";
import { valueFromSidedNumber } from "../utils/valueFromSidedNumber";
import { asNumber } from "../utils/asNumber";
import type { ChartPressState } from "./hooks/useChartPressState";
import type {
ChartPressState,
ChartPressStateInit,
} from "./hooks/useChartPressState";
import { useFunctionRef } from "../hooks/useFunctionRef";
import { CartesianChartProvider } from "./contexts/CartesianChartContext";
import { normalizeYAxisTicks } from "../utils/normalizeYAxisTicks";
Expand All @@ -47,6 +51,13 @@ import {
import { downsampleTicks } from "../utils/tickHelpers";
import { GestureHandler } from "../shared/GestureHandler";

export type CartesianActionsHandle<T = undefined> =
T extends ChartPressState<ChartPressStateInit>
? {
handleTouch: (v: T, x: number, y: number) => void;
}
: object;

type CartesianChartProps<
RawData extends Record<string, unknown>,
XK extends keyof InputFields<RawData>,
Expand Down Expand Up @@ -82,6 +93,14 @@ type CartesianChartProps<
transformConfig?: {
pan?: PanTransformGestureConfig;
};
customGestures?: ComposedGesture;
actionsRef?: MutableRefObject<CartesianActionsHandle<
| ChartPressState<{
x: InputFields<RawData>[XK];
y: Record<YK, number>;
}>
| undefined
> | null>;
};

export function CartesianChart<
Expand Down Expand Up @@ -121,6 +140,8 @@ function CartesianChartContent<
frame,
transformState,
transformConfig,
customGestures,
actionsRef,
}: CartesianChartProps<RawData, XK, YK>) {
const [size, setSize] = React.useState({ width: 0, height: 0 });
const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] =
Expand Down Expand Up @@ -296,6 +317,12 @@ function CartesianChartContent<
lastIdx.value = idx;
};

if (actionsRef) {
actionsRef.current = {
handleTouch,
};
}

/**
* Touch gesture is a modified Pan gesture handler that allows for multiple presses:
* - Using Pan Gesture handler effectively _just_ for the .activateAfterLongPress functionality.
Expand Down Expand Up @@ -573,7 +600,7 @@ function CartesianChartContent<
</Canvas>
);

let composed = Gesture.Race();
let composed = customGestures ?? Gesture.Race();
if (transformState) {
composed = Gesture.Race(
composed,
Expand Down
5 changes: 4 additions & 1 deletion lib/src/cartesian/hooks/useChartPressState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const useChartPressState = <Init extends ChartPressStateInit>(
return { state, isActive };
};

type ChartPressStateInit = { x: InputFieldType; y: Record<string, number> };
export type ChartPressStateInit = {
x: InputFieldType;
y: Record<string, number>;
};
export type ChartPressState<Init extends ChartPressStateInit> = {
isActive: SharedValue<boolean>;
matchedIndex: SharedValue<number>;
Expand Down
5 changes: 4 additions & 1 deletion lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/**
* Cartesian chart exports (including useful types)
*/
export { CartesianChart } from "./cartesian/CartesianChart";
export {
CartesianChart,
type CartesianActionsHandle,
} from "./cartesian/CartesianChart";

export {
type InputDatum,
Expand Down
42 changes: 42 additions & 0 deletions website/docs/cartesian/cartesian-chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,48 @@ An optional configuration object for customizing transform behavior when `transf
}
```

### `customGestures`

The `customGestures` prop allows you to provide custom gesture handlers that will work alongside (or instead of) the default chart press gestures. It accepts a `ComposedGesture` from react-native-gesture-handler.

When both `customGestures` and `chartPressState` are provided, the gestures will be composed using `Gesture.Race()`, allowing either gesture to be active.

```ts
const tapGesture = Gesture.Tap().onStart((e) => {
state.isActive.value = true;
ref.current?.handleTouch(state, e.x, e.y);
});

const composed = Gesture.Race(tapGesture);
```

### `actionsRef`

The `actionsRef` prop allows you to get programmatic access to certain chart actions. It accepts a ref object that will be populated with methods to control chart behavior. Currently supported actions:

- `handleTouch`: Programmatically trigger the chart's touch handling behavior at specific coordinates. This is useful for programmatically highlighting specific data points.

Example usage:

```tsx
function MyChart() {
const { state } = useChartPressState({ x: 0, y: { highTmp: 0 } });
const actionsRef = useRef<CartesianActionsHandle<typeof state>>(null);

const highlightPoint = () => {
// Programmatically highlight a point at coordinates (100, 200)
actionsRef.current?.handleTouch(state, 100, 200);
};

return (
<CartesianChart
actionsRef={actionsRef}
// ... other props
/>
);
}
```

## Render Function Fields

The `CartesianChart` `children` and `renderOutside` render functions both have a single argument that is an object with the following fields.
Expand Down

0 comments on commit bd8c92d

Please sign in to comment.