How-To Recipe
zumerlab/snapdom

How to save a Chart.js chart as a PNG

Chart.js draws to a <canvas>, so its built-in toBase64Image() gives you the plot and nothing else. When you want the title, HTML legend and container in the picture too, capture the whole component with snapDOM.

TL;DR

Wrap the chart, wait for it to finish drawing, then const img = await snapdom.toPng(wrapper). You get one PNG with the canvas and everything around it.

The problem

Chart.js renders inside a <canvas> element. Its native chart.toBase64Image() exports exactly that canvas — the plotting area — and drops anything you laid out in HTML around it: a heading, a custom legend, annotations, the card background. If your export needs to look like the chart your users actually see on the page, you need to capture the surrounding DOM as well.

snapDOM captures a DOM subtree — including live <canvas> pixels — into a single image, so the chart and its frame come out as one composited PNG.

Step 1 — Install

npm install @zumer/snapdom chart.js

Without a bundler, load both from a CDN:

import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs";
import Chart from "https://cdn.jsdelivr.net/npm/chart.js/auto/+esm";

Step 2 — Wrap the chart in a capture target

Put the canvas inside an element that also holds whatever you want in the exported image:

<div id="chart-card" style="padding:20px;background:#fff;border-radius:12px;">
  <h3>Monthly revenue</h3>
  <canvas id="chart"></canvas>
</div>

Step 3 — Wait for the draw, then capture

A canvas only holds pixels once Chart.js has painted them. Capturing during the entry animation gives you a blank or half-drawn plot. The reliable fix is to capture from the animation's onComplete callback:

import { snapdom } from '@zumer/snapdom';
import Chart from 'chart.js/auto';

const chart = new Chart(document.getElementById('chart'), {
  type: 'bar',
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr'],
    datasets: [{ label: 'Revenue', data: [12, 19, 9, 22] }]
  },
  options: {
    animation: {
      onComplete: async () => {
        const img = await snapdom.toPng(document.getElementById('chart-card'));
        document.body.appendChild(img);
      }
    }
  }
});

Prefer a static export with no timing games? Set options.animation = false and capture right after new Chart(...) returns.

Step 4 — Download it as a file

const result = await snapdom(document.getElementById('chart-card'));
await result.download({ format: 'png', filename: 'revenue-chart' });

Or get a Blob to upload:

const blob = await snapdom.toBlob(document.getElementById('chart-card'));

Expected result

A single PNG that matches what's on screen: the <h3> heading, the card's white background and rounded corners, and the fully rendered bar chart — all in one image at your device pixel ratio. Pass { scale: 2 } to toPng for a sharper, higher-resolution export.

When to use snapDOM (and when not to)

Known limitations

Frequently asked questions

My exported chart is blank — why?

You captured before Chart.js finished drawing. Capture inside options.animation.onComplete, or set options.animation = false so the chart paints synchronously.

Can I export just the canvas without the surrounding HTML?

Yes — that's exactly what Chart.js's own chart.toBase64Image() does, and it's the better tool for that job. Use snapDOM when you want the DOM around the canvas included too.

How do I get a higher-resolution image?

Pass { scale: 2 } (or higher) to snapdom.toPng. snapDOM also respects window.devicePixelRatio automatically.

Does this work with react-chartjs-2?

Yes. Put a ref on the wrapping element and call snapdom.toPng(ref.current) from the chart's onComplete option. The wrapper is just a DOM node — snapDOM doesn't care how it was rendered.

Try the live demo

snapDOM captures a live <canvas> in the browser — see it running before you install.

Open the canvas demo Star on GitHub