busi488energy/src/app/_sections/demand-summary.tsx
Joey Eamigh ad1a6792f5
phase 8: UI/UX overhaul — layout, charts, map, data freshness
- Fix ticker tape CLS with skeleton loader and fixed height
- Add Inter font, max-width container, responsive dvh units
- Hero metrics: trend deltas, per-metric sparkline colors, 3+2 grid
- GPU calculator: step=100 slider + text input, PUE factor, region comparison bars
- Grid stress: replace misleading arc gauges with demand status bars
- Demand summary: expand to 4-metric highlights grid
- Charts: responsive heights, ISO/non-ISO toggle, correlation R² + trend line
- Map: US-wide default view, marker clustering, enriched region panels, zoom controls
- Fix NYISO polygon (NYC), MISO polygon (Michigan), MISO south (MS/LA)
- Add automated ingestion via instrumentation.ts
- Add data freshness indicator in footer
- Fix backfill start date to 2019-01-01 (EIA RTO data availability)
2026-02-11 19:59:01 -05:00

120 lines
3.7 KiB
TypeScript

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
import { Activity } from 'lucide-react';
import { fetchRegionDemandSummary } from '@/actions/demand.js';
import { deserialize } from '@/lib/superjson.js';
interface RegionSummary {
regionCode: string;
latestDemandMw: number;
peakDemandMw: number;
avgDemandMw: number;
}
function formatGw(mw: number): string {
if (mw >= 1000) return `${(mw / 1000).toFixed(1)}`;
return `${Math.round(mw)}`;
}
function formatGwUnit(mw: number): string {
return mw >= 1000 ? 'GW' : 'MW';
}
export async function DemandSummary() {
const demandResult = await fetchRegionDemandSummary();
const demandRows = demandResult.ok
? deserialize<
Array<{
avg_demand: number | null;
peak_demand: number | null;
region_code: string;
region_name: string;
day: Date;
}>
>(demandResult.data)
: [];
// Aggregate per region: latest demand, peak demand, avg demand
const regionMap = new Map<string, RegionSummary>();
for (const row of demandRows) {
const demand = row.avg_demand ?? 0;
const peak = row.peak_demand ?? 0;
const existing = regionMap.get(row.region_code);
if (!existing) {
regionMap.set(row.region_code, {
regionCode: row.region_code,
latestDemandMw: demand,
peakDemandMw: peak,
avgDemandMw: demand,
});
} else {
// Query is ordered by day ASC, so later rows are more recent
existing.latestDemandMw = demand;
if (peak > existing.peakDemandMw) existing.peakDemandMw = peak;
// Running average approximation
existing.avgDemandMw = (existing.avgDemandMw + demand) / 2;
}
}
const regions = [...regionMap.values()].filter(r => r.peakDemandMw > 0);
if (regions.length === 0) return null;
const totalDemandMw = regions.reduce((sum, r) => sum + r.latestDemandMw, 0);
const peakRegion = regions.reduce((max, r) => (r.latestDemandMw > max.latestDemandMw ? r : max), regions[0]!);
const regionCount = regions.length;
const totalAvg = regions.reduce((sum, r) => sum + r.avgDemandMw, 0);
const totalPeak = regions.reduce((sum, r) => sum + r.peakDemandMw, 0);
const loadFactor = totalPeak > 0 ? (totalAvg / totalPeak) * 100 : 0;
const highlights = [
{
label: 'Total US Demand',
value: formatGw(totalDemandMw),
unit: formatGwUnit(totalDemandMw),
},
{
label: 'Peak Region',
value: peakRegion.regionCode,
unit: `${formatGw(peakRegion.latestDemandMw)} ${formatGwUnit(peakRegion.latestDemandMw)}`,
},
{
label: 'Regions Tracked',
value: regionCount.toString(),
unit: 'ISOs',
},
{
label: 'Avg Load Factor',
value: `${loadFactor.toFixed(0)}`,
unit: '%',
},
];
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-chart-3" />
Demand Highlights
</CardTitle>
<p className="text-xs text-muted-foreground">7-day summary across all grid regions</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
{highlights.map(h => (
<div key={h.label} className="flex flex-col gap-0.5">
<span className="text-[11px] font-medium tracking-wide text-muted-foreground">{h.label}</span>
<div className="flex items-baseline gap-1">
<span className="font-mono text-lg font-bold tabular-nums">{h.value}</span>
<span className="text-xs text-muted-foreground">{h.unit}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}