import React, {forwardRef, useEffect, useImperativeHandle} from 'react'; import { Gesture, GestureDetector, PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; import {useSharedValue} from 'react-native-reanimated'; import {EXTRA_CONFIG, LINE_CHART} from './defaults'; import { DataPoint, ExtraConfig, LineChart as LineChartProps, LineChartRef, } from './types'; import {isEqual} from './utils'; import {View} from 'react-native'; import ChartWrapper from './ChartWrapper'; import {scheduleOnRN} from 'react-native-worklets'; const getExtraConfig = (extraConfig: ExtraConfig): ExtraConfig => { return { alwaysShowActivePoint: extraConfig.alwaysShowActivePoint !== undefined ? extraConfig.alwaysShowActivePoint : EXTRA_CONFIG.alwaysShowActivePoint, hideActivePointOnBlur: extraConfig.hideActivePointOnBlur !== undefined ? extraConfig.hideActivePointOnBlur : EXTRA_CONFIG.hideActivePointOnBlur, alwaysStartYAxisFromZero: extraConfig.alwaysStartYAxisFromZero !== undefined ? extraConfig.alwaysStartYAxisFromZero : EXTRA_CONFIG.alwaysStartYAxisFromZero, initialActivePoint: extraConfig.initialActivePoint !== undefined ? extraConfig.initialActivePoint : EXTRA_CONFIG.initialActivePoint, simultaneousHandlers: extraConfig.simultaneousHandlers !== undefined ? extraConfig.simultaneousHandlers : EXTRA_CONFIG.simultaneousHandlers, endSpacing: extraConfig.endSpacing !== undefined ? extraConfig.endSpacing : EXTRA_CONFIG.endSpacing, calculateChartYAxisMinMax: extraConfig.calculateChartYAxisMinMax || EXTRA_CONFIG.calculateChartYAxisMinMax, calculateChartXAxisMinMax: extraConfig.calculateChartXAxisMinMax || EXTRA_CONFIG.calculateChartXAxisMinMax, activeOffsetX: extraConfig.activeOffsetX || EXTRA_CONFIG.activeOffsetX, animationConfig: extraConfig.animationConfig || EXTRA_CONFIG.animationConfig, }; }; const LineChart = forwardRef( ( { height = LINE_CHART.height, width = LINE_CHART.width, extraConfig, backgroundColor = LINE_CHART.backgroundColor, onPointFocus = LINE_CHART.onPointFocus, onPointLoseFocus = LINE_CHART.onPointLoseFocus, activeLineIndex = LINE_CHART.activeLineIndex, activePointSharedValue, lines = [], }, ref, ) => { const svgHeight = height; const svgWidth = width; const activeTouchX = useSharedValue(0); const activeTouch = useSharedValue(false); const [ isSimultaneousHandlersEnabled, setIsSimultaneousHandlersEnabled, ] = React.useState(true); extraConfig = getExtraConfig(extraConfig || {}); const {alwaysShowActivePoint, hideActivePointOnBlur} = extraConfig; const onPointChange = (point?: DataPoint) => { if (point) { if (onPointFocus) { onPointFocus(point); } if (activePointSharedValue) { activePointSharedValue.value = point; } } }; const onPointLoseFocusLocal = () => { if (onPointLoseFocus) { onPointLoseFocus(); } if (activePointSharedValue && !extraConfig?.alwaysShowActivePoint) { setTimeout(() => { activePointSharedValue.value = undefined; }, 10); } }; useEffect(() => { if (extraConfig?.initialActivePoint) { activeTouch.value = true; if (onPointFocus) { const point = lines[activeLineIndex]?.data ? lines[activeLineIndex]?.data[ extraConfig?.initialActivePoint as number ] : undefined; if (point) { onPointFocus(point); } } } else { onPointLoseFocusLocal(); } }, [lines[activeLineIndex]?.data]); const onPanUpdate = (e: PanGestureHandlerEventPayload) => { 'worklet'; if (isSimultaneousHandlersEnabled === true) { scheduleOnRN(setIsSimultaneousHandlersEnabled, false); } activeTouch.value = true; activeTouchX.value = e.x; }; const onPanEnd = () => { 'worklet'; if (!activeTouch.value) return; scheduleOnRN(setIsSimultaneousHandlersEnabled, true); if ( alwaysShowActivePoint === false || hideActivePointOnBlur === true ) { activeTouch.value = false; } scheduleOnRN(onPointLoseFocusLocal); }; const panGesture = extraConfig.simultaneousHandlers && isSimultaneousHandlersEnabled ? Gesture.Pan() .simultaneousWithExternalGesture( extraConfig.simultaneousHandlers, ) .activeOffsetX( extraConfig?.activeOffsetX || EXTRA_CONFIG.activeOffsetX, ) .onBegin(onPanUpdate) .onUpdate(onPanUpdate) .onTouchesUp(onPanEnd) .onTouchesCancelled(onPanEnd) : Gesture.Pan() .activeOffsetX( extraConfig?.activeOffsetX || EXTRA_CONFIG.activeOffsetX, ) .onBegin(onPanUpdate) .onUpdate(onPanUpdate) .onTouchesUp(onPanEnd) .onTouchesCancelled(onPanEnd); useImperativeHandle(ref, () => ({ setActiveIndex(index) { const activeLine = lines[activeLineIndex]; if ( index !== undefined && activeLine && activeLine.data && Array.isArray(activeLine.data) ) { activeTouch.value = true; activeTouchX.value = index * (width / (activeLine.data.length - 1)); } else { activeTouch.value = false; } }, })); return ( ); }, ); /** @ignore */ export const MemoizedLineChart = React.memo( LineChart, (previousProps, nextProps) => { if (previousProps.activeLineIndex !== nextProps.activeLineIndex) { return false; } if (!isEqual(previousProps.lines, nextProps.lines)) { return false; } if (previousProps.backgroundColor !== nextProps.backgroundColor) { return false; } return true; }, ); export default MemoizedLineChart;