← back to Ken
react-dash/src/pages/Weather.jsx
152 lines
import React, { useState, useEffect } from 'react';
import { api, post } from '../api';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from 'recharts';
const CITIES = ['New York', 'Chicago', 'Los Angeles', 'Miami', 'Denver', 'Seattle', 'Boston', 'San Francisco', 'Houston', 'Phoenix'];
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316'];
const ChartTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
<p className="text-gray-400 mb-1">{label}h lead</p>
{payload.map((p, i) => (
<p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {p.value?.toFixed(1)}</p>
))}
</div>
);
};
export default function Weather() {
const [fc, setFc] = useState([]);
const [loading, setL] = useState(true);
const [busy, setB] = useState(false);
const [city, setCity] = useState('New York');
const [chartVar, setChartVar] = useState('temperature');
const load = () => api('/weather').then(d => { setFc(d.forecasts || []); setL(false); });
useEffect(() => { load(); }, []);
async function fetchCity() { setB(true); await post('/weather', { action: 'fetch', city }); load(); setB(false); }
async function fetchAll() { setB(true); await post('/weather', { action: 'fetch_all' }); load(); setB(false); }
if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
const byLoc = {};
fc.forEach(f => { const k = f.location_name || f.geom_hash; if (!byLoc[k]) byLoc[k] = []; byLoc[k].push(f); });
// Build chart data - group by lead_hours for each location
const variables = [...new Set(fc.map(f => f.variable))];
const locations = Object.keys(byLoc);
// Chart data for selected variable across locations
const chartData = {};
Object.entries(byLoc).forEach(([loc, runs]) => {
runs.filter(r => r.variable === chartVar).forEach(r => {
const lead = parseInt(r.lead_hours || 0);
if (!chartData[lead]) chartData[lead] = { lead };
chartData[lead][loc] = parseFloat(r.value_mean || 0);
});
});
const chartArray = Object.values(chartData).sort((a, b) => a.lead - b.lead);
// Per-location temperature range chart (mean/min/max)
const firstLoc = locations[0];
const rangeData = firstLoc ? byLoc[firstLoc]
.filter(r => r.variable === chartVar)
.sort((a, b) => (a.lead_hours || 0) - (b.lead_hours || 0))
.map(r => ({
lead: parseInt(r.lead_hours || 0),
mean: parseFloat(r.value_mean || 0),
min: parseFloat(r.value_min || 0),
max: parseFloat(r.value_max || 0),
})) : [];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div><h1 className="text-2xl font-bold">Weather Forecasts</h1><p className="text-gray-500 text-sm">NWS forecast data + ensemble analysis</p></div>
<div className="flex items-center gap-2">
<select value={city} onChange={e => setCity(e.target.value)} className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm">
{CITIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<button onClick={fetchCity} disabled={busy} className="btn btn-p">{busy ? '...' : 'Fetch City'}</button>
<button onClick={fetchAll} disabled={busy} className="btn btn-s">Fetch All</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Data Points</p><p className="stat-v text-blue-400">{fc.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Locations</p><p className="stat-v text-green-400">{locations.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Latest Run</p><p className="text-sm text-gray-300 mt-1">{fc[0]?.run_ts ? new Date(fc[0].run_ts).toLocaleString() : 'None'}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Variables</p><p className="stat-v text-purple-400">{variables.length}</p></div>
</div>
{/* Variable selector for charts */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400">Chart variable:</span>
{variables.slice(0, 6).map(v => (
<button key={v} onClick={() => setChartVar(v)} className={`px-3 py-1 text-xs rounded-full border transition ${chartVar === v ? 'bg-blue-500/20 border-blue-500/50 text-blue-400' : 'border-gray-700 text-gray-500 hover:text-gray-300'}`}>
{v}
</button>
))}
</div>
{/* Forecast Trends Chart */}
{chartArray.length > 0 && (
<div className="card">
<h2 className="font-semibold mb-3">{chartVar} Forecast by Location (Lead Hours)</h2>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartArray}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="lead" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} label={{ value: 'Lead Hours', position: 'insideBottom', offset: -5, fill: '#6b7280', fontSize: 11 }} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#9ca3af' }} />
{locations.slice(0, 8).map((loc, i) => (
<Line key={loc} type="monotone" dataKey={loc} stroke={COLORS[i % COLORS.length]} strokeWidth={2} dot={false} name={loc} />
))}
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Range Chart for first location */}
{rangeData.length > 0 && (
<div className="card">
<h2 className="font-semibold mb-3">{firstLoc}: {chartVar} Range (Mean / Min / Max)</h2>
<ResponsiveContainer width="100%" height={240}>
<LineChart data={rangeData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="lead" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="max" stroke="#ef4444" strokeWidth={1.5} dot={false} strokeDasharray="4 4" name="Max" />
<Line type="monotone" dataKey="mean" stroke="#10b981" strokeWidth={2} dot={false} name="Mean" />
<Line type="monotone" dataKey="min" stroke="#3b82f6" strokeWidth={1.5} dot={false} strokeDasharray="4 4" name="Min" />
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Location Data Tables */}
{Object.entries(byLoc).slice(0, 8).map(([loc, runs]) => (
<div key={loc} className="card">
<h3 className="font-semibold mb-3">{loc}</h3>
<table><thead><tr><th>Var</th><th>Lead</th><th>Mean</th><th>Min</th><th>Max</th></tr></thead><tbody>
{runs.slice(0, 15).map((r, i) => <tr key={i}>
<td className="font-medium">{r.variable}</td>
<td className="font-mono">{r.lead_hours}h</td>
<td className="font-mono text-green-400">{parseFloat(r.value_mean || 0).toFixed(1)}</td>
<td className="font-mono text-blue-400">{parseFloat(r.value_min || 0).toFixed(1)}</td>
<td className="font-mono text-red-400">{parseFloat(r.value_max || 0).toFixed(1)}</td>
</tr>)}
</tbody></table>
</div>
))}
{fc.length === 0 && <div className="card text-center py-12"><p className="text-gray-500">No forecast data. Click Fetch.</p></div>}
</div>
);
}