react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
132 lines (131 loc) • 5.77 kB
JavaScript
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 }));