ScreenshotNeo

BlogHow-to

How to Take a Selenium Screenshot After a JavaScript-Rendered Chart Loads

Wait for a signal that means the chart is actually ready, then capture it with Selenium. Includes Python code, Chart.js and Plotly examples, and fixes for blank screenshots.

By the ScreenshotNeo team4 October 202610 min read

Wait for an application-specific signal that the chart has finished loading and rendering, then call Selenium’s screenshot method. A successful driver.get() only means the browser reached its configured document readiness state. JavaScript can still be fetching data, drawing a canvas, updating a plot, or animating the chart afterward.

The most reliable approach is to expose a stable readiness marker from the page and wait for it with Selenium’s explicit wait. If you cannot change the page, wait for a chart-library completion event or an application-specific element or JavaScript state that truly corresponds to the final chart you need.

1. Install Selenium and choose a browser

This example uses Python and Chrome. Install Selenium, then save the script below as screenshot_chart.py. Recent Selenium versions can manage compatible browser drivers through Selenium Manager; Chrome must still be installed. If you use another browser, install its browser and use its Selenium driver and options instead.

python -m pip install selenium

2. Add a chart-ready marker to the page

If you own the page, expose the precise state your screenshot needs. Set the marker only after the data has arrived and the chart has completed its initial render. If the final image must include the last animation frame, wait until that animation completes too.

<div id="sales-chart" data-chart-ready="false"></div>

<script>
async function renderSalesChart() {
  const response = await fetch('/api/sales');
  if (!response.ok) throw new Error(`Sales request failed: ${response.status}`);
  const data = await response.json();

  // Draw the chart with your charting library here.
  await drawSalesChart(document.querySelector('#sales-chart'), data);

  // Set this only once the data and required render work are complete.
  document.querySelector('#sales-chart').dataset.chartReady = 'true';
}

renderSalesChart();
</script>

The drawSalesChart function is a placeholder for your application’s renderer. If that renderer animates asynchronously, make it resolve only after the desired final frame, or set the marker from the library’s animation-complete callback. The marker’s meaning is yours to define; Selenium only observes the condition you expose.

3. Wait for readiness, then save the screenshot

Here is a complete script for a page that sets data-chart-ready="true" on the chart element. Replace the example URL and selector with your page’s URL and readiness marker.

from pathlib import Path

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

URL = "https://example.com/dashboard"
READY_SELECTOR = '#sales-chart[data-chart-ready="true"]'
OUTPUT = Path("chart.png")

options = webdriver.ChromeOptions()
# Uncomment to run without opening a visible browser window:
# options.add_argument("--headless")
options.add_argument("--window-size=1440,1200")

driver = webdriver.Chrome(options=options)
try:
    # Keep element waits explicit; do not also set an implicit wait.
    driver.implicitly_wait(0)
    driver.set_page_load_timeout(60)

    driver.get(URL)
    wait = WebDriverWait(driver, 30, poll_frequency=0.2)
    wait.until(
        EC.presence_of_element_located((By.CSS_SELECTOR, READY_SELECTOR))
    )

    if not driver.save_screenshot(str(OUTPUT)):
        raise RuntimeError(f"Selenium could not write {OUTPUT}")
    print(f"Saved {OUTPUT.resolve()}")
finally:
    driver.quit()

Run it with python screenshot_chart.py. The timeout and polling interval are examples. Set the timeout to match the page’s expected data and render times, while keeping a finite limit so a broken page does not hang the job.

This uses presence_of_element_located because the marker itself represents readiness. If your application instead reveals a separate ready element only when the chart is complete, EC.visibility_of_element_located can be appropriate. Selenium documents explicit waits as polling for a condition and warns against mixing implicit and explicit waits because their combined timing can be unpredictable. See the official Selenium waiting strategies and Python API.

4. Select a readiness condition that matches the chart

Application-owned marker or state

