diff --git a/.changeset/moody-mugs-thank.md b/.changeset/moody-mugs-thank.md new file mode 100644 index 00000000..d217a6ea --- /dev/null +++ b/.changeset/moody-mugs-thank.md @@ -0,0 +1,5 @@ +--- +"victory-native": minor +--- + +Add ability to pass use custom gesture definitions. diff --git a/example/app/consts/routes.ts b/example/app/consts/routes.ts index 8d81b479..9545e372 100644 --- a/example/app/consts/routes.ts +++ b/example/app/consts/routes.ts @@ -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__) { diff --git a/example/app/custom-gesture.tsx b/example/app/custom-gesture.tsx new file mode 100644 index 00000000..96820469 --- /dev/null +++ b/example/app/custom-gesture.tsx @@ -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>(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 ( + + + + {({ points }) => ( + <> + + {isActive && ( + + )} + + )} + + + + + 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 + + + + ); +} + +function ToolTip({ x, y }: { x: SharedValue; y: SharedValue }) { + return ; +} + +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, + }, + }, +}); diff --git a/lib/src/cartesian/CartesianChart.tsx b/lib/src/cartesian/CartesianChart.tsx index dc045680..69c6cbf7 100644 --- a/lib/src/cartesian/CartesianChart.tsx +++ b/lib/src/cartesian/CartesianChart.tsx @@ -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, @@ -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"; @@ -47,6 +51,13 @@ import { import { downsampleTicks } from "../utils/tickHelpers"; import { GestureHandler } from "../shared/GestureHandler"; +export type CartesianActionsHandle = + T extends ChartPressState + ? { + handleTouch: (v: T, x: number, y: number) => void; + } + : object; + type CartesianChartProps< RawData extends Record, XK extends keyof InputFields, @@ -82,6 +93,14 @@ type CartesianChartProps< transformConfig?: { pan?: PanTransformGestureConfig; }; + customGestures?: ComposedGesture; + actionsRef?: MutableRefObject[XK]; + y: Record; + }> + | undefined + > | null>; }; export function CartesianChart< @@ -121,6 +140,8 @@ function CartesianChartContent< frame, transformState, transformConfig, + customGestures, + actionsRef, }: CartesianChartProps) { const [size, setSize] = React.useState({ width: 0, height: 0 }); const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] = @@ -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. @@ -573,7 +600,7 @@ function CartesianChartContent< ); - let composed = Gesture.Race(); + let composed = customGestures ?? Gesture.Race(); if (transformState) { composed = Gesture.Race( composed, diff --git a/lib/src/cartesian/hooks/useChartPressState.ts b/lib/src/cartesian/hooks/useChartPressState.ts index e5e73e01..491563ec 100644 --- a/lib/src/cartesian/hooks/useChartPressState.ts +++ b/lib/src/cartesian/hooks/useChartPressState.ts @@ -40,7 +40,10 @@ export const useChartPressState = ( return { state, isActive }; }; -type ChartPressStateInit = { x: InputFieldType; y: Record }; +export type ChartPressStateInit = { + x: InputFieldType; + y: Record; +}; export type ChartPressState = { isActive: SharedValue; matchedIndex: SharedValue; diff --git a/lib/src/index.ts b/lib/src/index.ts index b74b5f78..da210c4d 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -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, diff --git a/website/docs/cartesian/cartesian-chart.md b/website/docs/cartesian/cartesian-chart.md index 37342c2f..cf659b6a 100644 --- a/website/docs/cartesian/cartesian-chart.md +++ b/website/docs/cartesian/cartesian-chart.md @@ -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>(null); + + const highlightPoint = () => { + // Programmatically highlight a point at coordinates (100, 200) + actionsRef.current?.handleTouch(state, 100, 200); + }; + + return ( + + ); +} +``` + ## Render Function Fields The `CartesianChart` `children` and `renderOutside` render functions both have a single argument that is an object with the following fields.