Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 97 additions & 34 deletions frontend/src/components/HospitalAnalyticsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Expand All @@ -16,6 +17,23 @@ import { getHospitalAnalytics, type HospitalAnalytics } from "../api/analytics";

const CHART_COLORS = ["#0284c7", "#16a34a", "#d97706", "#dc2626", "#7c3aed"];

type OutcomeChartItem = {
name: string;
value: number;
};

type ProcedureChartItem = {
procedure: string;
totalCost: number;
avgCost: number;
count: number;
};

function truncateLabel(label: string, maxLength = 22) {
if (label.length <= maxLength) return label;
return `${label.slice(0, maxLength)}...`;
}

export default function HospitalAnalyticsPanel() {
const [analytics, setAnalytics] = useState<HospitalAnalytics | null>(null);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -48,32 +66,42 @@ export default function HospitalAnalyticsPanel() {
};
}, []);

const outcomeData = useMemo(() => {
const outcomeData = useMemo<OutcomeChartItem[]>(() => {
const distribution = analytics?.outcome_distribution || {};
return Object.entries(distribution).map(([name, value]) => ({ name, value }));
return Object.entries(distribution).map(([name, value]) => ({
name,
value: Number(value),
}));
}, [analytics]);

const procedureData = useMemo(() => {
return (analytics?.procedure_cost_analysis || []).slice(0, 6).map((item) => ({
procedure: item.procedure,
avgCost: item.average_cost,
totalCost: item.total_cost,
count: item.count,
}));
const procedureData = useMemo<ProcedureChartItem[]>(() => {
return [...(analytics?.procedure_cost_analysis || [])]
.sort((a, b) => b.total_cost - a.total_cost)
.slice(0, 5)
.map((item) => ({
procedure: truncateLabel(item.procedure),
totalCost: item.total_cost,
avgCost: item.average_cost,
count: item.count,
}));
}, [analytics]);

const totalOutcomes = useMemo(() => {
return outcomeData.reduce((sum, item) => sum + item.value, 0);
}, [outcomeData]);

if (loading) {
return (
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-sky-900/40">
<p className="text-slate-700 dark:text-slate-200">Loading hospital analytics...</p>
Loading hospital analytics...
</div>
);
}

if (error) {
return (
<div className="rounded-2xl border border-red-300 bg-red-50 p-6 shadow-sm dark:border-red-700 dark:bg-red-950/50">
<p className="text-red-800 dark:text-red-200">{error}</p>
{error}
</div>
);
}
Expand All @@ -82,9 +110,14 @@ export default function HospitalAnalyticsPanel() {

return (
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-sky-900/40">
<div className="mb-4 flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold">Hospital Analytics</h2>
<span className="text-xs text-slate-500 dark:text-slate-400">Source: imported hospital CSV</span>
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Hospital Analytics</h2>
<p className="text-sm text-slate-500">
Key outcome, quality, and procedure cost metrics from imported hospital data.
</p>
</div>
<span className="text-xs text-slate-500">Source: imported hospital CSV</span>
</div>

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
Expand All @@ -95,36 +128,66 @@ export default function HospitalAnalyticsPanel() {
</div>

<div className="mt-6 grid gap-6 lg:grid-cols-2">
<div className="h-72 rounded-xl border border-slate-200 p-4 dark:border-slate-700">
<p className="mb-3 text-sm font-medium text-slate-600 dark:text-slate-300">Outcome Distribution</p>
{/* PIE CHART */}
<div className="h-80 rounded-xl border border-slate-200 p-4 dark:border-slate-700">
<p className="mb-1 text-sm font-medium">Outcome Distribution</p>
<p className="mb-3 text-xs text-slate-500">
Breakdown of patient outcome categories with counts and percentages.
</p>

<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={outcomeData} dataKey="value" nameKey="name" outerRadius={90} label>
<Pie
data={outcomeData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={85}
label={({ name, value }) => {
const percent = totalOutcomes > 0 ? (value / totalOutcomes) * 100 : 0;
return `${name}: ${percent.toFixed(1)}%`;
}}
>
{outcomeData.map((entry, idx) => (
<Cell key={entry.name} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
))}
</Pie>

<Tooltip />
<Legend verticalAlign="bottom" height={60} />
</PieChart>
</ResponsiveContainer>
</div>

<div className="h-72 rounded-xl border border-slate-200 p-4 dark:border-slate-700">
<p className="mb-3 text-sm font-medium text-slate-600 dark:text-slate-300">Procedure Cost Analysis</p>
{/* BAR CHART */}
<div className="h-80 rounded-xl border border-slate-200 p-4 dark:border-slate-700">
<p className="mb-1 text-sm font-medium">Top Procedures by Total Cost</p>
<p className="mb-3 text-xs text-slate-500">
Highest-cost procedures ranked by total spend.
</p>

<ResponsiveContainer width="100%" height="100%">
<BarChart data={procedureData} margin={{ top: 10, right: 20, left: 0, bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="procedure" angle={-45} textAnchor="end" interval={0} height={100} tick={{ fontSize: 11 }} />
<YAxis />
<Tooltip
formatter={(value: number, name: string) => {
if (name === "avgCost") return [`$${value.toLocaleString()}`, "Average Cost"];
if (name === "totalCost") return [`$${value.toLocaleString()}`, "Total Cost"];
return [value, name];
}}
/>
<Bar dataKey="avgCost" fill="#0284c7" name="avgCost" />
</BarChart>
{procedureData.length <= 1 ? (
<div className="flex h-full items-center justify-center text-sm text-slate-400">
Not enough procedure data to display comparison.
</div>
) : (
<BarChart data={procedureData} margin={{ top: 10, right: 20, left: 10, bottom: 85 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="procedure"
angle={-35}
textAnchor="end"
interval={0}
height={90}
tick={{ fontSize: 11 }}
/>
<YAxis tickFormatter={(value: number) => `$${(value / 1000).toFixed(0)}k`} />
<Tooltip formatter={(value: number) => [`$${value.toLocaleString()}`, "Total Cost"]} />
<Bar dataKey="totalCost" fill="#0284c7" radius={[6, 6, 0, 0]} />
</BarChart>
)}
</ResponsiveContainer>
</div>
</div>
Expand All @@ -135,8 +198,8 @@ export default function HospitalAnalyticsPanel() {
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-xl border border-slate-200 px-4 py-3 dark:border-slate-700">
<p className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">{label}</p>
<p className="text-xs uppercase tracking-wide text-slate-500">{label}</p>
<p className="mt-1 text-2xl font-semibold">{value}</p>
</div>
);
}
}
Loading