export function imageContentDisposition(displayName: string, fallbackName: string): string { const safeDisplayName = displayName.trim() || fallbackName; const fallback = safeDisplayName .normalize("NFKD") .replace(/[^a-z0-9]+/giu, "-") .replace(/^-|-$/gu, "") || fallbackName; const encoded = encodeURIComponent(`${safeDisplayName}.png`).replace( /['()*]/gu, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`, ); return `attachment; filename="${fallback}.png"; filename*=UTF-8''${encoded}`; } export interface ConstellationChartRow { level: number; percent: number; isGreat: boolean; widthPercent: number; } export function constellationChartRows( rows: ReadonlyArray<{ level: number; cumulativePercent: string; isGreat: boolean; }>, ): ConstellationChartRow[] { const values = [ ...rows.map((row) => ({ level: row.level, percent: Number(row.cumulativePercent), isGreat: row.isGreat, })), { level: 0, percent: 100, isGreat: false }, ].sort((left, right) => right.level - left.level); const maximum = Math.max(100, ...values.map((row) => row.percent)); return values.map((row) => ({ ...row, widthPercent: Math.max(0, Math.min(100, (row.percent / maximum) * 100)), })); }