- 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)
160 lines
4.9 KiB
TypeScript
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} · {datacenter.capacity_mw} MW
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AdvancedMarker>
|
|
);
|
|
}
|