Files
CA/frontend/src/components/clinical/CostByDiseaseChart.tsx
Akiba So 4df6c71628 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>
2026-06-21 21:42:52 +08:00

63 lines
1.9 KiB
TypeScript

import { memo } from 'react';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
interface CostByDiseaseChartProps {
data: { diagnosis: string; mean_cost: number; n: number }[];
}
function truncate(s: string, max: number): string {
return s.length > max ? s.slice(0, max) + '…' : s;
}
/** 各病种平均费用横向柱状图。 */
export const CostByDiseaseChart = memo(function CostByDiseaseChart({
data,
}: CostByDiseaseChartProps) {
if (!data || data.length === 0) {
return <div className="text-center py-8 text-text-muted text-sm"></div>;
}
const chartData = [...data]
.sort((a, b) => a.mean_cost - b.mean_cost)
.map((d) => ({ ...d, displayName: truncate(d.diagnosis, 8) }));
return (
<ResponsiveContainer width="100%" height={Math.max(240, chartData.length * 34)}>
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 20, left: 12, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke={CLINICAL_COLORS.grid} horizontal={false} />
<XAxis
type="number"
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
tickFormatter={(v: number) => `¥${(v / 1000).toFixed(0)}k`}
/>
<YAxis
type="category"
dataKey="displayName"
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axisLabel }}
width={72}
axisLine={false}
tickLine={false}
/>
<Tooltip
contentStyle={TOOLTIP_STYLE}
formatter={(v: number) => [`¥${Math.round(v).toLocaleString()}`, '人均费用']}
/>
<Bar dataKey="mean_cost" fill={CLINICAL_COLORS.cost} barSize={16} radius={[0, 3, 3, 0]} />
</BarChart>
</ResponsiveContainer>
);
});