// ─── Chart Components (Canvas-based) ──────────────────────────────────────── // Exports: RadialGauge, LineChart, BarChart, DonutChart, SparkLine, HeatBar const { useRef, useEffect, useState, useCallback } = React; // ─── RadialGauge ───────────────────────────────────────────────────────────── function RadialGauge({ value = 0, max = 100, label, unit = '%', color = '#00c8ff', size = 120, warning = 70, critical = 90 }) { const canvasRef = useRef(null); const pct = Math.min(value / max, 1); const c = pct * 100 >= critical ? '#ff2244' : pct * 100 >= warning ? '#ffd700' : color; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; ctx.scale(dpr, dpr); const cx = size / 2, cy = size / 2, r = size / 2 - 10; const startAngle = Math.PI * 0.75, endAngle = Math.PI * 2.25; ctx.clearRect(0, 0, size, size); // Track ctx.beginPath(); ctx.arc(cx, cy, r, startAngle, endAngle); ctx.strokeStyle = 'rgba(0,100,150,0.25)'; ctx.lineWidth = 6; ctx.lineCap = 'round'; ctx.stroke(); // Tick marks for (let i = 0; i <= 10; i++) { const a = startAngle + (endAngle - startAngle) * (i / 10); const inner = r - 8, outer = r - 4; ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * inner, cy + Math.sin(a) * inner); ctx.lineTo(cx + Math.cos(a) * outer, cy + Math.sin(a) * outer); ctx.strokeStyle = 'rgba(0,200,255,0.25)'; ctx.lineWidth = 1; ctx.stroke(); } // Fill arc if (pct > 0) { const fillEnd = startAngle + (endAngle - startAngle) * pct; ctx.beginPath(); ctx.arc(cx, cy, r, startAngle, fillEnd); ctx.strokeStyle = c; ctx.lineWidth = 6; ctx.lineCap = 'round'; ctx.shadowBlur = 10; ctx.shadowColor = c; ctx.stroke(); ctx.shadowBlur = 0; } // Inner circle ctx.beginPath(); ctx.arc(cx, cy, r - 14, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0,5,15,0.8)'; ctx.fill(); ctx.strokeStyle = 'rgba(0,200,255,0.08)'; ctx.lineWidth = 1; ctx.stroke(); // Value text ctx.fillStyle = c; ctx.font = `bold ${size > 100 ? 22 : 16}px 'Share Tech Mono', monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.shadowBlur = 8; ctx.shadowColor = c; ctx.fillText(Math.round(value) + unit, cx, cy - 4); ctx.shadowBlur = 0; // Label if (label) { ctx.fillStyle = 'rgba(0,200,255,0.5)'; ctx.font = `${size > 100 ? 8 : 7}px 'Share Tech Mono', monospace`; ctx.letterSpacing = '2px'; ctx.fillText(label.toUpperCase(), cx, cy + (size > 100 ? 16 : 12)); } }, [value, max, color, size, pct, c, label, unit]); return ; } // ─── LineChart ─────────────────────────────────────────────────────────────── function LineChart({ data = [], color = '#00c8ff', height = 80, width = '100%', fill = true, label }) { const canvasRef = useRef(null); const containerRef = useRef(null); const [w, setW] = useState(300); useEffect(() => { if (!containerRef.current) return; const ro = new ResizeObserver(([e]) => setW(e.contentRect.width)); ro.observe(containerRef.current); return () => ro.disconnect(); }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas || data.length < 2) return; const dpr = window.devicePixelRatio || 1; canvas.width = w * dpr; canvas.height = height * dpr; canvas.style.width = w + 'px'; canvas.style.height = height + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, height); const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const pad = 4; const pts = data.map((v, i) => ({ x: pad + (i / (data.length - 1)) * (w - pad * 2), y: height - pad - ((v - min) / range) * (height - pad * 2) })); // Grid lines for (let i = 0; i <= 3; i++) { const y = pad + (i / 3) * (height - pad * 2); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.strokeStyle = 'rgba(0,200,255,0.06)'; ctx.lineWidth = 1; ctx.stroke(); } // Fill if (fill) { ctx.beginPath(); ctx.moveTo(pts[0].x, height); pts.forEach(p => ctx.lineTo(p.x, p.y)); ctx.lineTo(pts[pts.length - 1].x, height); ctx.closePath(); const grad = ctx.createLinearGradient(0, 0, 0, height); grad.addColorStop(0, color + '30'); grad.addColorStop(1, color + '00'); ctx.fillStyle = grad; ctx.fill(); } // Line ctx.beginPath(); pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)); ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.shadowBlur = 6; ctx.shadowColor = color; ctx.stroke(); ctx.shadowBlur = 0; // Last point dot const last = pts[pts.length - 1]; ctx.beginPath(); ctx.arc(last.x, last.y, 3, 0, Math.PI * 2); ctx.fillStyle = color; ctx.shadowBlur = 8; ctx.shadowColor = color; ctx.fill(); ctx.shadowBlur = 0; }, [data, color, height, w, fill]); return (