import { useCallback, useMemo } from 'react';
import {
PieChart,
Pie,
Cell,
Curve,
Tooltip,
Sector,
PieProps,
PieLabelRenderProps,
ResponsiveContainer,
} from 'recharts';
import type CSS from 'csstype';
import {
TOOLTIP_STYLE,
TOOLTIP_OTHER_PROPS,
LABEL_STYLE,
COUNT_STYLE,
CHART_MISSING_FILL,
RADIAN,
LABEL_THRESHOLD,
COUNT_TEXT_STYLE,
TEXT_STYLE,
OTHER_KEY,
} from '../../constants/chartConstants';
import type { PieChartProps, TooltipPayload } from '../../types/chartTypes';
import {
useChartTheme,
useChartTranslation,
useChartThreshold,
useChartMaxLabelChars,
} from '../../ChartConfigProvider';
import { polarToCartesian, useTransformedChartData } from '../../util/chartUtils';
import NoData from '../NoData';
import ChartWrapper from './ChartWrapper';
const labelShortName = (name: string, maxChars: number) => {
if (name.length <= maxChars) {
return name;
}
// removing 3 character cause ... s add three characters
return `${name.substring(0, maxChars - 3)}\u2026`;
};
const _entryFill = (entry: { name: string }, index: number, theme: string[]) =>
entry.name.toLowerCase() === 'missing' ? CHART_MISSING_FILL : theme[index % theme.length];
// Prevents the last segment from having the same fill as the first segment (unless "missing") to ensure visual distinction.
const getPieSegmentFill = (entry: { name: string }, index: number, data: Array<{ name: string }>, theme: string[]) => {
let fill = _entryFill(entry, index, theme);
if (index === data.length - 1 && entry.name.toLowerCase() !== 'missing') {
const firstEntry = data[0];
const firstFill = _entryFill(firstEntry, 0, theme);
if (fill === firstFill) {
fill = theme[(index + 1) % theme.length];
}
}
return fill;
};
const BentoPie = ({
height,
width,
onClick,
sort = true,
colorTheme = 'default',
chartThreshold,
maxLabelChars,
...params
}: PieChartProps) => {
const t = useChartTranslation();
const { fill: theme } = useChartTheme().pie[colorTheme];
const defaultChartThreshold = useChartThreshold();
const defaultMaxLabelChars = useChartMaxLabelChars();
const resolvedChartThreshold = chartThreshold ?? defaultChartThreshold;
const resolvedMaxLabelChars = maxLabelChars ?? defaultMaxLabelChars;
// ##################### Data processing #####################
const transformedData = useTransformedChartData(params, true, sort);
const { data, sum } = useMemo(() => {
let data = [...transformedData];
// combining sections with less than chartThreshold
const sum = data.reduce((acc, e) => acc + e.y, 0);
const length = data.length;
const threshold = resolvedChartThreshold * sum;
const dataAboveThreshold = data.filter((e) => e.y > threshold);
// length - 1 intentional: if there is just one category below threshold, the "Other" category is not necessary.
data = dataAboveThreshold.length === length - 1 ? data : dataAboveThreshold;
if (data.length !== length) {
data.push({
x: t[OTHER_KEY],
y: sum - data.reduce((acc, e) => acc + e.y, 0),
id: OTHER_KEY,
});
}
return {
data: data.map((e) => ({ name: e.x, value: e.y, ...e })),
sum,
};
}, [t, transformedData, resolvedChartThreshold]);
// ##################### Rendering #####################
const onHover: PieProps['onMouseOver'] = useCallback(
(data, _index, e) => {
const { target } = e;
if (onClick && target && data.name !== t[OTHER_KEY]) (target as SVGElement).style.cursor = 'pointer';
},
[t, onClick]
);
if (data.length === 0) {
return
{name}
{' '} {value} ({percentage} %)