88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
'use client';
|
|
|
|
import { cn } from '@/lib/utils.js';
|
|
|
|
interface GridStressGaugeProps {
|
|
regionCode: string;
|
|
regionName: string;
|
|
demandMw: number;
|
|
capacityMw: number;
|
|
className?: string;
|
|
}
|
|
|
|
function getStressColor(pct: number): string {
|
|
if (pct >= 90) return '#ef4444';
|
|
if (pct >= 80) return '#f97316';
|
|
if (pct >= 60) return '#eab308';
|
|
return '#22c55e';
|
|
}
|
|
|
|
function getStressLabel(pct: number): string {
|
|
if (pct >= 90) return 'Critical';
|
|
if (pct >= 80) return 'High';
|
|
if (pct >= 60) return 'Moderate';
|
|
return 'Normal';
|
|
}
|
|
|
|
export function GridStressGauge({ regionCode, regionName, demandMw, capacityMw, className }: GridStressGaugeProps) {
|
|
const pct = capacityMw > 0 ? Math.min((demandMw / capacityMw) * 100, 100) : 0;
|
|
const color = getStressColor(pct);
|
|
const label = getStressLabel(pct);
|
|
|
|
const radius = 40;
|
|
const circumference = Math.PI * radius;
|
|
const offset = circumference - (pct / 100) * circumference;
|
|
|
|
const isCritical = pct >= 85;
|
|
|
|
return (
|
|
<div className={cn('flex flex-col items-center gap-2', className)}>
|
|
<svg
|
|
viewBox="0 0 100 55"
|
|
className="w-full max-w-[140px]"
|
|
style={{
|
|
filter: isCritical ? `drop-shadow(0 0 8px ${color}80)` : undefined,
|
|
}}>
|
|
{/* Background arc */}
|
|
<path
|
|
d="M 10 50 A 40 40 0 0 1 90 50"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="8"
|
|
strokeLinecap="round"
|
|
className="text-muted/40"
|
|
/>
|
|
{/* Filled arc */}
|
|
<path
|
|
d="M 10 50 A 40 40 0 0 1 90 50"
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth="8"
|
|
strokeLinecap="round"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={offset}
|
|
style={{
|
|
transition: 'stroke-dashoffset 1s ease-in-out, stroke 0.5s ease',
|
|
}}
|
|
/>
|
|
{/* Percentage text */}
|
|
<text
|
|
x="50"
|
|
y="45"
|
|
textAnchor="middle"
|
|
className="fill-foreground text-[14px] font-bold"
|
|
style={{ fontFamily: 'ui-monospace, monospace' }}>
|
|
{pct.toFixed(0)}%
|
|
</text>
|
|
</svg>
|
|
<div className="text-center">
|
|
<div className="text-xs font-semibold">{regionCode}</div>
|
|
<div className="text-[10px] text-muted-foreground">{regionName}</div>
|
|
<div className="mt-0.5 text-[10px] font-medium" style={{ color }}>
|
|
{label}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|