57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
|
|
import { memo } from 'react';
|
|||
|
|
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|||
|
|
import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
|
|||
|
|
|
|||
|
|
export interface DonutSlice {
|
|||
|
|
name: string;
|
|||
|
|
value: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface DonutChartProps {
|
|||
|
|
data: DonutSlice[];
|
|||
|
|
/** name -> color。未命中时按 palette 顺序回退。 */
|
|||
|
|
colorMap?: Record<string, string>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 通用环形图。复用于「出院结局构成」与「入院途径构成」。 */
|
|||
|
|
export const DonutChart = memo(function DonutChart({ data, colorMap }: DonutChartProps) {
|
|||
|
|
if (!data || data.length === 0) {
|
|||
|
|
return <div className="text-center py-8 text-text-muted text-sm">暂无数据</div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const total = data.reduce((s, d) => s + d.value, 0);
|
|||
|
|
const colorFor = (name: string, idx: number) =>
|
|||
|
|
colorMap?.[name] ??
|
|||
|
|
CLINICAL_COLORS.routePalette[idx % CLINICAL_COLORS.routePalette.length] ??
|
|||
|
|
CLINICAL_COLORS.outcomeFallback;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<ResponsiveContainer width="100%" height={280}>
|
|||
|
|
<PieChart>
|
|||
|
|
<Pie
|
|||
|
|
data={data}
|
|||
|
|
dataKey="value"
|
|||
|
|
nameKey="name"
|
|||
|
|
cx="50%"
|
|||
|
|
cy="50%"
|
|||
|
|
innerRadius={56}
|
|||
|
|
outerRadius={88}
|
|||
|
|
paddingAngle={2}
|
|||
|
|
>
|
|||
|
|
{data.map((d, idx) => (
|
|||
|
|
<Cell key={d.name} fill={colorFor(d.name, idx)} />
|
|||
|
|
))}
|
|||
|
|
</Pie>
|
|||
|
|
<Tooltip
|
|||
|
|
contentStyle={TOOLTIP_STYLE}
|
|||
|
|
formatter={(v: number, name: string) => [
|
|||
|
|
`${v.toLocaleString()}(${total > 0 ? ((v / total) * 100).toFixed(1) : '0'}%)`,
|
|||
|
|
name,
|
|||
|
|
]}
|
|||
|
|
/>
|
|||
|
|
<Legend wrapperStyle={{ fontSize: '11px' }} />
|
|||
|
|
</PieChart>
|
|||
|
|
</ResponsiveContainer>
|
|||
|
|
);
|
|||
|
|
});
|