AUDIT / src /components /ChartBlock.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
6.86 kB
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(
<path key={i}
d={`M ${cx} ${cy} L ${p1.x} ${p1.y} A ${r} ${r} 0 ${largeArc} 1 ${p2.x} ${p2.y} Z`}
fill={color} stroke="#0d0d18" strokeWidth={2}
style={{ transition: "opacity 0.12s" }}
/>,
val / total > 0.05 ? (
<text key={`l${i}`}
x={polarToCart(cx, cy, r * 0.65, mid).x}
y={polarToCart(cx, cy, r * 0.65, mid).y}
textAnchor="middle" dominantBaseline="middle"
fill="#fff" fontSize={9} fontWeight="600">
{Math.round((val / total) * 100)}%
</text>
) : <></>,
);
startAngle = end;
});
const labels = data.labels || all.map((_, i) => `Item ${i+1}`);
return (
<svg width={w} height={h}>
{slices}
{labels.slice(0, 6).map((l, i) => (
<g key={i} transform={`translate(${w - 70}, ${20 + i * 18})`}>
<rect width={10} height={10} rx={2} fill={COLORS[i % COLORS.length]} />
<text x={14} y={9} fill="#9090a8" fontSize={9}>{l.slice(0, 10)}</text>
</g>
))}
</svg>
);
}
const ChartBlock = memo(function ChartBlock({ content }: { content: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [error, _setError] = useState<string | null>(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 (
<div style={{ padding: "0.5rem 0.75rem", background: "#12121f", borderRadius: 8, border: "1px solid #222236", color: "#fca5a5", fontSize: "0.78rem" }}>
Formato grafico non valido. Usa JSON con: labels, datasets[].data, type (bar|line|pie).
</div>
);
}
const isPie = data.type === "pie" || data.type === "doughnut";
return (
<div style={{ background: "#0d0d18", borderRadius: 10, border: "1px solid #1e1e2e", overflow: "hidden", marginBottom: "0.6em" }}>
{data.title && (
<div style={{ padding: "0.5rem 0.85rem", borderBottom: "1px solid #1e1e2e", fontSize: "0.8rem", fontWeight: 600, color: "#c0c0d0" }}>
{data.title}
</div>
)}
<div style={{ padding: "0.75rem", display: "flex", justifyContent: "center" }}>
{isPie ? (
<PieChart data={data} />
) : (
<canvas ref={canvasRef} width={420} height={220} style={{ maxWidth: "100%" }} />
)}
</div>
{error && <div style={{ padding: "0.4rem 0.85rem", color: "#fca5a5", fontSize: "0.75rem" }}>{error}</div>}
</div>
);
}
);
export default ChartBlock;