Menu
Sign up

Chart to image: three ways to turn data into a PNG, measured

Turn data into a chart image: Chart.js in headless Chrome, a Chart.js image service, or a designed chart template. Code, render times and the traps.

Published November 8, 2026 · 5 min read

A chart as an image is what you need wherever JavaScript does not run: an email, a Slack or Discord message, a social post, a PDF report, an Open Graph image for a stats page. There are three ways to make one, and they trade flexibility against effort differently. This page shows each with tested code and the times we measured.

How we measured. On 2026-11-08: Chart.js 4.6.1 in Puppeteer 25.7's headless Chrome on our machine, QuickChart's public endpoint over the internet, and our chart templates through our API on our development stack. Same data everywhere: six weeks of signups, 412 to 611 (example numbers).

1. Chart.js in headless Chrome

The most flexible: any chart Chart.js can draw, styled exactly as on your site. The trap is the animation.

import puppeteer from 'puppeteer';

const html = `<!doctype html><body style="margin:0;background:#fff">
<canvas id="c" width="1200" height="630"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.6.1/dist/chart.umd.min.js"></script>
<script>
  window.chart = new Chart(document.getElementById('c'), {
    type: 'bar',
    data: { labels: ['W1','W2','W3','W4','W5','W6'],
            datasets: [{ label: 'Signups', data: [412,388,455,497,530,611], backgroundColor: '#0ea5e9' }] },
    options: { responsive: false, animation: false, plugins: { legend: { display: false } } },
  });
</script></body>`;

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(html, { waitUntil: 'load' });
await page.screenshot({ path: 'chart.png' });
await browser.close();

What the animation does to the picture, measured by the blue bar area in the screenshot:

Chart options and wait Bar area Time to the screenshot
Default animation, screenshot at load 16% of the final 0.2–0.5 s
animation: false 100% 0.19 s
Default animation, then waitForFunction(() => !Chart.animator.running(window.chart)) 100% 1.18 s

So turn animation off for images (Chart.js animations). Two more settings matter: responsive: false with a fixed canvas size, so the chart does not resize to the viewport, and deviceScaleFactor: 2 in setViewport for a sharp picture on retina screens. The rest of this approach is running Chrome yourself — memory, fonts, updates — the same as any Puppeteer screenshot.

2. A Chart.js image service

QuickChart is an open-source (AGPL-3.0) "chart image and QR code web API": you send a Chart.js configuration in the URL and get a PNG back, with a hosted endpoint at quickchart.io or your own instance.

C='{"type":"bar","data":{"labels":["W1","W2","W3","W4","W5","W6"],"datasets":[{"label":"Signups","data":[412,388,455,497,530,611]}]}}'
curl -s -o chart.png -G https://quickchart.io/chart \
  --data-urlencode "c=$C" --data-urlencode "width=1200" --data-urlencode "height=630"

It answered in 1.0 s and then 0.6 s with a 2400×1260 PNG of 37 KB (it draws at twice the size you ask for). No browser to run, any Chart.js chart, and the URL works in an <img> or an email. What it does not give you is a design around the chart: title, branding, source line and layout are yours to build in the Chart.js config.

3. A chart template

The third way starts from the finished image: a designed card with the chart, a title, a unit line, a source and your logo, filled from data. Our API has three such templates, and the data goes in as plain text:

curl -X POST "https://shareglint.com/v1/images?wait=true" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template": "bar-chart", "modifications": {
        "title": "Weekly signups",
        "values": "W1:412, W2:388, W3:455, W4:497, W5:530, W6:611",
        "unit": "Signups",
        "source": "Source: product analytics"}}'
Template Data field What we rendered Time
bar-chart values: "Label:number, …" 6 weeks of signups 1.08 s
line-chart series, plus highlight for one point 6 months of a falling median 0.99 s
donut-chart segments, plus center_value 5 traffic sources, 48k in the centre 0.67 s

Each came back as a 2400×1260 PNG: the 1200×630 card at the default 2× scale. Ask for "size": "square" and the same chart comes out 1080×1080 (2160×2160 at 2×); every size preset works.

The parser is forgiving because spreadsheet data is messy: commas, semicolons or new lines between values; "1,200", "1.200,5" and "12 400" read as numbers; a part without a number is skipped instead of failing the image. And it has limits worth knowing:

  • bar-chart draws at most 12 bars — we sent 14 and got the first 12 — and a negative value is drawn as a bar of zero height with its label ("-3") kept.
  • line-chart takes up to 24 points; beyond that the segments get shorter than the dots.
  • donut-chart shows the six largest segments; the rest stays in the ring as one unlabelled grey arc, so the shares still add up. A segment can carry its own colour ("Search:42 #16a34a").

What a template does not do is draw an arbitrary chart type: no scatter plots, stacked bars, dual axes or maps. For those, Chart.js (ways 1 or 2) is the tool. Templates also cannot load Chart.js themselves — scripts and external files are stripped from template HTML — so a custom chart template is drawn with HTML and CSS.

Which one?

You need Use
Any chart type, your own styling, you run servers anyway Chart.js in headless Chrome
Any Chart.js chart, no browser to run A Chart.js image service
A branded card with one chart, in many sizes, filled from a sheet or a webhook A chart template

The template route fits recurring images: a weekly stats post, a "your month in numbers" email per customer, an OG image for a public dashboard. Fill it from a CSV or Google Sheets row by row (bulk images from a sheet), or put a signed URL in the page so the image renders on first view (signed image URLs).

One more case: a chart that already lives on a public web page (your dashboard, a report) can be captured as it is with a screenshot and selector for just the chart element. Our screenshot capture waits for the network to go quiet but not for animations, so add "delay": 2 for animated charts (blank or blurry screenshots explains why).

A template render costs one render of the quota at the default 2× scale; the same data again within the cache period is free (pricing).

FAQ

How do I convert a chart to an image?

Render it where a browser can draw it and save the picture. Chart.js in headless Chrome with page.screenshot() works if you turn the animation off; a chart image service such as QuickChart takes a Chart.js config in a URL and returns a PNG; a chart template takes labels and values and returns a finished, branded image.

Why is my Chart.js screenshot showing half-drawn bars?

Chart.js animates by default for about a second. A screenshot taken at load caught the bars at 16% of their final area in our test. Set animation: false in the chart options, or wait until Chart.animator.running(chart) is false before the screenshot (1.18 s in our test).

How many values can a chart template show?

Our bar-chart template draws up to 12 bars, line-chart up to 24 points and donut-chart its six largest segments, with the rest as one grey arc so the shares still add up. Values go in as text such as "W1:412, W2:388"; a part without a number is skipped instead of failing the image.