Iman_AMS/asm_app/src/components/SimpleChart.tsx

91 lines
3.1 KiB
TypeScript

type Dataset = { name: string; values: number[]; color?: string };
interface Props {
type: 'Bar' | 'Pie' | 'Line' | string;
labels: string[];
datasets: Dataset[];
height?: number;
}
const clamp = (n: number) => (Number.isFinite(n) ? Math.max(0, n) : 0);
export default function SimpleChart({ type, labels, datasets, height = 220 }: Props) {
if (!labels?.length || !datasets?.length) {
return <div className="text-sm text-gray-500">No data</div>;
}
if (type.toLowerCase() === 'pie') {
const values = datasets[0].values.map(clamp);
const total = values.reduce((a, b) => a + b, 0) || 1;
const radius = Math.min(100, height / 2 - 10);
const cx = radius + 10;
const cy = radius + 10;
let cumulative = 0;
const colors = datasets[0].values.map((_, i) => datasets[0].color || defaultColor(i));
return (
<svg width={cx * 2} height={cy * 2} viewBox={`0 0 ${cx * 2} ${cy * 2}`}>
{values.map((v, i) => {
const startAngle = (cumulative / total) * 2 * Math.PI;
cumulative += v;
const endAngle = (cumulative / total) * 2 * Math.PI;
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
const x1 = cx + radius * Math.cos(startAngle);
const y1 = cy + radius * Math.sin(startAngle);
const x2 = cx + radius * Math.cos(endAngle);
const y2 = cy + radius * Math.sin(endAngle);
const d = `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
return <path key={i} d={d} fill={colors[i]} />;
})}
</svg>
);
}
// Bar chart (stack if multiple datasets)
const series = datasets;
const max = Math.max(...series.flatMap(s => s.values.map(clamp)), 1);
const width = Math.max(labels.length * 60, 300);
const chartHeight = height - 40;
const barWidth = Math.max(20, (width - 40) / labels.length - 10);
return (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
{/* Axis */}
<line x1={30} y1={10} x2={30} y2={chartHeight} stroke="#e5e7eb" />
<line x1={30} y1={chartHeight} x2={width - 10} y2={chartHeight} stroke="#e5e7eb" />
{labels.map((label, i) => {
const x = 40 + i * (barWidth + 10);
let yOffset = 0;
return (
<g key={i}>
{series.map((s, si) => {
const v = clamp(s.values[i] || 0);
const h = (v / max) * (chartHeight - 20);
const y = chartHeight - h - yOffset;
const color = s.color || defaultColor(si);
yOffset += h;
return <rect key={si} x={x} y={y} width={barWidth} height={h} fill={color} rx={2} />;
})}
<text x={x + barWidth / 2} y={height - 5} textAnchor="middle" fontSize="10" fill="#6b7280">
{truncate(label, 8)}
</text>
</g>
);
})}
</svg>
);
}
function defaultColor(i: number): string {
const palette = ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#6366F1', '#22C55E', '#E11D48'];
return palette[i % palette.length];
}
function truncate(s: string, n: number) {
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}