react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
295 lines (294 loc) • 14.1 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { ScrollView, StyleSheet, View } from "react-native";
import { resolveChartViewport, resolveChartViewportInitialOffset, resolveChartViewportWindow, sliceChartViewportData } from "../../../core/index";
import { useChartKitTheme } from "../../theme";
import { useLineChartAccessibilityLabel } from "./accessibility";
import { LineChartSurface } from "./ChartSurface";
import { StickyYAxis } from "./StickyYAxis";
import { buildLineChartSelectEvent, getLineChartInteractionConfig, getLineChartVisibleInteractionBounds, getNearestLineChartInteractionIndex, isLineChartInteractionEnabled, isLineChartInteractionInBounds, normalizeLineChartSelectedIndex } from "./interaction";
import { getSelectedLineSeries } from "./selection";
import { getLineChartOutsidePressSurfaces } from "./outsidePressSurfaces";
import { getLineChartOverviewProps } from "./overviewProps";
import { useLineChartResponderProps } from "./responders";
import { getStickyYAxisFadeOpacity } from "./stickyYAxisLayout";
import { LineChartRangeSelector } from "./rangeSelector";
import { getRangeSelectorConfig } from "./rangeSelectorConfig";
import { useAnimatedTooltipModel } from "./useAnimatedTooltipModel";
import { useLineChartYAxisLabels } from "./useAnimatedYAxisLabels";
import { useChartModel } from "./useChartModel";
import { useLineChartViewportPan } from "./viewportInteraction";
import { LineChartViewportGesture, useLineChartViewportPinchZoom } from "./viewportPinchZoom";
import { clampLineChartTooltipToViewport } from "./tooltip";
import { useScopedChartSelection } from "../../selection/ChartSelectionProvider";
export { getLineChartAccessibilitySummary, getLineChartDataTable } from "./accessibility";
export const LineChart = (props) => {
const chartId = useId().replace(/:/g, "");
const scopedChartId = props.id ?? chartId;
const chartKitTheme = useChartKitTheme();
const renderer = props.renderer ?? chartKitTheme.renderer;
const dataLength = props.data.length;
const accessibilityLabel = useLineChartAccessibilityLabel(props);
const scrollViewRef = useRef(null);
const interactionConfig = useMemo(() => getLineChartInteractionConfig(props.interaction), [props.interaction]);
const [gestureSelectedIndex, setGestureSelectedIndex] = useState(() => normalizeLineChartSelectedIndex(props.defaultSelectedIndex));
const [gestureSelectionPointer, setGestureSelectionPointer] = useState();
const effectiveSelectedIndex = props.selectedIndex ?? gestureSelectedIndex;
const rangeSelectorConfig = useMemo(() => getRangeSelectorConfig(props.rangeSelector), [props.rangeSelector]);
const viewportWindow = useMemo(() => resolveChartViewportWindow({
itemCount: props.data.length,
startIndex: props.viewport?.startIndex,
endIndex: props.viewport?.endIndex,
visiblePoints: props.viewport?.visiblePoints,
initialIndex: props.viewport?.initialIndex
}), [
props.data.length,
props.viewport?.endIndex,
props.viewport?.initialIndex,
props.viewport?.startIndex,
props.viewport?.visiblePoints
]);
const mainData = useMemo(() => sliceChartViewportData(props.data, viewportWindow), [props.data, viewportWindow]);
const isRangeSelectorVisible = rangeSelectorConfig.visible && props.data.length > 1;
const mainHeight = isRangeSelectorVisible
? Math.max(120, props.height - rangeSelectorConfig.height - rangeSelectorConfig.gap)
: props.height;
const viewport = useMemo(() => resolveChartViewport({
itemCount: mainData.length,
scrollable: props.scrollable,
viewportWidth: props.width,
visiblePoints: props.visiblePoints
}), [mainData.length, props.scrollable, props.visiblePoints, props.width]);
const initialScrollOffset = useMemo(() => resolveChartViewportInitialOffset({
initialIndex: props.initialIndex,
viewport
}), [props.initialIndex, viewport]);
const [scrollOffset, setScrollOffset] = useState(initialScrollOffset);
const chartProps = {
...props,
data: mainData,
height: mainHeight,
width: viewport.contentWidth,
...(effectiveSelectedIndex !== undefined
? { selectedIndex: effectiveSelectedIndex }
: {})
};
const model = useChartModel({
...chartProps,
chartKitTheme,
dataIndexOffset: viewportWindow.startIndex,
selectionPointer: gestureSelectionPointer?.index === effectiveSelectedIndex
? gestureSelectionPointer
: undefined,
stableYAxisData: props.data
});
const { boxes, geometries, interactionPoints, selectionModel, formatYLabel } = model;
const overviewBaseProps = getLineChartOverviewProps(props);
const overviewModel = useChartModel({
...overviewBaseProps,
activeDot: false,
chartKitTheme,
crosshair: false,
height: rangeSelectorConfig.height,
labelStrategy: "hide",
legend: false,
showDots: false,
tooltip: false,
width: props.width
});
const isInteractionEnabled = isLineChartInteractionEnabled(interactionConfig);
const animatedYAxisLabels = useLineChartYAxisLabels(model, props.axisLabelAnimation);
useEffect(() => {
if (!viewport.scrollable) {
return;
}
const frame = requestAnimationFrame(() => {
setScrollOffset(initialScrollOffset);
scrollViewRef.current?.scrollTo({
animated: false,
x: initialScrollOffset
});
});
return () => {
cancelAnimationFrame(frame);
};
}, [initialScrollOffset, viewport.scrollable]);
const handleScroll = useCallback((event) => {
setScrollOffset(event.nativeEvent.contentOffset.x);
}, []);
const preventBrowserSelection = useCallback((event) => {
event.preventDefault();
}, []);
const viewportPan = useLineChartViewportPan({
dataLength,
enabled: !viewport.scrollable,
onViewportChange: props.onViewportChange,
plotBounds: boxes.plot,
preventBrowserSelection,
viewportInteraction: props.viewportInteraction,
viewportWindow
});
const viewportPinchZoom = useLineChartViewportPinchZoom({
dataLength,
enabled: !viewport.scrollable,
onViewportChange: props.onViewportChange,
plotBounds: boxes.plot,
viewportInteraction: props.viewportInteraction,
viewportWindow
});
const isResponderEventInPlot = useCallback((event) => {
const { locationX, locationY } = event.nativeEvent;
return isLineChartInteractionInBounds({
bounds: boxes.plot,
locationX,
locationY
});
}, [boxes.plot]);
const visibleInteractionBounds = useMemo(() => getLineChartVisibleInteractionBounds({
bounds: boxes.plot,
scrollable: viewport.scrollable,
viewportWidth: props.width
}), [boxes.plot, props.width, viewport.scrollable]);
const clearGestureSelection = useCallback((event) => {
setGestureSelectionPointer(undefined);
if (props.selectedIndex === undefined) {
setGestureSelectedIndex(undefined);
}
interactionConfig.onDeselect?.(event);
}, [interactionConfig, props.selectedIndex]);
const clearSelectionFromScope = useCallback((reason) => {
if (reason === "scopeChange") {
if (props.selectedIndex === undefined) {
setGestureSelectedIndex(undefined);
}
return;
}
clearGestureSelection({ reason });
}, [clearGestureSelection, props.selectedIndex]);
const scopedSelection = useScopedChartSelection({
chartId: scopedChartId,
controlled: props.selectedIndex !== undefined,
hasSelection: effectiveSelectedIndex !== undefined,
onClear: clearSelectionFromScope
});
const clearScopedGestureSelection = useCallback((event) => {
clearGestureSelection(event);
if (event.reason === "outsidePress") {
scopedSelection.dismissSelection?.("outsidePress");
}
else if (event.reason === "gestureEnd") {
scopedSelection.dismissSelection?.("programmatic");
}
}, [clearGestureSelection, scopedSelection]);
const handleInteractionEvent = useCallback((event) => {
preventBrowserSelection(event);
const { locationX, locationY } = event.nativeEvent;
const selectedDataIndex = getNearestLineChartInteractionIndex({
locationX,
points: interactionPoints
});
if (selectedDataIndex === undefined) {
return;
}
const selectedSeries = getSelectedLineSeries({
activeDot: props.activeDot,
formatYLabel,
geometries,
selectedDataIndex
});
const selectEvent = buildLineChartSelectEvent({
interactionPoints,
selectedDataIndex,
selectedSeries
});
if (!selectEvent) {
return;
}
setGestureSelectionPointer({
index: selectedDataIndex,
x: locationX,
y: locationY
});
if (props.selectedIndex === undefined &&
interactionConfig.selectionPersistence !== "none") {
setGestureSelectedIndex(selectedDataIndex);
}
scopedSelection.selectChart();
interactionConfig.onSelect?.(selectEvent);
}, [
formatYLabel,
geometries,
interactionConfig,
interactionPoints,
props.activeDot,
props.selectedIndex,
preventBrowserSelection,
scopedSelection
]);
const responderProps = useLineChartResponderProps({
clearGestureSelection: clearScopedGestureSelection,
handleInteractionEvent,
interactionConfig,
isInteractionEnabled,
isResponderEventInPlot,
preventBrowserSelection,
viewportPan
});
const outsidePressSurfaceResponderProps = isInteractionEnabled && interactionConfig.deselectOnOutsidePress
? {
onStartShouldSetResponder: () => true,
onResponderGrant: (event) => {
preventBrowserSelection(event);
clearScopedGestureSelection({ reason: "outsidePress" });
}
}
: {};
const outsidePressSurfaces = getLineChartOutsidePressSurfaces({
enabled: isInteractionEnabled && interactionConfig.deselectOnOutsidePress,
mainHeight,
visibleInteractionBounds,
width: props.width
});
const animatedTooltip = useAnimatedTooltipModel(selectionModel?.tooltip);
const displayTooltip = animatedTooltip && viewport.scrollable
? clampLineChartTooltipToViewport(animatedTooltip, {
leftInset: boxes.plot.x + 4,
scrollOffset,
viewportWidth: props.width
})
: animatedTooltip;
const chartWidth = viewport.contentWidth;
const xAxisLabelFadeY = boxes.plot.y + boxes.plot.height;
const scrollStartFadeId = `${chartId}-scroll-start-fade`;
const mainSurface = (_jsx(LineChartSurface, { animatedTooltip: displayTooltip, chartId: chartId, chartWidth: chartWidth, isScrollable: viewport.scrollable, mainHeight: mainHeight, model: model, props: props, renderer: renderer, responderProps: responderProps, yAxisLabels: animatedYAxisLabels }));
return (_jsxs(View, { accessible: true, accessibilityLabel: accessibilityLabel, accessibilityRole: "image", testID: props.testID, style: [styles.container, { width: props.width, height: props.height }], children: [_jsxs(View, { style: { width: props.width, height: mainHeight }, children: [viewport.scrollable ? (_jsx(ScrollView, { ref: scrollViewRef, horizontal: true, bounces: false, showsHorizontalScrollIndicator: true, style: [
styles.scroller,
{ width: props.width, height: mainHeight }
], contentContainerStyle: [
styles.scrollerContent,
{ width: chartWidth, height: mainHeight }
], onScroll: handleScroll, scrollEventThrottle: 16, children: mainSurface })) : (_jsx(LineChartViewportGesture, { gesture: viewportPinchZoom, children: mainSurface })), viewport.scrollable ? (_jsx(StickyYAxis, { fadeHeight: Math.max(0, mainHeight - xAxisLabelFadeY), fadeOpacity: getStickyYAxisFadeOpacity(scrollOffset), fadeWidth: Math.min(12, Math.max(0, props.width - boxes.plot.x)), fadeY: xAxisLabelFadeY, gradientId: scrollStartFadeId, mainHeight: mainHeight, model: model, renderer: renderer, width: props.width, yAxisLabels: animatedYAxisLabels })) : null, outsidePressSurfaces.map((surface) => (_jsx(View, { style: [
styles.outsidePressSurface,
{
height: surface.height,
left: surface.left,
top: surface.top,
width: surface.width
}
], ...outsidePressSurfaceResponderProps }, `outside-press-${surface.key}`)))] }), _jsx(LineChartRangeSelector, { config: rangeSelectorConfig, dataLength: dataLength, isVisible: isRangeSelectorVisible, model: overviewModel, onViewportChange: props.onViewportChange, preventBrowserSelection: preventBrowserSelection, renderer: renderer, testID: props.testID, viewportWindow: viewportWindow, width: props.width })] }));
};
const styles = StyleSheet.create({
container: {
overflow: "hidden",
userSelect: "none"
},
scroller: {
overflow: "hidden"
},
scrollerContent: {
flexGrow: 0
},
outsidePressSurface: {
position: "absolute"
}
});