ScreenshotNeo

BlogHow-to

Puppeteer Screenshot of a Canvas Chart: Wait for the Chart to Appear

Wait for an application-specific chart-ready signal before taking a Puppeteer screenshot. Canvas presence and network idle alone do not guarantee a complete chart.

By the ScreenshotNeo team4 October 20268 min read

To capture a complete chart rendered on a canvas, wait for a condition that means the chart has finished rendering, then call page.screenshot(). In Puppeteer, page.waitForFunction() can poll an application-specific readiness signal. A canvas appearing in the DOM—or the page reaching network idle—does not by itself prove that the chart pixels are ready.

1. Choose a real chart-ready signal

The readiness condition must come from your application or chart integration. Good candidates include a documented render-complete callback that sets an app-owned flag, or a chart-specific state that reliably indicates the final drawing is done. The right signal depends on the chart library and application; there is no universal canvas property that proves every chart is complete.

For example, if your application sets window.chartReady only after its chart has rendered, wait for that flag together with the canvas:

await page.waitForFunction(() => {
  const canvas = document.querySelector('#chart canvas');
  return canvas && window.chartReady === true;
}, { timeout: 30000 });

await page.screenshot({ path: 'chart.png' });

Replace #chart canvas and window.chartReady with values defined by your page. The flag must be set after the chart’s final draw, not merely when chart initialization starts. Puppeteer documents waitForFunction() as waiting until the supplied page function returns a truthy value. See the API documentation.

2. Runnable Node.js example

This example starts a local HTTP server to serve a test page, launches Chromium through Puppeteer, waits for an app-owned signal, captures the chart, and closes the browser and server. Save it as capture-chart.cjs, install Puppeteer with npm install puppeteer, then run node capture-chart.cjs. The example chart draws directly on a canvas and sets its ready flag after drawing, so the predicate has a defined meaning.

const http = require('node:http');
const puppeteer = require('puppeteer');

const html = `<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Canvas chart capture example</title>
    <style>
      body { font: 16px sans-serif; margin: 2rem; }
      canvas { display: block; width: 640px; height: 320px; }
    </style>
  </head>
  <body>
    <main id="chart"><canvas width="1280" height="640"></canvas></main>
    <script>
      window.chartReady = false;
      const canvas = document.querySelector('#chart canvas');
      const ctx = canvas.getContext('2d');

      // Stand in for asynchronous data loading and chart rendering.
      setTimeout(() => {
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.strokeStyle = '#d1d5db';
        ctx.lineWidth = 2;
        for (let y = 80; y < canvas.height; y += 120) {
          ctx.beginPath();
          ctx.moveTo(80, y);
          ctx.lineTo(canvas.width - 40, y);
          ctx.stroke();
        }
        ctx.strokeStyle = '#2563eb';
        ctx.lineWidth = 8;
        ctx.beginPath();
        ctx.moveTo(100, 460);
        ctx.lineTo(360, 320);
        ctx.lineTo(620, 370);
        ctx.lineTo(900, 180);
        ctx.lineTo(1180, 220);
        ctx.stroke();
        window.chartReady = true;
      }, 500);
    </script>
  </body>
</html>`;

