# Agent chart PNG guide

This guide covers two PNG capture flows that share the same browser hooks (`window.__chartReady`, `window.__chartPng`, `?autoPng=1`):

- **Per-chart, from `/api/create-chart`** — POST a spec, capture from the hosted chart URL. Section immediately below.
- **Page-level, from a Dealroom dashboard** (`/multiples/`, `/vc-investment-dashboard/`, `/power-law-investor-ranking/`) — multiple addressable charts per page. Rest of the doc, starting at "Page-level dashboards" below.

## Per-chart, from `/api/create-chart`

If you just generated a chart with `POST /api/create-chart`, capture its PNG directly from the hosted URL the API returns. Same browser contract as the page-level flow below — simpler entry point.

```js
// Puppeteer / Playwright
const r = await fetch('https://dealroom.co/api/create-chart?format=json', {
  method: 'POST', headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({ ...spec, bare: true })   // bare:true = chrome-free export
}).then(r => r.json());

await page.setViewport({ width: 1600, height: 900, deviceScaleFactor: 1 });  // pick aspect per channel
await page.goto(r.url);                            // r.url = /api/serve-chart/<id>
await page.waitForFunction('window.__chartReady === true');
const { dataUrl } = await page.evaluate(() => window.__chartPng({ scale: 3 }));
// dataUrl is the base64 PNG, ready to write or post.
```

Iframe variant — append `?autoPng=1` to `r.url` and listen for `postMessage({type: 'chartPng', dataUrl})`.

Knobs:

- **Aspect per channel** — set the page viewport before `goto`: 1600×900 for landscape (X/LinkedIn), 1080×1080 for square, 1080×1350 for portrait. The frame template is fluid; the chart adapts.
- **`bare: true` in the spec** — hides the theme toggle and PNG button so the export is pure chart card.
- **`scale: 3`** — retina (~2880px wide on a 1600 viewport). Bump to 4 if a channel needs higher density.

Validation before scaling: capture one chart per shape you ship (multiLine, treemap, geoMap, marketMap), eyeball against the hosted page. If anything's cropped or fonts didn't load, it's almost always viewport-too-small, missing `__chartReady` wait, or html2canvas blocked from cdnjs.

## Page-level dashboards

Dealroom pages can expose chart screenshots as polished PNG banners for agents, social posts, and QA. The contract is intentionally page-local: load the public page, wait for readiness, call the browser API, upload the returned PNG.

## Public browser contract

Every chart-enabled page should expose:

```js
await window.__chartReadyPromise;     // resolves when data + charts are rendered
window.__chartReady === true;         // boolean convenience flag
window.__chartList();                 // discover available chart IDs

const png = await window.__chartPng({
  chart: 'chart-id',                  // required unless the page has exactly one chart
  scale: 2,                           // 1 = 1500px wide, 2 = 3000px wide
  theme: 'light'                      // default; use light for social unless there is a reason not to
});

const snapshot = await window.__chartSnapshot({
  chart: 'chart-id',
  filters: { region: 'Europe' },      // optional; page-specific filter adapter applies these first
  scale: 2,
  theme: 'light'
});
```

Return shape:

```js
{
  dataUrl: 'data:image/png;base64,...',
  blob: Blob,
  filename: 'dealroom-...png',
  width: 3000,
  height: 1800,
  chart: 'chart-id',
  theme: 'light',
  filters: { ... }
}
```

## Zero-automation iframe shape

For runtimes that cannot run Playwright/Puppeteer but can load an iframe:

```js
window.addEventListener('message', (event) => {
  if (event.data?.type === 'chartPng') {
    // { chart, dataUrl, filename, filters, width, height, theme }
  }
  if (event.data?.type === 'chartPngDone') {
    // all requested charts finished
  }
});

const iframe = document.createElement('iframe');
iframe.src = 'https://dealroom.co/multiples/?autoPng=1&chart=revenue-growth-vc-rounds&scale=2&theme=light';
iframe.style.cssText = 'position:fixed;left:-10000px;width:1280px;height:900px';
document.body.appendChild(iframe);
```

