91 lines
3.1 KiB
TypeScript
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;
|
|
}
|
|
|
|
|