import { ChartInput, getTheme } from "../types.js";
function esc(s: string): string {
return s.replace(/&/g, "&").replace(//g, ">");
}
function truncate(s: string, n: number): string {
return s.length > n ? s.slice(0, n - 1) + "…" : s;
}
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
const rad = ((angleDeg - 90) * Math.PI) / 180;
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
}
function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number): string {
// Clamp to just under 360 to avoid degenerate full-circle arc
const end = endAngle - startAngle >= 360 ? startAngle + 359.99 : endAngle;
const s = polarToCartesian(cx, cy, r, startAngle);
const e = polarToCartesian(cx, cy, r, end);
const largeArc = end - startAngle > 180 ? 1 : 0;
return `M ${cx} ${cy} L ${s.x.toFixed(2)} ${s.y.toFixed(2)} A ${r} ${r} 0 ${largeArc} 1 ${e.x.toFixed(2)} ${e.y.toFixed(2)} Z`;
}
function describeDonutArc(cx: number, cy: number, r: number, ir: number, startAngle: number, endAngle: number): string {
const end = endAngle - startAngle >= 360 ? startAngle + 359.99 : endAngle;
const s1 = polarToCartesian(cx, cy, r, startAngle);
const e1 = polarToCartesian(cx, cy, r, end);
const s2 = polarToCartesian(cx, cy, ir, end);
const e2 = polarToCartesian(cx, cy, ir, startAngle);
const largeArc = end - startAngle > 180 ? 1 : 0;
return [
`M ${s1.x.toFixed(2)} ${s1.y.toFixed(2)}`,
`A ${r} ${r} 0 ${largeArc} 1 ${e1.x.toFixed(2)} ${e1.y.toFixed(2)}`,
`L ${s2.x.toFixed(2)} ${s2.y.toFixed(2)}`,
`A ${ir} ${ir} 0 ${largeArc} 0 ${e2.x.toFixed(2)} ${e2.y.toFixed(2)}`,
"Z",
].join(" ");
}
function buildPieOrDonut(input: ChartInput, isDonut: boolean): string {
const theme = getTheme(input.style);
const W = input.style?.width ?? 520;
const H = input.style?.height ?? 400;
const font = input.style?.fontFamily ?? "Inter, system-ui, sans-serif";
const showLegend = input.style?.showLegend !== false;
const legendH = showLegend ? Math.ceil(input.data.length / 2) * 22 + 10 : 0;
const titleH = input.title ? 40 : 10;
const availH = H - legendH - titleH;
const cx = W / 2;
const cy = titleH + availH / 2;
const r = Math.min(W / 2, availH / 2) * 0.78;
const ir = isDonut ? r * 0.52 : 0;
const total = input.data.reduce((sum, d) => sum + (d.value ?? 0), 0);
if (total <= 0) return ``;
let svg = ``;
return svg;
}
function formatCenter(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return n.toFixed(0);
}
export function generatePieChart(input: ChartInput): string {
return buildPieOrDonut(input, false);
}
export function generateDonutChart(input: ChartInput): string {
return buildPieOrDonut(input, true);
}