Omit `chart=` to export every chart from the page.

## Discovery

Read the manifest and list IDs instead of hard-coding:

```js
const manifest = JSON.parse(document.querySelector('#chart-export-manifest').textContent);
const charts = window.__chartList().map(c => ({ id: c.id, title: c.title, subtitle: c.subtitle, filters: c.filters }));
```

Current high-value pages:

- `/vc-investment-dashboard/`
  - Custom API: `treemap`, `stack`, `loc`.
  - Filters: `region`, `minAmount`, `roundType`, `sector`.
  - Example: `window.__chartSnapshot({ chart: 'treemap', filters: { region: 'Europe', minAmount: '15' }, scale: 2 })`.
- `/power-law-investor-ranking/`
  - Use `window.__chartList()`; examples include `power-law-curve`, `power-law-ranking`, `investor-outcome-compare`.
- `/multiples/`
  - Use `window.__chartList()`; examples include `revenue-growth-vc-rounds`, `deal-size-vc-rounds`, `location-exit-rounds`.

## Agent tweet workflow

1. Start from the point of view, not the chart. Decide the claim you want to make in one sentence.
2. Pick the chart whose title directly supports that claim. If no chart supports it, do not force it.
3. Use filters that make the claim standalone in the PNG title/subtitle/filename. Prefer `region`, `sector`, `roundType`, and amount thresholds over UI-only context.
4. Fetch at `scale: 2` for final posting. Use `scale: 1` only for smoke tests.
5. Visually QA before posting:
   - headline includes geography / sector / window;
   - bars or marks fill the plot area sensibly;
   - legend is present when color encodes meaning;
   - footer/source/logo are visible;
   - no filters, hover controls, tables, or page chrome are captured;
   - no critical label is clipped.
6. Draft the tweet around the chart, not as a caption dump:
   - first line: the punchline;
   - second line: the surprising number or comparison;
   - final line: what it implies for founders/investors.

## Implementation checklist for new pages

1. Add `/js/agent-charts.js` after the page defines `window.AgentChartsConfig`.
2. Mark each exportable chart/card with `data-agent-chart`, `data-agent-title`, `data-agent-kicker`, and `data-agent-subtitle`.
3. Hide UI-only chrome in PNGs with existing ignore classes (`png-btn`, `agent-only-link`, `agent-hide`, `chart-filters`, `chart-controls`, etc.).
4. If data loads asynchronously, set `AgentChartsConfig.waitForReady` or call `window.AgentCharts.markReady()` after the first full render.
5. If agents should control filters, register a filter adapter:

```js
window.AgentCharts.registerFilterAdapter('chart-id', {
  get() { return { region: currentRegion, sector: currentSector }; },
  async set(filters) {
    applyFilters(filters);
    await renderDonePromise;
  },
  async refresh() {
    await renderDonePromise;
  }
});
```

6. For Chart.js/canvas charts, force final pixels before html2canvas captures: disable animations where possible, or call `chart.update('none')` inside the page's render/adapter path. Avoid relying on `requestAnimationFrame` as the only settle mechanism; background tabs throttle/pause it.
7. Add a manual smoke test before shipping:

```js
await window.__chartReadyPromise;
for (const c of window.__chartList().slice(0, 3)) {
  const r = await window.__chartPng({ chart: c.id, scale: 1 });
  console.log(c.id, r.filename, r.width, r.height, r.dataUrl.length);
}
```

## Production smoke results, 2026-05-24

- `dealroom.co/vc-investment-dashboard/`: `treemap`, `stack`, and `loc` exported at 1500×1216 with Europe filters; filenames include region.
- `dealroom.co/power-law-investor-ranking/`: `power-law-curve` exported at 1500×1264.
- `dealroom.co/multiples/`: `revenue-growth-vc-rounds` exported at 1500×933.
