// ─── 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 (
); } // ─── BarChart ───────────────────────────────────────────────────────────────── function BarChart({ data = [], color = '#00c8ff', height = 80 }) { 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) 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 max = Math.max(...data.map(d => d.value), 1); const gap = 2, bw = (w - gap * (data.length - 1)) / data.length; data.forEach((d, i) => { const bh = ((d.value / max) * (height - 14)); const x = i * (bw + gap), y = height - bh - 10; const c = d.color || color; ctx.fillStyle = c + '20'; ctx.fillRect(x, y, bw, bh); ctx.fillStyle = c; ctx.shadowBlur = 4; ctx.shadowColor = c; ctx.fillRect(x, y, bw, 1.5); ctx.shadowBlur = 0; if (d.label) { ctx.fillStyle = 'rgba(0,200,255,0.4)'; ctx.font = `7px 'Share Tech Mono', monospace`; ctx.textAlign = 'center'; ctx.fillText(d.label, x + bw / 2, height - 2); } }); }, [data, color, height, w]); return (
); } // ─── DonutChart ────────────────────────────────────────────────────────────── function DonutChart({ segments = [], size = 100, label, value }) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const dpr = window.devicePixelRatio || 1; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, size, size); const cx = size / 2, cy = size / 2, r = size / 2 - 8; const total = segments.reduce((s, d) => s + d.value, 0) || 1; let angle = -Math.PI / 2; segments.forEach(seg => { const sweep = (seg.value / total) * Math.PI * 2; ctx.beginPath(); ctx.arc(cx, cy, r, angle, angle + sweep); ctx.arc(cx, cy, r - 12, angle + sweep, angle, true); ctx.closePath(); ctx.fillStyle = seg.color + '30'; ctx.fill(); ctx.strokeStyle = seg.color; ctx.lineWidth = 1.5; ctx.shadowBlur = 6; ctx.shadowColor = seg.color; ctx.stroke(); ctx.shadowBlur = 0; angle += sweep; }); // Center ctx.beginPath(); ctx.arc(cx, cy, r - 14, 0, Math.PI * 2); ctx.fillStyle = 'rgba(0,5,15,0.9)'; ctx.fill(); if (value !== undefined) { ctx.fillStyle = '#00c8ff'; ctx.font = `bold ${size > 80 ? 18 : 13}px 'Share Tech Mono', monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.shadowBlur = 6; ctx.shadowColor = '#00c8ff'; ctx.fillText(value, cx, cy - 4); ctx.shadowBlur = 0; } if (label) { ctx.fillStyle = 'rgba(0,200,255,0.45)'; ctx.font = `7px 'Share Tech Mono', monospace`; ctx.fillText(label.toUpperCase(), cx, cy + (size > 80 ? 12 : 9)); } }, [segments, size, label, value]); return ; } // ─── HorizontalBar ──────────────────────────────────────────────────────────── function HorizontalBar({ value = 0, max = 100, color = '#00c8ff', height = 6, showLabel = true, label, sublabel }) { const pct = Math.min(value / max, 1) * 100; const c = pct >= 90 ? '#ff2244' : pct >= 70 ? '#ffd700' : color; return (
{(label || sublabel) && (
{label} {sublabel || `${Math.round(pct)}%`}
)}
); } // ─── SparkLine (inline tiny) ────────────────────────────────────────────────── function SparkLine({ data = [], color = '#00c8ff', width = 60, height = 24 }) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas || data.length < 2) return; const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, width, height); const min = Math.min(...data), max = Math.max(...data), range = max - min || 1; const pts = data.map((v, i) => ({ x: (i / (data.length - 1)) * width, y: height - ((v - min) / range) * height })); 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.stroke(); }, [data, color, width, height]); return ; } Object.assign(window, { RadialGauge, LineChart, BarChart, DonutChart, HorizontalBar, SparkLine });