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>
63 lines
1.9 KiB
TypeScript
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>
|
|
);
|
|
});
|