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>
  );
}