UNPKG

react-native-chart-kit

Version:

Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.

132 lines (131 loc) 5.77 kB
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; /* eslint-disable react-hooks/refs -- PanResponder stores callbacks during creation; refs keep active pinch state stable across controlled viewport rerenders. */ import { useMemo, useRef } from "react"; import { PanResponder, View } from "react-native"; import { resolveChartViewportWindowFromZoom } from "../../core/index"; import { getChartViewportPinchZoomFactor, resolveChartViewportInteractionConfig } from "./config"; const minPinchDistance = 4; const isSameViewportWindow = (first, second) => first.startIndex === second.startIndex && first.endIndex === second.endIndex; const getTouches = (event) => event.nativeEvent.touches .map((touch) => ({ locationX: touch.locationX, locationY: touch.locationY })) .filter((touch) => Number.isFinite(touch.locationX) && Number.isFinite(touch.locationY)); const getPinchMetrics = (event) => { const [first, second] = getTouches(event); if (!first || !second) return undefined; const dx = second.locationX - first.locationX; const dy = second.locationY - first.locationY; return { distance: Math.hypot(dx, dy), focalX: (first.locationX + second.locationX) / 2 }; }; const getAnchorIndex = ({ focalX, plotBounds, viewportWindow }) => { if (!Number.isFinite(focalX) || plotBounds.width <= 0) { return undefined; } const plotX = Math.min(Math.max(focalX - plotBounds.x, 0), plotBounds.width); const ratio = plotX / plotBounds.width; return (viewportWindow.startIndex + Math.max(0, viewportWindow.visibleCount - 1) * ratio); }; const emitViewportChange = ({ nextWindow, onViewportChange }) => { onViewportChange?.({ viewport: { startIndex: nextWindow.startIndex, endIndex: nextWindow.endIndex }, startIndex: nextWindow.startIndex, endIndex: nextWindow.endIndex, visibleCount: nextWindow.visibleCount, itemCount: nextWindow.itemCount, isWindowed: nextWindow.isWindowed, source: "mainPlot", interaction: "pinchZoom" }); }; export const useChartViewportPinchZoom = ({ dataLength, enabled, onViewportChange, plotBounds, viewportInteraction, viewportWindow }) => { const pinchStateRef = useRef(undefined); return useMemo(() => { const config = resolveChartViewportInteractionConfig(viewportInteraction); const canPinch = enabled && config.pinchZoom && onViewportChange !== undefined && dataLength > 1 && viewportWindow.visibleCount > 0 && plotBounds.width > 0; if (!canPinch) return undefined; const startPinch = (event) => { const metrics = getPinchMetrics(event); if (!metrics || metrics.distance < minPinchDistance) return; pinchStateRef.current = { lastWindow: viewportWindow, startDistance: metrics.distance, startFocalX: metrics.focalX, startWindow: viewportWindow }; config.onGestureStart?.({ interaction: "pinchZoom" }); }; const updatePinch = (event) => { if (!pinchStateRef.current) startPinch(event); const pinchState = pinchStateRef.current; const metrics = getPinchMetrics(event); if (!pinchState || !metrics || metrics.distance < minPinchDistance) { return; } const nextWindow = resolveChartViewportWindowFromZoom({ anchorIndex: getAnchorIndex({ focalX: pinchState.startFocalX, plotBounds, viewportWindow: pinchState.startWindow }), currentWindow: pinchState.startWindow, itemCount: dataLength, maxVisibleCount: config.maxVisiblePoints, minVisibleCount: config.minVisiblePoints, zoomFactor: getChartViewportPinchZoomFactor({ scale: metrics.distance / pinchState.startDistance, sensitivity: config.pinchSensitivity }) }); if (isSameViewportWindow(nextWindow, pinchState.lastWindow)) return; pinchStateRef.current = { ...pinchState, lastWindow: nextWindow }; emitViewportChange({ nextWindow, onViewportChange }); }; const endPinch = () => { if (!pinchStateRef.current) return; pinchStateRef.current = undefined; config.onGestureEnd?.({ interaction: "pinchZoom" }); }; const hasTwoTouches = (event) => getTouches(event).length >= 2; return PanResponder.create({ onMoveShouldSetPanResponderCapture: (event) => canPinch && hasTwoTouches(event), onPanResponderGrant: startPinch, onPanResponderMove: updatePinch, onPanResponderRelease: endPinch, onPanResponderTerminate: endPinch, onPanResponderTerminationRequest: () => !pinchStateRef.current, onShouldBlockNativeResponder: () => config.lockParentScroll, onStartShouldSetPanResponderCapture: (event) => canPinch && hasTwoTouches(event) }); }, [ dataLength, enabled, onViewportChange, plotBounds, viewportInteraction, viewportWindow ]); }; export const ChartViewportGesture = ({ children, gesture }) => gesture ? (_jsx(View, { collapsable: false, ...gesture.panHandlers, children: children })) : (_jsx(_Fragment, { children: children }));