feat: deep statistical analytics — clinical, symptoms, incidence, env correlation, weekday
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>
This commit is contained in:
56
frontend/src/components/clinical/DonutChart.tsx
Normal file
56
frontend/src/components/clinical/DonutChart.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user