import { useEffect, useState } from "react"; interface SpeedometerProps { speed: string; // e.g., "80 Mbps", "1 Gbps", "1.5 Gbps" size?: number; } const Speedometer = ({ speed, size = 140 }: SpeedometerProps) => { const [animatedValue, setAnimatedValue] = useState(0); // Parse speed to get a normalized value (0-1) for indicator position // Uses logarithmic curve: fast climb for lower speeds, diminishing impact for higher speeds const getSpeedValue = (speedStr: string): number => { const lower = speedStr.toLowerCase(); let mbps: number; if (lower.includes("gbps")) { mbps = parseFloat(speedStr) * 1000; // Convert Gbps to Mbps } else { mbps = parseFloat(speedStr); } // Logarithmic curve: log(1 + x * k) / log(1 + k) // Higher k = faster climb for lower speeds, compressed difference at higher speeds const maxSpeed = 3500; const k = 50; // Higher value for more aggressive lower speed progression const normalized = Math.min(mbps / maxSpeed, 1); return Math.log(1 + normalized * k) / Math.log(1 + k); }; const targetValue = getSpeedValue(speed); // Animate on mount useEffect(() => { const duration = 1200; const startTime = performance.now(); const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Ease-out cubic for smooth deceleration const eased = 1 - Math.pow(1 - progress, 3); setAnimatedValue(eased * targetValue); if (progress < 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); }, [targetValue]); // Parse speed into number and unit const speedNumber = parseFloat(speed); const speedUnit = speed.toLowerCase().includes("gbps") ? "Gbps" : "Mbps"; const strokeWidth = 14; const radius = (size - strokeWidth) / 2; const center = size / 2; // Arc path for the gauge const polarToCartesian = (cx: number, cy: number, r: number, angleDeg: number) => { const rad = ((angleDeg - 90) * Math.PI) / 180; return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad), }; }; const describeArc = (cx: number, cy: number, r: number, startAngle: number, endAngle: number) => { const start = polarToCartesian(cx, cy, r, endAngle); const end = polarToCartesian(cx, cy, r, startAngle); const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 0 ${end.x} ${end.y}`; }; // Full arc from -135 to 135 (270 degrees) const arcPath = describeArc(center, center, radius, -135, 135); // Filled arc based on animated value const filledAngle = -135 + animatedValue * 270; const filledArcPath = describeArc(center, center, radius, -135, filledAngle); // Position for the indicator circle const indicatorPos = polarToCartesian(center, center, radius, filledAngle); const uniqueId = `speedGradient-${speed.replace(/\s/g, '-')}`; // Gradient with lighter green tones const getGradientColors = () => { return { start: "#34d399", mid: "#4ade80", end: "#86efac" }; }; const colors = getGradientColors(); return (
{/* Background arc (grey) */} {/* Colored gradient arc */} {/* Indicator circle with shadow */} {/* Speed number - large */} {speedNumber} {/* Speed unit - small */} {speedUnit}
); }; export default Speedometer;