A marker such as data-chart-ready="true" is usually easiest to reason about because the application can make its meaning precise: required data loaded, render completed, and optional animation finished. You can also expose an app-owned JavaScript flag and poll it:

wait.until(
    lambda d: d.execute_script("return window.dashboardChartReady === true")
)

Do not set a global flag from an unrelated component or on the first render if later requests replace the data. The condition needs to represent the exact chart state you intend to capture.

Chart.js completion callback

Chart.js provides an animation.onComplete callback. Set an application marker there after the data has been loaded and the chart instance has been created. The callback indicates completion of the configured chart animation; it does not prove that a later data refresh will not change the chart.

const chartElement = document.querySelector('#sales-chart');

const chart = new Chart(chartElement, {
  type: 'line',
  data,
  options: {
    animation: {
      onComplete() {
        chartElement.dataset.chartReady = 'true';
      }
    }
  }
});

For a screenshot that does not need animation, Chart.js also allows animation to be disabled with animation: false. Still wait for your application’s data-loading and chart-update work before capture. Chart.js documents animation callbacks and configuration.

Plotly.js completion event or promise

Plotly’s plotly_afterplot event fires after a plot operation, including redraws following restyle or relayout. The promise returned by Plotly.newPlot resolves after that call has plotted. Make sure the signal you use corresponds to the last update needed for the screenshot, not merely the first plot.

const graph = document.querySelector('#sales-chart');

Plotly.newPlot(graph, data, layout, config).then(() => {
  graph.dataset.chartReady = 'true';
});

// If later updates are part of the workflow, surface readiness after
// those updates as well, or listen for the relevant plotly_afterplot event.

Plotly documents plot events, including plotly_afterplot, and the plot function reference.

When you cannot change the page

Wait for an observable condition that is causally tied to the finished chart: a known axis label, a loaded-state attribute, a specific legend item, a completed network-backed application flag, or another element added only after rendering. A visible canvas by itself is not enough. It can exist before data arrives or before pixels have been drawn. A canvas is a rendering surface, not a chart-readiness signal.

You can inspect the page’s DOM and application scripts to find a useful signal. If none exists, ask the page owner to expose one. A fixed delay can serve as a temporary diagnostic, but it is not a reliable synchronization contract: a short delay fails on slow runs and a long delay wastes time on fast ones.

5. Choose what Selenium captures

driver.save_screenshot("chart.png") captures the current browser window. Set the viewport before navigation if the chart’s responsive layout depends on window size, and scroll the chart into view when necessary. For a chart element rather than the whole viewport, Selenium’s WebElement screenshot method can save just that element:

chart = driver.find_element(By.CSS_SELECTOR, "#sales-chart")
chart.screenshot("chart-element.png")

Element screenshots are useful for a chart embedded in a dashboard, but the element must be displayed and its layout stable. If the chart is larger than the viewport or uses a canvas with responsive sizing, check the output dimensions and consider setting a suitable window size before loading the page. Selenium’s screenshot methods are described in the Python API reference.

6. Avoid timing traps

  • Do not treat navigation completion as chart completion. Selenium’s default page load strategy waits for the document’s complete state, but a single-page app can keep fetching data and rendering afterward. Page load strategies affect when navigation returns, not whether application-specific chart work is done.
  • Prefer a condition to a fixed sleep. Use WebDriverWait to poll for the required state. A sleep has no knowledge of whether the chart is ready.
  • Do not mix implicit and explicit waits. Keep the implicit wait at zero when using explicit waits for chart readiness.
  • Account for subsequent updates. A first-render callback may fire before a polling refresh, streaming update, or delayed layout change that matters to the image.
  • Wait for animation only when the final frame matters. If intermediate animation frames are acceptable or animation is disabled, the readiness condition can be simpler.

7. Troubleshoot missing or incomplete charts

