Adds a substantial layer of data-backed statistics (all grounded in verified, clean source data — no fabricated metrics). Backend (new routers/statistics.py, prefix /api/stats; +106 pytest still green): - /inpatient-clinical: LOS dist + by-disease quartiles, cost dist + by-disease + cost-vs-LOS, outcome counts, admission-route counts, BMI-by-age, KPIs (5822 admissions, median LOS 4d, mean ¥6294, cure 99.1%, emergency 47%) - /symptoms: 主诉 keyword frequencies (发热/咳嗽/肺炎…) + revisit ratio (36%) - /incidence-rate: per-10k-population standardized rate by district (cases ÷ pop) - /env-correlation: pollutant×cases Pearson + 7×7 pairwise matrix + PM2.5 scatter - /temporal: weekday distribution (+ month/yoy returned but UI omits them — data is December-only, so seasonality/YoY would be misleading) Frontend: - NEW 住院临床分析 page (/analysis/clinical, nav 临床分析): 9 charts + KPI row — LOS histogram + box-by-disease, cost histogram + scatter + by-disease, outcome donut (severity-colored), admission-route donut, age-band BMI box - DiseaseAnalysis: 主诉症状词频 horizontal bar + revisit ratio - DistrictComparison: 标化发病率(每万人)with 病例数↔发病率 toggle (rate is epidemiologically correct; raw counts mislead by population) - EnvironmentalHealth: pollutant-cases correlation bar + 7×7 correlation heatmap + PM2.5×cases scatter with least-squares regression line - TrendAnalysis: 星期就诊分布 + honest "data is December-only" note - statsApi client + types Gates: tsc 0 · build ok · functional e2e 43/43 (incl 2 new clinical) · verified live against real backend data via dev proxy Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
|
||
);
|
||
});
|