diff --git a/README.md b/README.md index 0f702f7..1ff4843 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,34 @@ Example: ``` See this [example `` component](./example/src/components/CustomSelectionDot.tsx). +### `events` + An array of events to be marked in the graph. The position is calculated based on the `date` property of each event relatively to `points` of the graph. + +### `EventComponent` + A component that is used to render an event. + + See this [example `` component](./example/src/components/GraphEvent.tsx). + +### `EventTooltipComponent` + An additional event component that is rendered if the `SelectionDot` overlaps an `Event`. + See this [example `` component](./example/src/components/GraphEventTooltip.tsx). +### `onEventHover` + Callback called when an `Event` is hovered on. + +Example: +```jsx + +``` +> Events related props require `animated` and `enablePanGesture` to be `true`. + + ## Sponsor diff --git a/example/src/components/GraphEvent.tsx b/example/src/components/GraphEvent.tsx new file mode 100644 index 0000000..bcfa2e4 --- /dev/null +++ b/example/src/components/GraphEvent.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react' +import { + runOnJS, + useDerivedValue, + useSharedValue, + withSpring, + withTiming, +} from 'react-native-reanimated' + +import { + Circle, + Group, + TwoPointConicalGradient, + vec, +} from '@shopify/react-native-skia' +import { EventComponentProps } from '../../../src/LineGraphProps' + +const EVENT_SIZE = 6 +const ACTIVE_EVENT_SIZE = 8 +const ENTERING_ANIMATION_DURATION = 750 + +export function GraphEvent({ + isGraphActive, + fingerX, + eventX, + eventY, + color, + index, + onEventHover, +}: EventComponentProps) { + const isEventActive = useDerivedValue(() => { + // If the finger is on X position of the event. + if ( + isGraphActive.value && + Math.abs(fingerX.value - eventX) < ACTIVE_EVENT_SIZE + ) { + if (onEventHover) runOnJS(onEventHover)(index, true) + + return true + } + + if (onEventHover) runOnJS(onEventHover)(index, false) + return false + }) + + const dotRadius = useDerivedValue(() => + withSpring(isEventActive.value ? ACTIVE_EVENT_SIZE : EVENT_SIZE) + ) + const gradientEndRadius = useDerivedValue(() => + withSpring(dotRadius.value / 2) + ) + const animatedOpacity = useSharedValue(0) + + useEffect(() => { + // Entering opacity animation triggered on the first render. + animatedOpacity.value = withTiming(1, { + duration: ENTERING_ANIMATION_DURATION, + }) + }, [animatedOpacity]) + + return ( + + + + + + ) +} diff --git a/example/src/components/GraphEventTooltip.tsx b/example/src/components/GraphEventTooltip.tsx new file mode 100644 index 0000000..33e3712 --- /dev/null +++ b/example/src/components/GraphEventTooltip.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { Dimensions, Platform, StyleSheet, Text, View } from 'react-native' +import { EventTooltipComponentProps } from '../../../src/LineGraphProps' + +export type TransactionEventTooltipProps = EventTooltipComponentProps<{}> + +const SCREEN_WIDTH = Dimensions.get('screen').width +const ANIMATION_DURATION = 200 +const TOOLTIP_LEFT_OFFSET = 25 +const TOOLTIP_RIGHT_OFFSET = 145 + +export const GraphEventTooltip = ({ + eventX, + eventY, +}: TransactionEventTooltipProps) => { + const tooltipPositionStyle = { + left: + eventX > SCREEN_WIDTH / 2 + ? eventX - TOOLTIP_RIGHT_OFFSET + : eventX + TOOLTIP_LEFT_OFFSET, + top: eventY, + } + return ( + + + + Here you can display {'\n'} + any information you {'\n'} + want about the event. + + + + ) +} + +const styles = StyleSheet.create({ + tooltip: { + position: 'absolute', + backgroundColor: 'white', + paddingHorizontal: 10, + + borderRadius: 20, + // add shadow based on platform + ...Platform.select({ + ios: { + shadowColor: 'black', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 3, + }, + android: { + elevation: 3, + }, + }), + }, + content: { + paddingVertical: 12, + }, + textNote: { + color: 'gray', + fontSize: 10, + }, +}) diff --git a/example/src/data/GraphData.ts b/example/src/data/GraphData.ts index f0c3fd8..7ac11c1 100644 --- a/example/src/data/GraphData.ts +++ b/example/src/data/GraphData.ts @@ -1,4 +1,4 @@ -import type { GraphPoint } from '../../../src/LineGraphProps' +import type { GraphEvent, GraphPoint } from '../../../src/LineGraphProps' import gaussian from 'gaussian' function weightedRandom(mean: number, variance: number): number { @@ -7,7 +7,7 @@ function weightedRandom(mean: number, variance: number): number { return distribution.ppf(Math.random()) } -export function generateRandomGraphData(length: number): GraphPoint[] { +export function generateRandomGraphPoints(length: number): GraphPoint[] { return Array(length) .fill(0) .map((_, index) => ({ @@ -18,6 +18,28 @@ export function generateRandomGraphData(length: number): GraphPoint[] { })) } +export function generateRandomGraphEvents( + length: number, + points: GraphPoint[] +): GraphEvent[] { + const firstPointTimestamp = points[0]?.date.getTime() + const lastPointTimestamp = points[points.length - 1]?.date.getTime() + + if (!firstPointTimestamp || !lastPointTimestamp) { + return [] + } + return Array(length) + .fill(0) + .map((_) => ({ + date: new Date( // Get a random date between the two defined timestamps. + Math.floor( + Math.random() * (lastPointTimestamp - firstPointTimestamp + 1) + ) + firstPointTimestamp + ), + payload: {}, + })) +} + export function generateSinusGraphData(length: number): GraphPoint[] { return Array(length) .fill(0) diff --git a/example/src/screens/GraphPage.tsx b/example/src/screens/GraphPage.tsx index 8dbfd9b..f1b1bf2 100644 --- a/example/src/screens/GraphPage.tsx +++ b/example/src/screens/GraphPage.tsx @@ -4,16 +4,21 @@ import { LineGraph } from 'react-native-graph' import StaticSafeAreaInsets from 'react-native-static-safe-area-insets' import type { GraphRange } from '../../../src/LineGraphProps' import { SelectionDot } from '../components/CustomSelectionDot' +import { GraphEvent } from '../components/GraphEvent' +import { GraphEventTooltip } from '../components/GraphEventTooltip' import { Toggle } from '../components/Toggle' import { - generateRandomGraphData, + generateRandomGraphEvents, + generateRandomGraphPoints, generateSinusGraphData, } from '../data/GraphData' import { useColors } from '../hooks/useColors' import { hapticFeedback } from '../utils/HapticFeedback' const POINT_COUNT = 70 -const POINTS = generateRandomGraphData(POINT_COUNT) +const POINTS = generateRandomGraphPoints(POINT_COUNT) +const EVENT_COUNT = 10 +const EVENTS = generateRandomGraphEvents(EVENT_COUNT, POINTS) const COLOR = '#6a7ee7' const GRADIENT_FILL_COLORS = ['#7476df5D', '#7476df4D', '#7476df00'] const SMALL_POINTS = generateSinusGraphData(9) @@ -30,11 +35,17 @@ export function GraphPage() { const [enableRange, setEnableRange] = useState(false) const [enableIndicator, setEnableIndicator] = useState(false) const [indicatorPulsating, setIndicatorPulsating] = useState(false) + const [enableEvents, setEnableEvents] = useState(false) + const [enableEventTooltip, setEnableEventTooltip] = useState(false) const [points, setPoints] = useState(POINTS) + const [events, setEvents] = useState(EVENTS) const refreshData = useCallback(() => { - setPoints(generateRandomGraphData(POINT_COUNT)) + const freshPoints = generateRandomGraphPoints(POINT_COUNT) + const freshEvents = generateRandomGraphEvents(EVENT_COUNT, freshPoints) + setPoints(freshPoints) + setEvents(freshEvents) hapticFeedback('impactLight') }, []) @@ -100,6 +111,9 @@ export function GraphPage() { enableIndicator={enableIndicator} horizontalPadding={enableIndicator ? 15 : 0} indicatorPulsating={indicatorPulsating} + events={enableEvents ? events : []} + EventComponent={enableEvents ? GraphEvent : null} + EventTooltipComponent={enableEventTooltip ? GraphEventTooltip : null} />