const CHART_PALETTE = ['#6f9f48', '#d07b50', '#7d8c72', '#b7d889', '#53684b', '#d7ad77', '#8a6f58', '#9eb0a0']; export interface ChartInput { title: string; labels: string[]; values: number[]; colors?: string[]; } function escapeHtml(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function normalizedInput(input: ChartInput): Required { const title = input.title.trim().slice(0, 120) || 'Untitled chart'; if (!Array.isArray(input.labels) || !Array.isArray(input.values)) { throw new Error('chart labels and values must be arrays'); } if (input.labels.length === 0 || input.labels.length > 32) { throw new Error('chart requires 1–32 data points'); } if (input.labels.length !== input.values.length) { throw new Error('chart labels and values must have the same length'); } const labels = input.labels.map((label) => String(label).trim().slice(0, 80)); const values = input.values.map((value) => { const numeric = Number(value); if (!Number.isFinite(numeric) || numeric < 0) { throw new Error('chart values must be finite non-negative numbers'); } return numeric; }); const colors = labels.map((_, index) => { const candidate = input.colors?.[index]; return typeof candidate === 'string' && /^#[0-9a-f]{6}$/iu.test(candidate) ? candidate : CHART_PALETTE[index % CHART_PALETTE.length]!; }); return { title, labels, values, colors }; } function chartShell(title: string, body: string): string { return ` ${escapeHtml(title)}

${escapeHtml(title)}

${body}
`; } export function generateBarChartHtml(input: ChartInput): string { const { title, labels, values, colors } = normalizedInput(input); const maximum = Math.max(...values, 1); const rows = labels.map((label, index) => { const value = values[index]!; const percentage = value / maximum * 100; return `
  • ${escapeHtml(label)} ${escapeHtml(value.toLocaleString('en-US'))}
  • `; }).join(''); return chartShell(title, `
      ${rows}

    Scale maximum: ${escapeHtml(maximum.toLocaleString('en-US'))}

    `); } export function generatePieChartHtml(input: ChartInput): string { const { title, labels, values, colors } = normalizedInput(input); const total = values.reduce((sum, value) => sum + value, 0); if (total <= 0) throw new Error('pie_chart requires a positive total'); let cursor = 0; const stops = values.map((value, index) => { const start = cursor; cursor += value / total * 100; return `${colors[index]!} ${start.toFixed(4)}% ${cursor.toFixed(4)}%`; }).join(', '); const legend = labels.map((label, index) => { const value = values[index]!; const percentage = value / total * 100; return `
  • ${escapeHtml(label)}${percentage.toFixed(1)}%${escapeHtml(value.toLocaleString('en-US'))}
  • `; }).join(''); return chartShell(title, `

    Total: ${escapeHtml(total.toLocaleString('en-US'))}

    `); }