Spaces:
Sleeping
Sleeping
File size: 5,324 Bytes
f381be8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | import { useEffect, useState } from "react";
import {
LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer,
BarChart, Bar, CartesianGrid,
} from "recharts";
import { fetchDashboard, DashboardData } from "../api";
export default function Dashboard() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchDashboard()
.then(setData)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, []);
if (loading)
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-400" />
</div>
);
if (error)
return (
<div className="bg-red-900/30 border border-red-500 rounded-lg p-4 text-red-300">
Error loading dashboard: {error}
</div>
);
if (!data) return null;
// Prepare capacity fade chart data (first 6 batteries)
const fadeEntries = Object.entries(data.capacity_fade).slice(0, 6);
const maxLen = Math.max(...fadeEntries.map(([, v]) => v.length));
const fadeData = Array.from({ length: maxLen }, (_, i) => {
const row: Record<string, number> = { cycle: i + 1 };
fadeEntries.forEach(([bid, caps]) => {
if (i < caps.length) row[bid] = +(caps[i] / 2 * 100).toFixed(1);
});
return row;
});
// Model metrics
const metricsList = Object.entries(data.model_metrics)
.map(([name, m]) => ({ name, r2: m.R2 ?? m.r2 ?? 0, mae: m.MAE ?? m.mae ?? 0 }))
.sort((a, b) => b.r2 - a.r2)
.slice(0, 10);
const COLORS = ["#22c55e", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4"];
return (
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard label="Batteries" value={data.batteries.length} />
<StatCard label="Models Trained" value={Object.keys(data.model_metrics).length} />
<StatCard label="Best Model" value={data.best_model} />
<StatCard
label="Best R²"
value={
data.model_metrics[data.best_model]
? (data.model_metrics[data.best_model].R2 ?? data.model_metrics[data.best_model].r2 ?? 0).toFixed(4)
: "—"
}
/>
</div>
{/* Battery Grid */}
<section>
<h2 className="text-lg font-semibold mb-3">Battery Fleet Overview</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
{data.batteries.map((b) => (
<div
key={b.battery_id}
className="rounded-lg p-3 border border-gray-800 bg-gray-900 text-center"
style={{ borderLeftColor: b.color_hex, borderLeftWidth: "4px" }}
>
<div className="text-xs text-gray-400">{b.battery_id}</div>
<div className="text-xl font-bold" style={{ color: b.color_hex }}>
{b.soh_pct}%
</div>
<div className="text-xs text-gray-500">{b.degradation_state}</div>
</div>
))}
</div>
</section>
{/* Capacity Fade Chart */}
<section className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold mb-4">SOH Capacity Fade</h2>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={fadeData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="cycle" stroke="#9ca3af" />
<YAxis domain={[50, 100]} stroke="#9ca3af" />
<Tooltip contentStyle={{ backgroundColor: "#1f2937", border: "1px solid #374151" }} />
<Legend />
{fadeEntries.map(([bid], i) => (
<Line
key={bid}
type="monotone"
dataKey={bid}
stroke={COLORS[i % COLORS.length]}
dot={false}
strokeWidth={2}
/>
))}
</LineChart>
</ResponsiveContainer>
</section>
{/* Model Comparison */}
<section className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold mb-4">Model R² Comparison</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={metricsList} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis type="number" domain={[0, 1]} stroke="#9ca3af" />
<YAxis dataKey="name" type="category" width={150} stroke="#9ca3af" tick={{ fontSize: 12 }} />
<Tooltip contentStyle={{ backgroundColor: "#1f2937", border: "1px solid #374151" }} />
<Bar dataKey="r2" fill="#22c55e" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</section>
</div>
);
}
function StatCard({ label, value }: { label: string; value: string | number }) {
return (
<div className="bg-gray-900 rounded-xl p-4 border border-gray-800">
<div className="text-sm text-gray-400">{label}</div>
<div className="text-2xl font-bold text-green-400 mt-1 truncate">{value}</div>
</div>
);
}
|