src/lib/funnel-image.ts
Rama main · réplica de lectura del repositorio original.
import { chromium } from "playwright-core";
import { FUNNEL_TITLE, funnelOptions, toSankeyInput } from "@/components/analytics/funnel-sankey";
import { sankeyDocument, sankeyLayout } from "@/lib/funnel-sankey";
import { chromiumPath } from "@/lib/pdf";
import type { FunnelRung } from "@/lib/data/pipeline";
import { civilDay, SERVER_ZONE } from "@/lib/time";
/**
* The funnel as a file you can post.
*
* Two formats and one drawing. The SVG comes from the same emitter the page
* uses, so what you download is what you were looking at; the PNG is that SVG
* through the same headless Chromium `pdf.ts` already needs, and where the host
* has none the SVG is the answer rather than an error. Slack, LinkedIn and
* every browser open an SVG, so the degradation costs a file format rather than
* the feature.
*
* `setContent` rather than `page.goto` at an app URL: this document has no
* stylesheet, no fonts to wait for and no session to carry, so pointing a
* browser back at ourselves would mean a second authenticated request to render
* something we already have in a string.
*/
/** 2× so the image is sharp where people actually look at it. */
const SCALE = 2;
export function funnelSvg(rungs: FunnelRung[], options?: { width?: number }): string {
const layout = sankeyLayout(toSankeyInput(rungs), funnelOptions(options?.width ?? 1000, 460));
// React never renders this, so the XML declaration a standalone file wants
// has to be written by hand.
return `<?xml version="1.0" encoding="UTF-8"?>\n${sankeyDocument(layout, FUNNEL_TITLE)}`;
}
export async function funnelPng(rungs: FunnelRung[]): Promise<Buffer> {
const executablePath = chromiumPath();
if (!executablePath) {
throw new Error(
"No Chromium on this host, so the server cannot render a PNG. Download the SVG instead — it opens anywhere an image does.",
);
}
const svg = funnelSvg(rungs, { width: 1000 });
// A white ground rather than transparent: a transparent PNG pasted into a
// dark Slack shows dark text on dark, and this is a thing people paste.
const html = `<!doctype html><meta charset="utf-8"><style>
html,body{margin:0;padding:0;background:#ffffff}
#sheet{padding:28px 32px;width:1000px;box-sizing:content-box;
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
</style><div id="sheet">${svg}</div>`;
let browser = null;
try {
browser = await chromium.launch({
executablePath,
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
});
const context = await browser.newContext({ deviceScaleFactor: SCALE });
const page = await context.newPage();
await page.setContent(html, { waitUntil: "load" });
const sheet = page.locator("#sheet");
if ((await sheet.count()) === 0) throw new Error("The chart rendered nothing.");
const bytes = await sheet.screenshot({ type: "png" });
return Buffer.from(bytes);
} finally {
await browser?.close().catch(() => {});
}
}
/** `hired-funnel-2026-09-04.png`, so a downloads folder stays legible. */
export function funnelFilename(extension: "png" | "svg", timeZone = SERVER_ZONE, now = new Date()) {
return `hired-funnel-${civilDay(now, timeZone)}.${extension}`;
}