Convert Chart.js to Image: PNG, SVG, WebP, and PDF

4 min readChart-Output team

Chart.js is built for the browser — it draws into a <canvas> element using the DOM. That's fine for an interactive dashboard. It's a dead end the moment you need that same chart somewhere a browser doesn't exist: an email, a PDF report, a Slack message, a nightly cron job.

What you actually need in those cases isn't "run Chart.js differently" — it's a static image: a PNG, SVG, WebP, or PDF generated from the same config you already have.

The approach most people reach for first: node-canvas

The most common DIY path is polyfilling the Canvas API in Node so Chart.js can draw somewhere that isn't a browser. Libraries like chartjs-node-canvas provide a Node-compatible canvas environment for Chart.js, typically built on node-canvas (a native Cairo-based Canvas implementation) — check the package's own docs for the exact dependency chain in the version you're using, since implementation details vary by package and version.

javascript
const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: 600, height: 400 }); const image = await chartJSNodeCanvas.renderToBuffer({ type: 'bar', data: { labels: ['Jan', 'Feb', 'Mar'], datasets: [{ label: 'Revenue', data: [12000, 15000, 13500] }] } });

This works, and it's genuinely the right call for plenty of projects. But it comes with two well-known costs:

  • node-canvas is a native module. It depends on Cairo, Pango, and libjpeg being present on the host system. Installing it is usually fine on a local machine with the right system libraries, but it's a recurring source of friction on serverless platforms, Docker images that don't include the right build tools, or CI environments — npm install failing on a native node-gyp build step is a common complaint with this approach.
  • Rendering fidelity can drift from the browser. Because node-canvas reimplements Canvas rather than using an actual browser rendering engine, fonts, gradients, and some layout details can render subtly differently than what you see in Chart.js running in Chrome or Firefox.

The other DIY path: headless browser screenshotting

The alternative is Puppeteer or Playwright: launch a real headless browser, load a page with actual Chart.js running in it, screenshot the canvas. This is usually the closest match to browser rendering, because it runs a real browser — at the cost of Chromium as a dependency: larger deploy size, slower cold starts, and a browser process to manage. It's a legitimate approach, and one you may already have available if your stack uses headless browsers elsewhere (e2e testing, PDF export of full pages). We cover the honest tradeoff between this and native-canvas rendering in chartjs-node-canvas vs Puppeteer.

The third option: a rendering API

If you don't want to own either a native-module build chain or a browser process, you can send your Chart.js config to an API and get the image back directly:

javascript
const response = await fetch("https://www.chart-output.com/api/v1/render", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.CHART_OUTPUT_API_KEY}` }, body: JSON.stringify({ type: "bar", data: { labels: ["Jan", "Feb", "Mar"], datasets: [{ label: "Revenue", data: [12000, 15000, 13500] }] }, format: "png" }) }); const imageBuffer = await response.arrayBuffer();

Because Chart-Output accepts the same type/data/options structure Chart.js itself uses, there's no new config format to learn. No native build step, no browser to run — the request returns rendered bytes directly, with a median response time around 133ms.

If you'd rather see it before writing any code, try this config in the playground — it renders the same bar chart to a PNG in the browser, no sign-up needed.

Choosing an output format

  • PNG — the universal default; safest choice for email clients and anywhere compatibility matters most
  • SVG — stays crisp at any zoom or print size
  • WebP — smaller file size for web-first delivery
  • PDF — for charts embedded directly into generated PDF reports

Getting a URL instead of raw bytes

For cases where you need to reference the image by URL rather than handle bytes directly — an <img src> in an email or Slack message — set returnUrl: true and you'll get a CDN-hosted URL back instead.

Which approach fits your situation

  • Rendering a handful of charts, comfortable managing native dependencies or already have a working build pipeline → node-canvas is a reasonable, zero-marginal-cost choice
  • Need the closest practical match to Chromium rendering and already run headless-browser infrastructure for other reasons → Puppeteer/Playwright
  • Rendering at real volume (batch reports, per-user emails), deploying to serverless, or would rather not own either a native build chain or a browser process as part of your operational surface → a rendering API

The quick start guide gets you from an existing Chart.js config to a rendered image in about five minutes if you want to compare it directly against whatever you're running now.

More from the blog