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