async function main() {
  const server = http.createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
    res.end(html);
  });
  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  const address = server.address();
  const url = `http://127.0.0.1:${address.port}/`;
  let browser;

  try {
    browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage({ viewport: { width: 900, height: 600 } });
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    await page.waitForFunction(() => {
      const canvas = document.querySelector('#chart canvas');
      return canvas && window.chartReady === true;
    }, { timeout: 10000 });
    await page.screenshot({ path: 'chart.png' });
    console.log('Saved chart.png');
  } finally {
    if (browser) await browser.close();
    await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The built-in page deliberately owns window.chartReady. In a real application, set an equivalent signal from the chart’s documented completion hook. If chart data can change after the first render, reset the flag before each update and set it again only after the new draw finishes.

3. Pick the right Puppeteer wait

Wait method What it establishes When to use it
page.waitForSelector() A matching DOM element exists; options can also require it to be visible or hidden. Wait for the chart container or canvas to be inserted or shown. It does not establish that canvas pixels are complete.
page.waitForFunction() Your page-context predicate returns a truthy value. Wait for an app-owned ready flag, chart state, or other chart-specific completion signal.
page.goto(..., { waitUntil: 'networkidle2' }) Navigation reaches Puppeteer’s documented network-idle condition. Useful as a navigation wait in some pages, but asynchronous chart drawing may continue afterward.

waitForSelector() supports visibility, hidden state, timeout, and abort signal options. Use it when the question is whether the element is present or visible, not as a substitute for a render-complete signal. Selector wait options. Puppeteer’s screenshot guide shows networkidle2 as a navigation option; pair it with a chart readiness predicate when the chart renders asynchronously. Screenshot guide.

4. Configure polling and timeouts

waitForFunction() accepts a page function, optional arguments, and options including a timeout and polling mode. Its documented default timeout is 30 seconds; setting timeout to 0 disables the timeout. Prefer a finite timeout so automation fails clearly when the page never becomes ready.

await page.waitForFunction(
  (selector) => {
    const canvas = document.querySelector(selector);
    return canvas && window.chartReady === true;
  },
  {
    timeout: 45000,
    polling: 'raf'
  },
  '#chart canvas'
);

The raf polling mode checks through animation frames and can be useful for visual state changes. Other documented polling choices include a mutation-based mode and a numeric interval. Faster polling cannot repair a predicate that becomes true too early or never reflects final drawing. See the wait function options.

5. Capture the intended pixels

After the readiness promise resolves, capture the page or a defined region. For a full-page image, pass fullPage: true. If the chart is below the fold or depends on lazy loading, ensure the chart has been scrolled into view and that your application’s readiness signal accounts for any required loading.

// Viewport screenshot
await page.screenshot({ path: 'chart.png' });

// Full document screenshot
await page.screenshot({ path: 'chart-full.png', fullPage: true });

Puppeteer’s screenshot method can save to a path or return image data depending on the options and overload. Page.screenshot() API.

6. Troubleshoot blank or incomplete charts

Symptom Likely cause Fix
The canvas exists, but the image is blank. The wait checks DOM presence rather than chart completion, or the chart failed before drawing. Wait on an app-owned render-complete signal. Also inspect browser console errors and data-loading failures.
The screenshot contains only some series or labels. The ready signal fires after initialization or partial rendering. Move the signal update to the chart’s final render callback or after all asynchronous data and overlays have completed.
waitForFunction() times out. The predicate is wrong, the chart did not load, the selector differs, or the application never sets its ready flag. Evaluate the selector and flag in the page, confirm the chart’s success path, and use a timeout appropriate to the real load time.
Network idle occurs, but the chart is still changing. Canvas drawing can happen after requests finish, for example in a later task or animation frame. Use network idle only for navigation readiness, then wait for a chart-specific condition.
Screenshot clips the chart. The chart is larger than the viewport or the capture is viewport-only. Use fullPage: true for the whole document, or set a viewport that fits the desired chart dimensions.
Locator actions succeed but the chart is incomplete. Locator preconditions such as a stable bounding box establish action readiness, not chart render completion. Keep interaction waits and chart readiness waits separate; check the app’s render state before capturing.

Puppeteer Locators wait for interaction preconditions, including a stable bounding box across animation frames for relevant actions. Those checks help make interactions reliable but are not a general canvas-render-complete signal. Page interaction guide.

7. Reliability, performance, and cost

  • Reliability: Tie the predicate to the final chart state, and make sure it can become true on both success and error paths. On failure, include a useful timeout and log the page console and request failures to distinguish a slow chart from a failed one.
  • Dynamic updates: If the page redraws after an initial render, a one-time ready flag can become stale. Reset it before data changes and set it after the updated chart is complete.
  • Performance: Avoid long fixed sleeps when a reliable signal exists. A predicate wait can proceed as soon as the chart is ready, while a fixed delay either wastes time or remains too short under load.
  • Cost: Puppeteer itself is an automation library; operational cost depends on where Chromium runs and how often captures run. Account for browser memory and CPU, concurrency, execution time, and any hosting charges. The research sources provide no benchmark or fixed cost figure.
  • Versioning: The cited documentation covers Puppeteer 25.x. Check the API docs for the version installed in your project if option behavior differs.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request captures a URL as PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API documentation for its options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Details at ScreenshotNeo. For a canvas chart, the page still needs to expose a suitable readiness condition if its drawing is asynchronous.

Sign up free for 1,000 screenshots a month, with no card.

FAQ

Does waiting for the canvas selector guarantee the chart is drawn?

No. It confirms the element exists (and, if requested, is visible). Wait for a chart-specific or application-owned completion signal to know the pixels are ready.

Can I use a fixed delay instead?

You can, but a fixed delay is a guess: it may waste time on fast loads and still capture too early on slow ones. Prefer a condition that reflects the chart’s final state.

Does this work with every chart library?

The Puppeteer waiting pattern is general, but the readiness signal is library- and application-specific. Use the chart integration’s documented completion mechanism or expose an app-owned flag.

Should I use Locator stability as the readiness check?

No. Locator stability helps determine whether an element is ready for an interaction; it does not establish that a canvas has finished drawing its chart.