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