130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
|
import type { IChartData } from '@/trpc/client';
|
|
import { cn } from '@/utils/cn';
|
|
import { round } from '@/utils/math';
|
|
import { getChartColor } from '@/utils/theme';
|
|
import { truncate } from '@/utils/truncate';
|
|
import { Cell, Pie, PieChart, Tooltip } from 'recharts';
|
|
|
|
import { useChartContext } from './ChartProvider';
|
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
|
import { ReportTable } from './ReportTable';
|
|
|
|
interface ReportPieChartProps {
|
|
data: IChartData;
|
|
}
|
|
|
|
export function ReportPieChart({ data }: ReportPieChartProps) {
|
|
const { editMode } = useChartContext();
|
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
|
|
|
const sum = series.reduce((acc, serie) => acc + serie.metrics.sum, 0);
|
|
const pieData = series.map((serie) => ({
|
|
id: serie.id,
|
|
color: getChartColor(serie.index),
|
|
index: serie.index,
|
|
name: serie.name,
|
|
count: serie.metrics.sum,
|
|
percent: serie.metrics.sum / sum,
|
|
}));
|
|
|
|
return (
|
|
<>
|
|
<div className={cn('max-sm:-mx-3', editMode && 'card p-4')}>
|
|
<AutoSizer disableHeight>
|
|
{({ width }) => {
|
|
const height = Math.min(Math.max(width * 0.5625, 250), 400);
|
|
return (
|
|
<PieChart width={width} height={height}>
|
|
<Tooltip content={<ReportChartTooltip />} />
|
|
<Pie
|
|
dataKey={'count'}
|
|
data={pieData}
|
|
innerRadius={height / 4}
|
|
outerRadius={height / 2.5}
|
|
isAnimationActive={false}
|
|
label={renderLabel}
|
|
>
|
|
{pieData.map((item) => {
|
|
return (
|
|
<Cell
|
|
key={item.id}
|
|
strokeWidth={2}
|
|
stroke={item.color}
|
|
fill={item.color}
|
|
/>
|
|
);
|
|
})}
|
|
</Pie>
|
|
</PieChart>
|
|
);
|
|
}}
|
|
</AutoSizer>
|
|
</div>
|
|
{editMode && (
|
|
<ReportTable
|
|
data={data}
|
|
visibleSeries={series}
|
|
setVisibleSeries={setVisibleSeries}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const renderLabel = ({
|
|
cx,
|
|
cy,
|
|
midAngle,
|
|
innerRadius,
|
|
outerRadius,
|
|
fill,
|
|
payload,
|
|
}: {
|
|
cx: number;
|
|
cy: number;
|
|
midAngle: number;
|
|
innerRadius: number;
|
|
outerRadius: number;
|
|
fill: string;
|
|
payload: { name: string; percent: number };
|
|
}) => {
|
|
const RADIAN = Math.PI / 180;
|
|
const radius = 25 + innerRadius + (outerRadius - innerRadius);
|
|
const radiusProcent = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
const xProcent = cx + radiusProcent * Math.cos(-midAngle * RADIAN);
|
|
const yProcent = cy + radiusProcent * Math.sin(-midAngle * RADIAN);
|
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
|
const name = payload.name;
|
|
const percent = round(payload.percent * 100, 1);
|
|
|
|
return (
|
|
<>
|
|
<text
|
|
x={xProcent}
|
|
y={yProcent}
|
|
fill="white"
|
|
textAnchor="middle"
|
|
dominantBaseline="central"
|
|
fontSize={12}
|
|
fontWeight={700}
|
|
pointerEvents={'none'}
|
|
>
|
|
{percent}%
|
|
</text>
|
|
<text
|
|
x={x}
|
|
y={y}
|
|
fill={fill}
|
|
textAnchor={x > cx ? 'start' : 'end'}
|
|
dominantBaseline="central"
|
|
fontSize={12}
|
|
>
|
|
{truncate(name, 20)}
|
|
</text>
|
|
</>
|
|
);
|
|
};
|