import type { ChartSpec } from "../lib/chartspec"; // ChartView:把 chart spec 自绘成 SVG(bar / line / pie),无第三方图表依赖。 const PALETTE = ["#60a5fa", "#34d399", "#fbbf24", "#f87171", "#a78bfa", "#22d3ee", "#fb923c", "#4ade80"]; export function ChartView({ spec }: { spec: ChartSpec }) { return (
{spec.title &&
{spec.title}
} {spec.type === "pie" ? : } {spec.type !== "pie" && spec.series.length > 1 && s.name || `系列${i + 1}`)} />}
); } function Legend({ names }: { names: string[] }) { return (
{names.map((n, i) => ( {n} ))}
); } // BarLine:柱状图 / 折线图(共用坐标系)。 function BarLine({ spec }: { spec: ChartSpec }) { const W = 480, H = 220, padL = 40, padB = 28, padT = 8, padR = 8; const plotW = W - padL - padR, plotH = H - padT - padB; const all = spec.series.flatMap((s) => s.data); const max = Math.max(1, ...all); const min = Math.min(0, ...all); const span = max - min || 1; const y = (v: number) => padT + plotH - ((v - min) / span) * plotH; const n = spec.labels.length; const slot = plotW / n; return ( {/* y 轴基准线 */} {fmt(max)} {fmt(min)} {spec.type === "bar" ? spec.series.map((s, si) => s.data.map((v, i) => { const bw = (slot * 0.7) / spec.series.length; const x = padL + i * slot + slot * 0.15 + si * bw; return ( {`${spec.labels[i]}: ${v}`} ); }), ) : spec.series.map((s, si) => { const pts = s.data.map((v, i) => `${padL + i * slot + slot / 2},${y(v)}`).join(" "); return ( {s.data.map((v, i) => ( {`${spec.labels[i]}: ${v}`} ))} ); })} {/* x 轴标签 */} {spec.labels.map((lb, i) => ( {lb.length > 6 ? lb.slice(0, 6) + "…" : lb} ))} ); } // Pie:饼图(用第一条系列)。 function Pie({ spec }: { spec: ChartSpec }) { const data = spec.series[0]?.data ?? []; const total = data.reduce((a, b) => a + Math.max(0, b), 0) || 1; const cx = 110, cy = 110, r = 90; let acc = -Math.PI / 2; // 从 12 点方向起 const arcs = data.map((v, i) => { const frac = Math.max(0, v) / total; const a0 = acc; const a1 = acc + frac * Math.PI * 2; acc = a1; const large = a1 - a0 > Math.PI ? 1 : 0; const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0); const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1); const d = `M${cx},${cy} L${x0.toFixed(2)},${y0.toFixed(2)} A${r},${r} 0 ${large} 1 ${x1.toFixed(2)},${y1.toFixed(2)} Z`; return { d, color: PALETTE[i % PALETTE.length], label: spec.labels[i], pct: Math.round(frac * 100) }; }); return (
{arcs.map((a, i) => ( {`${a.label}: ${data[i]} (${a.pct}%)`} ))}
{arcs.map((a, i) => ( {a.label} · {a.pct}% ))}
); } function fmt(v: number): string { if (Math.abs(v) >= 1000) return (v / 1000).toFixed(1) + "k"; return String(Math.round(v * 100) / 100); }