busi488energy/src/components/map/datacenter-marker.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

160 lines
4.9 KiB
TypeScript

'use client';
import { AdvancedMarker } from '@vis.gl/react-google-maps';
import { useCallback, useRef, useState } from 'react';
const OPERATOR_COLORS: Record<string, string> = {
AWS: '#FF9900',
Google: '#4285F4',
Microsoft: '#00A4EF',
Meta: '#0668E1',
Oracle: '#F80000',
Equinix: '#ED1C24',
'Digital Realty': '#0072CE',
CoreWeave: '#7B2FBE',
QTS: '#00B388',
CyrusOne: '#003B5C',
NTT: '#E60012',
Vantage: '#0099D8',
};
function getOperatorColor(operator: string): string {
return OPERATOR_COLORS[operator] ?? '#6B7280';
}
function getMarkerSize(capacityMw: number): number {
if (capacityMw >= 500) return 32;
if (capacityMw >= 200) return 26;
if (capacityMw >= 100) return 22;
if (capacityMw >= 50) return 18;
return 14;
}
function getPulseDuration(capacityMw: number): number {
if (capacityMw >= 500) return 1.2;
if (capacityMw >= 200) return 1.8;
if (capacityMw >= 100) return 2.4;
return 3.0;
}
export interface DatacenterMarkerData {
id: string;
name: string;
operator: string;
capacity_mw: number;
status: string;
year_opened: number;
region_id: string;
region_code: string;
region_name: string;
lat: number;
lng: number;
}
interface DatacenterMarkerProps {
datacenter: DatacenterMarkerData;
onClick: (datacenter: DatacenterMarkerData) => void;
isPulsing?: boolean;
isSelected?: boolean;
/** Callback to register/unregister the underlying AdvancedMarkerElement for clustering. */
setMarkerRef?: (marker: google.maps.marker.AdvancedMarkerElement | null, id: string) => void;
}
export function DatacenterMarker({
datacenter,
onClick,
isPulsing = false,
isSelected = false,
setMarkerRef,
}: DatacenterMarkerProps) {
const [hovered, setHovered] = useState(false);
const size = getMarkerSize(datacenter.capacity_mw);
const color = getOperatorColor(datacenter.operator);
const pulseDuration = getPulseDuration(datacenter.capacity_mw);
const registeredRef = useRef(false);
const handleClick = useCallback(() => {
onClick(datacenter);
}, [datacenter, onClick]);
const refCallback = useCallback(
(marker: google.maps.marker.AdvancedMarkerElement | null) => {
if (!setMarkerRef) return;
if (marker && !registeredRef.current) {
registeredRef.current = true;
setMarkerRef(marker, datacenter.id);
} else if (!marker && registeredRef.current) {
registeredRef.current = false;
setMarkerRef(null, datacenter.id);
}
},
[datacenter.id, setMarkerRef],
);
return (
<AdvancedMarker
ref={refCallback}
position={{ lat: datacenter.lat, lng: datacenter.lng }}
onClick={handleClick}
zIndex={isSelected ? 1000 : hovered ? 999 : undefined}
title={`${datacenter.name} (${datacenter.operator}) - ${datacenter.capacity_mw} MW`}>
<div
className="relative flex cursor-pointer items-center justify-center transition-transform duration-150"
style={{ transform: hovered ? 'scale(1.3)' : 'scale(1)' }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}>
{isPulsing && (
<div
className="absolute rounded-full"
style={{
width: size,
height: size,
backgroundColor: color,
animation: `marker-pulse ${pulseDuration}s ease-out infinite`,
}}
/>
)}
<div
className={`rounded-full shadow-lg ${
datacenter.status === 'under_construction'
? 'border-2 border-dashed border-white/80'
: datacenter.status === 'planned'
? 'border-2 border-white/80'
: 'border-2 border-white/80'
}`}
style={{
width: size,
height: size,
backgroundColor: datacenter.status === 'planned' ? 'transparent' : color,
boxShadow: hovered ? `0 0 12px ${color}80` : `0 2px 4px rgba(0,0,0,0.3)`,
...(datacenter.status === 'planned' ? { borderColor: color } : {}),
}}
/>
{size >= 26 && (
<div
className="pointer-events-none absolute text-white"
style={{
fontSize: '7px',
fontWeight: 700,
lineHeight: 1,
textShadow: '0 1px 3px rgba(0,0,0,0.8)',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}>
{datacenter.capacity_mw}
</div>
)}
{hovered && (
<div className="absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 rounded-md bg-zinc-900/95 px-3 py-1.5 text-xs whitespace-nowrap text-zinc-100 shadow-xl">
<div className="font-semibold">{datacenter.name}</div>
<div className="text-zinc-400">
{datacenter.operator} &middot; {datacenter.capacity_mw} MW
</div>
</div>
)}
</div>
</AdvancedMarker>
);
}