Symptom Likely cause Fix
Screenshot has an empty chart area The chart container exists before its data or drawing is complete. Wait on a data-and-render readiness marker, not container presence or visibility alone.
Chart is present but has old or partial data The marker or library callback corresponds to the initial render, while a later request updates the chart. Move or reset readiness around the final update and set it only after that update completes.
TimeoutException waiting for readiness The selector is wrong, the marker is never set, data loading failed, or the timeout is shorter than the real workflow. Inspect the page and browser console, verify the marker’s exact value and timing, then adjust the timeout based on expected behavior.
Canvas exists but looks blank Canvas creation happened before data resolution or drawing. Wait for a library completion callback or application state tied to completed drawing.
Chart is clipped or uses unexpected layout The browser viewport is too small, the chart is responsive, or the page has not completed a layout-affecting update. Set the window size before navigation and include the relevant layout completion in the readiness condition.
Screenshot call returns false or no file appears The path is unwritable, invalid, or its parent directory does not exist. Use a writable absolute path, create the directory first, and check the boolean result from save_screenshot.
Navigation times out before the chart wait The page load timeout is reached while unrelated resources are still loading. Check whether the page itself is stalled. Where appropriate, consider Selenium’s eager page load strategy, then rely on an explicit chart-ready condition for the capture.
Waits take unexpectedly long Implicit and explicit waits are combined, or the condition repeatedly triggers slow element lookups. Set implicit wait to zero and keep the explicit predicate focused on one readiness condition.

For a timeout, log the current URL and inspect a diagnostic screenshot or page source before quitting the browser. Selenium’s troubleshooting guide identifies synchronization as a common source of automation failures.

8. Performance, reliability, and operating cost

Explicit polling usually avoids both needless waiting on fast pages and premature screenshots on slow pages. Keep the poll condition cheap; a DOM attribute or simple app-owned flag is generally easier to evaluate than repeatedly scanning a large page. Use finite page-load and readiness timeouts, and record which stage failed so retries do not hide a broken chart or endpoint.

Headless mode can make automated jobs easier to run in a server environment, but it does not change the need for a readiness signal. Browser startup, chart data fetching, rendering, image writing, and any remote browser session all contribute to total run time. Reuse a browser session for a batch only when pages can be isolated reliably; always quit the driver when the work ends.

With a self-managed Selenium setup, your operational costs include the machine or browser service, browser and driver maintenance, and time spent handling flaky pages. No universal runtime or cost figure applies: chart size, data source, browser, viewport, and execution environment all vary.

9. Or skip the browser setup

If you need a clean screenshot of a publicly reachable chart page without managing Selenium and a browser session, ScreenshotNeo is a website screenshot API. It captures the page as an image or PDF; for JavaScript-heavy pages, use its wait options to choose a selector, delay, or network-idle condition that fits the page. The API and options are in the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/dashboard \
  -o chart.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/dashboard"},
    timeout=90,
)
r.raise_for_status()
open("chart.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/dashboard'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('chart.webp', bytes));

ScreenshotNeo removes known cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, and failed loads are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Choose an explicit wait option if the chart needs more than the default page load to render.

Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.

10. Frequently asked questions

How do I wait for a JavaScript chart to load before taking a Selenium screenshot?

Wait with WebDriverWait for an application-owned readiness marker or a chart-library signal that represents the required final state, then call driver.save_screenshot.

Why is my Selenium screenshot missing the chart?

The chart’s container may be present before its data has loaded or before rendering has finished. A completed navigation or visible canvas does not establish chart readiness.

Can I just use time.sleep()?

It can help diagnose a synchronization problem, but it is a poor permanent wait because it can be too short on slow runs and unnecessarily long on fast ones.

Does this capture the entire page?

driver.save_screenshot captures the browser window. Use an element screenshot for a chart element, or choose a full-page capture method appropriate to your browser and Selenium setup.

Does Chart.js’s completion callback mean my data request finished?

It means the chart animation completed. Your application should fetch and apply the data before relying on that callback as the final screenshot signal.