import React, {useEffect} from 'react'; import {ColorValue, I18nManager} from 'react-native'; import Animated, { SharedValue, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import ActivePointComponentWrapper from './ActivePointComponentWrapper'; import { ActivePointComponent, ActivePointComponentSharedValue, DataPoint, } from '../types'; import {PathObject, useForceReRender} from '../utils'; import {scheduleOnRN} from 'react-native-worklets'; type PositionSharedValue = {x: number; y: number; y2?: number}; const ActivePoint = ({ data, activeTouch, width, height, activePointComponent, activePointComponentWithSharedValue, activeIndex, path, onPointChange, color, borderColor, borderWidth, radius, showVerticalLine, showActivePointCircle, verticalLineColor, verticalLineOpacity, verticalLineWidth, verticalLineDashArray, animateTransition, }: { data: DataPoint[]; activeTouch: SharedValue; width: number; height: number; activePointComponent?: ActivePointComponent; activePointComponentWithSharedValue?: ActivePointComponentSharedValue; activeIndex: SharedValue; path: PathObject; onPointChange?: (point?: DataPoint) => void; color: ColorValue; borderColor: ColorValue; borderWidth: number; radius: number; showVerticalLine: boolean; showActivePointCircle: boolean; verticalLineColor: ColorValue; verticalLineOpacity: number; verticalLineWidth: number; verticalLineDashArray: number[]; animateTransition: boolean; }) => { const positions = useSharedValue([]); const activePointSV = useSharedValue({ x: 0, y: 0, extraData: { isInitialPoint: true, date: undefined, }, }); const pointOpacity = useSharedValue(0); const lineOpacitySV = useSharedValue(0); const activePointPositionX = useSharedValue(-radius); const activePointPositionY = useSharedValue(-radius); const forceRerender = useForceReRender(); // forcing a re-render after x ms to fix sharedValues not causing a rerender. useEffect(() => { setTimeout(() => { forceRerender(); }, 200); }, []); useEffect(() => { const newPositions: PositionSharedValue[] = []; data.forEach(item => { const y = item?.y2 !== undefined ? path.y(item.y - item.y2) // check for ranged chart : path.y(item.y); const x = path.x(item.x); if (x !== undefined && y !== undefined) { newPositions.push({ x, y, }); } }); positions.value = newPositions; }, [data]); useAnimatedReaction( () => { return { activeIndex: activeIndex.value, activeTouch: activeTouch.value, }; }, (current, previous) => { const currentIndexData = data[current.activeIndex]; if (currentIndexData?.disableActivePoint !== true) { if ( current.activeIndex !== null && current.activeTouch === true ) { try { if (onPointChange) { scheduleOnRN(onPointChange, currentIndexData); } } catch (_) { // error } } // active point position if ( current.activeIndex !== previous?.activeIndex || currentIndexData?.y !== activePointPositionY?.value ) { const point = positions.value[activeIndex.value]; const y = point?.y; const x = point?.x; if (x !== undefined && y !== undefined) { if ( activePointPositionX.value !== x || activePointPositionY.value !== y ) { activePointPositionX.value = x; activePointPositionY.value = y; } } else if ( activePointPositionX.value !== -radius || activePointPositionY.value !== -radius ) { activePointPositionX.value = -radius; activePointPositionY.value = -radius; } } if (current.activeTouch) { activePointSV.value = currentIndexData; } } if (current.activeTouch === true) { if ( activePointSV.value === undefined && pointOpacity.value === 1 ) { pointOpacity.value = 0; lineOpacitySV.value = 0; } if ( activePointSV.value !== undefined && pointOpacity.value === 0 ) { pointOpacity.value = 1; lineOpacitySV.value = 1; } } // point and line animations if (current.activeTouch !== previous?.activeTouch) { // if user touched in a place where there is no point (for example a point with disableActivePoint: true) if ( activePointSV.value === undefined || activePointSV.value?.extraData?.isInitialPoint === true ) { return; } if (current.activeTouch === true) { pointOpacity.value = 1; lineOpacitySV.value = verticalLineOpacity; } else { pointOpacity.value = 0; lineOpacitySV.value = 0; } } }, [activeIndex, data, activeTouch], ); const verticalLineActivePosition = useSharedValue( activePointPositionX.value || 0, ); const verticalLineStyle = useAnimatedStyle(() => { verticalLineActivePosition.value = withTiming( activePointPositionX.value || 0, {duration: animateTransition ? 200 : 0}, ); return { position: 'absolute' as const, left: !I18nManager.isRTL ? verticalLineActivePosition.value - verticalLineWidth / 2 : undefined, right: !I18nManager.isRTL ? undefined : verticalLineActivePosition.value - verticalLineWidth / 2, top: 0, width: verticalLineWidth, height, opacity: lineOpacitySV.value, }; }); const activePointStyle = useAnimatedStyle(() => { return { position: 'absolute' as const, left: I18nManager.isRTL ? undefined : activePointPositionX.value - (radius + borderWidth), right: I18nManager.isRTL ? activePointPositionX.value - (radius + borderWidth) : undefined, top: activePointPositionY.value - (radius + borderWidth), width: (radius + borderWidth) * 2, height: (radius + borderWidth) * 2, borderRadius: radius + borderWidth, opacity: pointOpacity.value, }; }); const dashGap = verticalLineDashArray.length >= 2 ? verticalLineDashArray[1] : 0; const dashLength = verticalLineDashArray.length >= 1 ? verticalLineDashArray[0] : 0; const isDashed = dashLength && dashGap && dashLength > 0 && dashGap > 0; return ( <> {showVerticalLine && ( )} {showActivePointCircle && ( )} {(activePointComponent || activePointComponentWithSharedValue) && ( )} ); }; export default ActivePoint;