import { memo, useState, useEffect, useRef } from "react"; import type { ReactElement } from "react"; interface ChartData { labels: string[]; datasets: Array<{ label: string; data: number[]; color?: string }>; type?: "bar" | "line" | "pie" | "doughnut"; title?: string; } function parseChartData(raw: string): ChartData | null { try { return JSON.parse(raw) as ChartData; } catch { // try to extract JSON from markdown const match = raw.match(/```(?:json)?\s*([\s\S]*?)```/); if (match) { try { return JSON.parse(match[1]) as ChartData; } catch { /* continue */ } } return null; } } function polarToCart(cx: number, cy: number, r: number, angle: number) { return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) }; } function PieChart({ data, w = 240, h = 200 }: { data: ChartData; w?: number; h?: number }) { const cx = w / 2 - 10, cy = h / 2; const r = Math.min(cx, cy) - 20; const COLORS = ["#4f8ef7","#7c4dff","#22c55e","#f59e0b","#ef4444","#06b6d4","#ec4899","#a3e635"]; const all = data.datasets.flatMap(d => d.data); const total = all.reduce((a, b) => a + b, 0); if (total === 0) return null; const slices: ReactElement[] = []; let startAngle = -Math.PI / 2; all.forEach((val, i) => { const angle = (val / total) * 2 * Math.PI; const end = startAngle + angle; const mid = startAngle + angle / 2; const p1 = polarToCart(cx, cy, r, startAngle); const p2 = polarToCart(cx, cy, r, end); const largeArc = angle > Math.PI ? 1 : 0; const color = COLORS[i % COLORS.length]; slices.push( , val / total > 0.05 ? ( {Math.round((val / total) * 100)}% ) : <>, ); startAngle = end; }); const labels = data.labels || all.map((_, i) => `Item ${i+1}`); return ( {slices} {labels.slice(0, 6).map((l, i) => ( {l.slice(0, 10)} ))} ); } const ChartBlock = memo(function ChartBlock({ content }: { content: string }) { const canvasRef = useRef(null); const [error, _setError] = useState(null); const data = parseChartData(content); useEffect(() => { if (!data || !canvasRef.current) return; if (data.type === "pie" || data.type === "doughnut") return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; const COLORS = ["#4f8ef7","#7c4dff","#22c55e","#f59e0b","#ef4444","#06b6d4","#ec4899"]; const w = canvas.width, h = canvas.height; const pad = { top: 20, right: 20, bottom: 35, left: 40 }; const chartW = w - pad.left - pad.right; const chartH = h - pad.top - pad.bottom; ctx.clearRect(0, 0, w, h); const allVals = data.datasets.flatMap(d => d.data); const maxVal = Math.max(...allVals, 1); const minVal = Math.min(0, ...allVals); // Grid ctx.strokeStyle = "#1e1e2e"; ctx.lineWidth = 1; const ySteps = 4; for (let i = 0; i <= ySteps; i++) { const y = pad.top + (i / ySteps) * chartH; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + chartW, y); ctx.stroke(); const val = maxVal - (i / ySteps) * (maxVal - minVal); ctx.fillStyle = "#5a5a72"; ctx.font = "9px sans-serif"; ctx.textAlign = "right"; ctx.fillText(val.toFixed(1), pad.left - 4, y + 3); } const barGroupW = chartW / data.labels.length; const barCount = data.datasets.length; const barW = Math.min(barGroupW * 0.7 / barCount, 40); const yZero = pad.top + chartH * (maxVal / (maxVal - minVal)); data.datasets.forEach((ds, di) => { ctx.fillStyle = ds.color || COLORS[di % COLORS.length]; ctx.strokeStyle = ds.color || COLORS[di % COLORS.length]; ctx.lineWidth = 2; if (data.type === "line") { ctx.beginPath(); ds.data.forEach((val, xi) => { const x = pad.left + barGroupW * (xi + 0.5); const y = pad.top + chartH * (1 - (val - minVal) / (maxVal - minVal)); if (xi === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ds.data.forEach((val, xi) => { const x = pad.left + barGroupW * (xi + 0.5); const y = pad.top + chartH * (1 - (val - minVal) / (maxVal - minVal)); ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill(); }); } else { ds.data.forEach((val, xi) => { const x = pad.left + barGroupW * xi + (barGroupW - barCount * barW) / 2 + di * barW; const barH = Math.abs((val / (maxVal - minVal)) * chartH); const y = val >= 0 ? yZero - barH : yZero; ctx.fillRect(x, y, barW - 2, barH); }); } }); // X labels ctx.fillStyle = "#5a5a72"; ctx.font = "9px sans-serif"; ctx.textAlign = "center"; data.labels.forEach((l, i) => { const x = pad.left + barGroupW * (i + 0.5); ctx.fillText(l.slice(0, 10), x, pad.top + chartH + 14); }); }, [data]); if (!data) { return (
Formato grafico non valido. Usa JSON con: labels, datasets[].data, type (bar|line|pie).
); } const isPie = data.type === "pie" || data.type === "doughnut"; return (
{data.title && (
{data.title}
)}
{isPie ? ( ) : ( )}
{error &&
{error}
}
); } ); export default ChartBlock;