UNPKG

react-native-chart-kit

Version:

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

200 lines (199 loc) 9.69 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import { useChartKitTheme } from "../../theme"; import { useScopedChartSelection } from "../../selection/ChartSelectionProvider"; import { getBarChartAccessibilitySummary } from "./accessibility"; import { BarChartSurface, BarChartTooltipOverlay, StickyBarChartYAxis } from "./BarChartSurface"; import { buildBarChartSelectEvent, getBarChartBarAtPoint, getBarChartBarKey, getBarChartInteractionConfig, isBarChartInteractionEnabled } from "./interaction"; import { buildBarChartModel } from "./model"; import { getBarChartTooltipConfig } from "./options"; import { getSafeBarChartContentWidth, getSafeBarChartRenderer } from "./rendererSafety"; import { getBarChartTooltipModel } from "./tooltipModel"; import { resolveBarChartViewport, resolveBarChartViewportInitialOffset } from "./viewport"; export { getBarChartAccessibilitySummary, getBarChartDataTable } from "./accessibility"; export const BarChart = (props) => { const generatedChartId = useId().replace(/:/g, ""); const scopedChartId = props.id ?? generatedChartId; const chartKitTheme = useChartKitTheme(); const renderer = props.renderer ?? chartKitTheme.renderer; const scrollViewRef = useRef(null); const interactionConfig = useMemo(() => getBarChartInteractionConfig(props.interaction), [props.interaction]); const [gestureSelectedBarKey, setGestureSelectedBarKey] = useState(() => getBarChartBarKey(props.defaultSelectedBar)); const [gestureSelectionPointer, setGestureSelectionPointer] = useState(); const [scrollOffsetX, setScrollOffsetX] = useState(0); const viewport = useMemo(() => resolveBarChartViewport({ itemCount: props.data.length, scrollable: props.scrollable, viewportWidth: props.width, visiblePoints: props.visiblePoints }), [props.data.length, props.scrollable, props.visiblePoints, props.width]); const safeContentWidth = getSafeBarChartContentWidth({ contentWidth: viewport.contentWidth, renderer, scrollable: viewport.scrollable }); const safeViewport = useMemo(() => ({ ...viewport, contentWidth: safeContentWidth, maxOffset: Math.max(0, safeContentWidth - viewport.viewportWidth), scrollable: viewport.scrollable && safeContentWidth > viewport.viewportWidth }), [safeContentWidth, viewport]); const initialScrollOffset = useMemo(() => resolveBarChartViewportInitialOffset({ initialIndex: props.initialIndex, viewport: safeViewport }), [props.initialIndex, safeViewport]); const model = useMemo(() => buildBarChartModel({ ...props, chartKitTheme, width: safeViewport.contentWidth }), [chartKitTheme, props, safeViewport.contentWidth]); const { bars, boxes, resolvedTheme } = model; const safeRenderer = getSafeBarChartRenderer({ contentWidth: viewport.contentWidth, renderer, scrollable: viewport.scrollable }); const scrollInitialOffset = safeViewport.scrollable && props.initialIndex === "end" ? Math.max(0, initialScrollOffset - boxes.plot.x * 0.66) : initialScrollOffset; const barRadius = Math.max(0, props.barRadius ?? 5); const controlledSelectedBarKey = getBarChartBarKey(props.selectedBar); const selectedBarKey = controlledSelectedBarKey ?? gestureSelectedBarKey; const clearGestureSelection = useCallback((reason) => { setGestureSelectionPointer(undefined); if (props.selectedBar === undefined) { setGestureSelectedBarKey(undefined); } interactionConfig.onDeselect?.({ reason }); }, [interactionConfig, props.selectedBar]); const clearSelectionFromScope = useCallback((reason) => { if (reason === "scopeChange") { if (props.selectedBar === undefined) { setGestureSelectedBarKey(undefined); } return; } clearGestureSelection(reason); }, [clearGestureSelection, props.selectedBar]); const scopedSelection = useScopedChartSelection({ chartId: scopedChartId, controlled: props.selectedBar !== undefined, hasSelection: selectedBarKey !== undefined, onClear: clearSelectionFromScope }); const clearScopedGestureSelection = useCallback((reason) => { clearGestureSelection(reason); if (reason !== "programmatic") { scopedSelection.dismissSelection?.(reason); } }, [clearGestureSelection, scopedSelection]); const selectedBar = bars.find((bar) => bar.key === selectedBarKey); const tooltipConfig = useMemo(() => getBarChartTooltipConfig({ themeTooltip: resolvedTheme.tooltip, tooltip: props.tooltip }), [props.tooltip, resolvedTheme.tooltip]); const tooltipModel = useMemo(() => getBarChartTooltipModel({ bar: selectedBar, boxes, config: tooltipConfig, pointer: gestureSelectionPointer?.key === selectedBarKey ? gestureSelectionPointer : undefined }), [boxes, gestureSelectionPointer, selectedBar, selectedBarKey, tooltipConfig]); const isInteractionEnabled = isBarChartInteractionEnabled(interactionConfig); const handleResponderRelease = useCallback((event) => { event.preventDefault(); const { locationX, locationY } = event.nativeEvent; const tappedBar = getBarChartBarAtPoint({ bars, locationX, locationY }); if (!tappedBar) { if (interactionConfig.deselectOnOutsidePress) { clearScopedGestureSelection("outsidePress"); } return; } if (props.selectedBar === undefined) { setGestureSelectedBarKey(tappedBar.key); } setGestureSelectionPointer({ key: tappedBar.key, x: locationX, y: locationY }); scopedSelection.selectChart(); const selectEvent = buildBarChartSelectEvent(tappedBar); if (selectEvent) { interactionConfig.onSelect?.(selectEvent); } }, [ bars, clearScopedGestureSelection, interactionConfig, props.selectedBar, scopedSelection ]); const responderProps = isInteractionEnabled ? { onStartShouldSetResponderCapture: () => true, onStartShouldSetResponder: () => true, onResponderGrant: (event) => { event.preventDefault(); }, onResponderRelease: handleResponderRelease, onResponderTerminationRequest: () => true } : {}; const accessibilityLabel = props.accessibilityLabel ?? getBarChartAccessibilitySummary({ data: props.data, formatXLabel: props.formatXLabel, formatYLabel: props.formatYLabel, series: props.series, xKey: props.xKey, yKey: props.yKey, yKeys: props.yKeys }); const chartSurface = (_jsx(BarChartSurface, { barRadius: barRadius, height: props.height, model: model, responderProps: responderProps, renderBar: props.renderBar, renderer: safeRenderer, selectedBarKey: selectedBarKey, selectionAnimation: props.selectionAnimation, showYAxis: !safeViewport.scrollable, width: safeViewport.contentWidth })); const tooltipOverlay = (_jsx(BarChartTooltipOverlay, { height: props.height, model: model, tooltipConfig: tooltipConfig, tooltipModel: tooltipModel, viewportOffsetX: safeViewport.scrollable ? scrollOffsetX : 0, width: props.width, renderer: safeRenderer })); useEffect(() => { if (!safeViewport.scrollable) { const frame = requestAnimationFrame(() => { setScrollOffsetX(0); }); return () => { cancelAnimationFrame(frame); }; } const frame = requestAnimationFrame(() => { setScrollOffsetX(scrollInitialOffset); scrollViewRef.current?.scrollTo({ animated: false, x: scrollInitialOffset }); }); return () => { cancelAnimationFrame(frame); }; }, [safeViewport.scrollable, scrollInitialOffset]); return (_jsx(View, { accessible: true, accessibilityLabel: accessibilityLabel, accessibilityRole: "image", style: { width: props.width, height: props.height }, testID: props.testID, children: safeViewport.scrollable ? (_jsxs(_Fragment, { children: [_jsx(ScrollView, { ref: scrollViewRef, horizontal: true, bounces: false, showsHorizontalScrollIndicator: true, style: [ styles.scroller, { width: props.width, height: props.height } ], contentContainerStyle: [ styles.scrollerContent, { width: safeViewport.contentWidth, height: props.height } ], onScroll: (event) => { setScrollOffsetX(event.nativeEvent.contentOffset.x); }, scrollEventThrottle: 16, children: chartSurface }), _jsx(StickyBarChartYAxis, { height: props.height, model: model, renderer: safeRenderer, width: props.width }), tooltipOverlay] })) : (_jsxs(_Fragment, { children: [chartSurface, tooltipOverlay] })) })); }; const styles = StyleSheet.create({ scroller: { overflow: "hidden" }, scrollerContent: { flexGrow: 0 } });