react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
186 lines (185 loc) • 10.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useRef } from "react";
import { View } from "react-native";
import { resolveChartViewportWindowFromHandlePosition, resolveChartViewportWindowFromPosition } from "../../../core/index";
import { getLineChartRenderer } from "./renderer";
import { clamp } from "./utils";
export const LineChartRangeSelector = ({ config, dataLength, isVisible, model, onViewportChange, preventBrowserSelection, renderer: rendererProp, testID, viewportWindow, width }) => {
const interactionRef = useRef("move");
const rangeStartPoint = model.interactionPoints.find((point) => point.dataIndex === viewportWindow.startIndex);
const rangeEndPoint = model.interactionPoints.find((point) => point.dataIndex === Math.max(0, viewportWindow.endIndex - 1));
const windowX = rangeStartPoint?.x ?? model.boxes.plot.x;
const windowEndX = rangeEndPoint?.x ?? model.boxes.plot.x + model.boxes.plot.width;
const windowWidth = Math.max(10, windowEndX - windowX);
const emitViewportChange = useCallback((nextWindow, interaction) => {
onViewportChange?.({
viewport: {
startIndex: nextWindow.startIndex,
endIndex: nextWindow.endIndex
},
startIndex: nextWindow.startIndex,
endIndex: nextWindow.endIndex,
visibleCount: nextWindow.visibleCount,
itemCount: nextWindow.itemCount,
isWindowed: nextWindow.isWindowed,
source: "rangeSelector",
interaction
});
}, [onViewportChange]);
const isInteractive = isVisible && config.interactive && onViewportChange !== undefined;
const getInteraction = useCallback((locationX) => {
const handleHitSlop = Math.max(config.handleHitSlop, config.handleWidth * 2);
const startDistance = Math.abs(locationX - windowX);
const endDistance = Math.abs(locationX - windowEndX);
if (startDistance <= handleHitSlop && startDistance <= endDistance) {
return "resizeStart";
}
if (endDistance <= handleHitSlop) {
return "resizeEnd";
}
return "move";
}, [config.handleHitSlop, config.handleWidth, windowEndX, windowX]);
const handleInteraction = useCallback((event, interaction) => {
preventBrowserSelection(event);
if (!isInteractive) {
return;
}
const nextWindow = interaction === "move"
? resolveChartViewportWindowFromPosition({
itemCount: dataLength,
locationX: event.nativeEvent.locationX,
plotWidth: model.boxes.plot.width,
plotX: model.boxes.plot.x,
visibleCount: viewportWindow.visibleCount
})
: resolveChartViewportWindowFromHandlePosition({
currentWindow: viewportWindow,
handle: interaction === "resizeStart" ? "start" : "end",
itemCount: dataLength,
locationX: event.nativeEvent.locationX,
minVisibleCount: config.minVisiblePoints,
plotWidth: model.boxes.plot.width,
plotX: model.boxes.plot.x
});
if (nextWindow.startIndex === viewportWindow.startIndex &&
nextWindow.endIndex === viewportWindow.endIndex) {
return;
}
emitViewportChange(nextWindow, interaction);
}, [
config.minVisiblePoints,
dataLength,
emitViewportChange,
isInteractive,
model.boxes.plot.width,
model.boxes.plot.x,
preventBrowserSelection,
viewportWindow
]);
const responderProps = isInteractive
? {
onStartShouldSetResponderCapture: () => true,
onMoveShouldSetResponderCapture: () => true,
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: () => true,
onResponderGrant: (event) => {
const interaction = getInteraction(event.nativeEvent.locationX);
interactionRef.current = interaction;
config.onGestureStart?.({ interaction });
handleInteraction(event, interaction);
},
onResponderMove: (event) => {
handleInteraction(event, interactionRef.current);
},
onResponderRelease: () => {
config.onGestureEnd?.({ interaction: interactionRef.current });
interactionRef.current = "move";
},
onResponderTerminate: () => {
config.onGestureEnd?.({ interaction: interactionRef.current });
interactionRef.current = "move";
},
onResponderTerminationRequest: () => false
}
: {};
if (!isVisible) {
return null;
}
const plotX = model.boxes.plot.x;
const plotY = model.boxes.plot.y;
const plotWidth = model.boxes.plot.width;
const plotHeight = model.boxes.plot.height;
const handleWidth = config.handleWidth;
const handleColor = config.handleColor ?? config.windowStroke ?? model.resolvedTheme.axis;
const renderer = getLineChartRenderer(rendererProp);
const { Group, Path, Rect, Surface } = renderer;
const Layer = renderer.Layer ?? Group;
const handleMinX = model.boxes.plot.x;
const handleMaxX = model.boxes.plot.x + model.boxes.plot.width - handleWidth;
const startHandleX = clamp(windowX - handleWidth / 2, handleMinX, handleMaxX);
const endHandleX = clamp(windowEndX - handleWidth / 2, handleMinX, handleMaxX);
const handleHeight = Math.max(8, config.handleHeight ?? plotHeight - config.handleInset * 2);
const handleY = plotY + (plotHeight - handleHeight) / 2;
const handleRadius = config.handleRadius ?? handleWidth / 2;
const windowProps = {
x: windowX,
y: plotY,
width: windowWidth,
height: plotHeight,
radius: config.windowRadius,
fill: config.windowFill ?? model.resolvedTheme.axis,
opacity: config.windowOpacity,
stroke: config.windowStroke ?? model.resolvedTheme.axis,
strokeOpacity: config.windowStrokeOpacity,
strokeWidth: config.windowStrokeWidth,
theme: model.resolvedTheme
};
const renderWindow = () => config.renderWindow ? (config.renderWindow(windowProps)) : (_jsx(Rect, { x: windowProps.x, y: windowProps.y, width: windowProps.width, height: windowProps.height, rx: windowProps.radius, fill: windowProps.fill, opacity: windowProps.opacity, stroke: windowProps.stroke, strokeOpacity: windowProps.strokeOpacity, strokeWidth: windowProps.strokeWidth }));
const renderHandle = (side, x) => {
const handleProps = {
side,
x,
y: handleY,
width: handleWidth,
height: handleHeight,
radius: handleRadius,
color: handleColor,
opacity: config.handleOpacity,
theme: model.resolvedTheme,
window: windowProps
};
return config.renderHandle ? (config.renderHandle(handleProps)) : (_jsx(Rect, { x: handleProps.x, y: handleProps.y, width: handleProps.width, height: handleProps.height, rx: handleProps.radius, fill: handleProps.color, opacity: handleProps.opacity }));
};
const renderLine = (props) => config.renderLine ? (_jsx(Group, { children: config.renderLine(props) }, `range-line-${props.key}`)) : (_jsx(Path, { d: props.path, fill: "none", stroke: props.color, strokeLinecap: "round", strokeLinejoin: "round", strokeOpacity: props.opacity, strokeWidth: props.strokeWidth, ...(props.strokeDasharray
? { strokeDasharray: props.strokeDasharray }
: {}) }, `range-line-${props.key}`));
return (_jsxs(View, { collapsable: false, pointerEvents: isInteractive ? "auto" : "none", style: {
height: config.height,
marginTop: config.gap,
overflow: "hidden",
position: "relative",
width
}, testID: testID ? `${testID}-range-selector` : undefined, children: [_jsxs(Surface, { width: width, height: config.height, children: [_jsxs(Layer, { name: "background", children: [_jsx(Rect, { x: 0, y: 0, width: width, height: config.height, rx: 8, fill: config.backgroundFill ?? model.resolvedTheme.background }), _jsx(Rect, { x: plotX, y: plotY, width: plotWidth, height: plotHeight, rx: config.plotRadius, fill: config.plotFill ?? model.resolvedTheme.plotBackground })] }), _jsx(Layer, { name: "data", children: model.geometries.map(({ geometry, style }, index) => {
const seriesStyle = config.series?.[geometry.key];
const lineProps = {
color: seriesStyle?.color ?? style.color,
index,
key: geometry.key,
label: geometry.label,
opacity: seriesStyle?.opacity ?? config.lineOpacity,
path: geometry.line.path,
strokeDasharray: seriesStyle?.strokeDasharray,
strokeWidth: seriesStyle?.strokeWidth ??
config.lineStrokeWidth ??
Math.max(config.lineMinStrokeWidth, style.strokeWidth * config.lineStrokeWidthScale),
theme: model.resolvedTheme
};
return renderLine(lineProps);
}) }), _jsxs(Layer, { name: "overlays", children: [config.outsideOpacity > 0 ? (_jsxs(Group, { children: [_jsx(Rect, { x: plotX, y: plotY, width: Math.max(0, windowX - plotX), height: plotHeight, fill: config.outsideFill ?? model.resolvedTheme.background, opacity: config.outsideOpacity }, "range-selector-outside-start"), _jsx(Rect, { x: windowEndX, y: plotY, width: Math.max(0, plotX + plotWidth - windowEndX), height: plotHeight, fill: config.outsideFill ?? model.resolvedTheme.background, opacity: config.outsideOpacity }, "range-selector-outside-end")] }, "range-selector-outside")) : null, renderWindow(), isInteractive ? (_jsxs(Group, { children: [renderHandle("start", startHandleX), renderHandle("end", endHandleX)] }, "range-selector-handles")) : null] })] }), isInteractive ? (_jsx(View, { collapsable: false, pointerEvents: "auto", style: {
bottom: 0,
left: 0,
position: "absolute",
right: 0,
top: 0
}, ...responderProps })) : null] }));
};