UNPKG

react-native-chart-kit

Version:

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

254 lines (253 loc) 12.7 kB
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useId, useMemo, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { useChartKitTheme } from "../../theme"; import { useScopedChartSelection } from "../../selection/ChartSelectionProvider"; import { getLineChartRenderer as getPieChartRenderer } from "../line/renderer"; import { getPieChartAccessibilitySummary } from "./accessibility"; import { resolvePieChartActiveSliceConfig } from "./activeSlice"; import { buildPieChartSelectEvent, getPieChartInteractionConfig, getPieChartSliceAtPoint, isPieChartInteractionEnabled, normalizePieChartSelectedIndex } from "./interaction"; import { buildPieChartModel } from "./model"; import { usePieChartSelectionAnimation } from "./selectionAnimation"; import { PieChartSlices } from "./slices"; export { getPieChartAccessibilitySummary, getPieChartDataTable } from "./accessibility"; const defaultDonutInnerRadiusRatio = 0.58; const defaultPieLegendGap = 8; const RendererLayer = ({ children }) => _jsx(_Fragment, { children: children }); export const PieChart = (props) => { const generatedChartId = useId().replace(/:/g, ""); const scopedChartId = props.id ?? generatedChartId; const chartKitTheme = useChartKitTheme(); const interactionConfig = useMemo(() => getPieChartInteractionConfig(props.interaction), [props.interaction]); const [gestureSelectedIndex, setGestureSelectedIndex] = useState(() => normalizePieChartSelectedIndex(props.defaultSelectedIndex)); const selectedIndex = normalizePieChartSelectedIndex(props.selectedIndex) ?? gestureSelectedIndex; const model = useMemo(() => buildPieChartModel({ chartKitTheme, props, selectedIndex }), [chartKitTheme, props, selectedIndex]); const { arcLabels, arcs, centerX, centerY, chartHeight, legendItems, legendVisible, resolvedTheme, radius, sliceSeparator, total } = model; const legendConfig = typeof props.legend === "object" ? props.legend : {}; const activeSlice = resolvePieChartActiveSliceConfig({ activeSlice: props.activeSlice, backgroundColor: resolvedTheme.background }); const selectionAnimationState = usePieChartSelectionAnimation({ animation: props.selectionAnimation, selectedIndex }); const isInteractionEnabled = isPieChartInteractionEnabled(interactionConfig); const hasSelectedSlice = selectedIndex !== undefined; const clearGestureSelection = useCallback((reason) => { if (props.selectedIndex === undefined) { setGestureSelectedIndex(undefined); } interactionConfig.onDeselect?.({ reason }); }, [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: hasSelectedSlice, onClear: clearSelectionFromScope }); const clearScopedGestureSelection = useCallback((reason) => { clearGestureSelection(reason); if (reason !== "programmatic") { scopedSelection.dismissSelection?.(reason); } }, [clearGestureSelection, scopedSelection]); const handleResponderRelease = useCallback((event) => { event.preventDefault(); const { locationX, locationY } = event.nativeEvent; const tappedSlice = getPieChartSliceAtPoint({ arcs, centerX, centerY, innerRadius: model.innerRadius, locationX, locationY, radius }); if (!tappedSlice) { if (interactionConfig.deselectOnOutsidePress) { clearScopedGestureSelection("outsidePress"); } return; } if (props.selectedIndex === undefined) { setGestureSelectedIndex(tappedSlice.index); } scopedSelection.selectChart(); const selectEvent = buildPieChartSelectEvent(tappedSlice); if (selectEvent) { interactionConfig.onSelect?.(selectEvent); } }, [ arcs, centerX, centerY, clearScopedGestureSelection, interactionConfig, model.innerRadius, props.selectedIndex, radius, scopedSelection ]); const responderProps = isInteractionEnabled ? { onStartShouldSetResponderCapture: () => true, onStartShouldSetResponder: () => true, onResponderGrant: (event) => { event.preventDefault(); }, onResponderRelease: handleResponderRelease, onResponderTerminationRequest: () => true } : {}; const centerLabelProps = { arcs, theme: resolvedTheme, total }; if (selectedIndex !== undefined) { centerLabelProps.selectedIndex = selectedIndex; const selectedArc = arcs[selectedIndex]; if (selectedArc) { centerLabelProps.selectedArc = selectedArc; } } const centerLabel = typeof props.centerLabel === "function" ? props.centerLabel(centerLabelProps) : props.centerLabel; const isTextCenterLabel = typeof centerLabel === "string" && centerLabel.length > 0; const customCenterLabel = centerLabel !== undefined && centerLabel !== null && !isTextCenterLabel ? centerLabel : null; const accessibilityLabel = props.accessibilityLabel ?? getPieChartAccessibilitySummary({ colorKey: props.colorKey, colors: props.colors, data: props.data, formatPercentage: props.formatPercentage, formatValue: props.formatValue, labelKey: props.labelKey, valueKey: props.valueKey }); const renderer = getPieChartRenderer(props.renderer ?? chartKitTheme.renderer); const Layer = renderer.Layer ?? RendererLayer; const Line = renderer.Line; const Surface = renderer.Surface; const SvgText = renderer.Text; const canRenderText = renderer.capabilities?.text !== false; return (_jsxs(View, { accessible: true, accessibilityLabel: accessibilityLabel, accessibilityRole: "image", collapsable: false, style: [ styles.container, { backgroundColor: resolvedTheme.background, height: props.height, position: "relative", width: props.width } ], testID: props.testID, children: [_jsxs(View, { style: [ styles.plotArea, { height: chartHeight, width: props.width } ], children: [_jsxs(Surface, { width: props.width, height: chartHeight, children: [_jsx(Layer, { name: "background", children: _jsx(PieChartSlices, { activeSlice: activeSlice, arcs: arcs, centerX: centerX, centerY: centerY, innerRadius: model.innerRadius, radius: radius, renderer: renderer, resolvedTheme: resolvedTheme, selectedIndex: selectedIndex, selectionAnimationState: selectionAnimationState, sliceSeparator: sliceSeparator, testID: props.testID }) }), isTextCenterLabel && canRenderText ? (_jsx(Layer, { name: "overlays", children: _jsx(SvgText, { fill: resolvedTheme.text, fontSize: 16, fontWeight: "800", text: centerLabel, textAnchor: "middle", x: centerX, y: centerY + 5, ...(resolvedTheme.typography.fontFamily ? { fontFamily: resolvedTheme.typography.fontFamily } : {}), children: centerLabel }) })) : null, arcLabels.length > 0 ? (_jsxs(Layer, { name: "interaction", children: [arcLabels.map((label) => label.connectorVisible ? (_jsx(Line, { x1: label.connectorStartX, x2: label.connectorBendX, y1: label.connectorStartY, y2: label.connectorBendY, stroke: label.connectorColor, strokeOpacity: label.connectorOpacity, strokeWidth: label.connectorWidth }, `${label.key}-connector-a`)) : null), arcLabels.map((label) => label.connectorVisible ? (_jsx(Line, { x1: label.connectorBendX, x2: label.connectorEndX, y1: label.connectorBendY, y2: label.connectorEndY, stroke: label.connectorColor, strokeOpacity: label.connectorOpacity, strokeWidth: label.connectorWidth }, `${label.key}-connector-b`)) : null), canRenderText ? arcLabels.map((label) => (_jsx(SvgText, { fill: resolvedTheme.text, fontSize: label.fontSize, fontWeight: "800", text: label.text, textAnchor: label.textAnchor, x: label.x, y: label.y + label.fontSize / 3, ...(resolvedTheme.typography.fontFamily ? { fontFamily: resolvedTheme.typography.fontFamily } : {}), children: label.text }, label.key))) : null] })) : null] }), customCenterLabel ? (_jsx(View, { pointerEvents: "none", style: [ styles.centerLabelOverlay, { height: Math.max(44, Math.min(76, radius * 0.82)), top: centerY - Math.max(22, Math.min(38, radius * 0.41)), width: props.width } ], children: customCenterLabel })) : null, isInteractionEnabled ? (_jsx(View, { accessible: false, collapsable: false, importantForAccessibility: "no", pointerEvents: "auto", style: styles.interactionOverlay, ...responderProps })) : null] }), legendVisible && legendItems.length > 0 ? (_jsx(View, { style: [ styles.legend, { gap: legendConfig.itemGap ?? defaultPieLegendGap } ], children: legendItems.map((item, index) => { const selected = selectedIndex === item.index; const renderedItem = legendConfig.renderItem?.({ index, item, selected, theme: resolvedTheme }); return (_jsx(View, { style: [ styles.legendItem, { maxWidth: legendConfig.maxItemWidth ?? "48%" } ], children: renderedItem ?? (_jsxs(_Fragment, { children: [_jsx(View, { style: [ styles.legendSwatch, { backgroundColor: item.color } ] }), _jsx(Text, { numberOfLines: 1, style: [styles.legendText, { color: resolvedTheme.text }], children: item.label }), _jsx(Text, { numberOfLines: 1, style: [ styles.legendValue, { color: resolvedTheme.mutedText } ], children: item.percentageLabel })] })) }, item.key)); }) })) : null] })); }; export const DonutChart = (props) => (_jsx(PieChart, { ...props, innerRadiusRatio: props.innerRadiusRatio ?? defaultDonutInnerRadiusRatio })); const styles = StyleSheet.create({ container: { borderRadius: 8, overflow: "hidden" }, centerLabelOverlay: { alignItems: "center", justifyContent: "center", left: 0, position: "absolute" }, plotArea: { position: "relative" }, legend: { alignContent: "center", flexDirection: "row", flexWrap: "wrap", gap: defaultPieLegendGap, justifyContent: "center", paddingHorizontal: 8, paddingTop: 4 }, legendItem: { alignItems: "center", flexDirection: "row", gap: 5, maxWidth: "48%", minHeight: 18 }, legendSwatch: { borderRadius: 4, height: 8, width: 8 }, legendText: { flexShrink: 1, fontSize: 11, fontWeight: "700" }, legendValue: { fontSize: 10, fontWeight: "700" }, interactionOverlay: { bottom: 0, left: 0, position: "absolute", right: 0, top: 0 } });