A report page looks perfect in the browser, then the PDF shows bars stuck near zero, a chart cut off at the right edge, or fuzzy text. We rendered the same Chart.js page several ways and measured what came out. Below are the causes and a tested script that avoids all three problems.
| What you see | Cause and fix |
|---|---|
| Bars near zero, lines half-drawn | The PDF is printed while Chart.js is still animating (with defaults our bars reached about 5% of their height). Fix: animation: false |
| Chart cut off at the right edge | The canvas is sized for the 1280px screen viewport (it came out 1264px wide), then printed on an A4 page about 718px wide. Chart.js doesn't resize in time. Fix: give the chart box a fixed size in mm that fits the page |
| Blurry labels and lines | A canvas is a bitmap: at the default pixel ratio it lands in the PDF at 96 ppi. Fix: devicePixelRatio: 2 (192 ppi in our test) |
The settings below go in the Chart.js config and the page CSS, so they work the same in our API, in Puppeteer's page.pdf() or in a browser's Print dialog.
new Chart(canvas, {
type: 'bar',
data: { labels, datasets },
options: {
animation: false, // draw the final state immediately
devicePixelRatio: 2, // sharp when printed
maintainAspectRatio: false // fill the fixed-size box below
}
});
.chart { width: 180mm; height: 85mm; break-inside: avoid; }
180mm fits A4 with the default 10mm margins (190mm usable), and Letter is wider. break-inside: avoid keeps a chart and its caption on the same page.
This builds the HTML in code, sends it to POST /v1/pdf and saves report.pdf. Node 18+, no dependencies. Save it as report.mjs and run SHOTMINT_KEY=... node report.mjs.
// report.mjs: monthly sales report with two charts, rendered to report.pdf
import { writeFile } from 'node:fs/promises';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const revenue = [12400, 19100, 7300, 15800, 22050, 30120];
const signups = [210, 340, 180, 290, 410, 560];
// Every chart gets these two options: no animation, 2x canvas resolution.
const PRINT = { animation: false, devicePixelRatio: 2 };
const chart = (id, config) => `
<figure class="chart"><canvas id="${id}"></canvas></figure>
<script>new Chart(document.getElementById('${id}'), ${JSON.stringify(config)});</script>`;
const html = `<!doctype html><html><head><meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
body { font-family: Arial, sans-serif; color: #111; }
/* Fixed size in mm: the canvas is sized before print layout, so it must already fit the page. */
.chart { width: 180mm; height: 85mm; margin: 0 0 8mm; break-inside: avoid; }
</style></head><body>
<h1>Sales report, H1 2026</h1>
<p>Revenue: $${revenue.reduce((a, b) => a + b, 0).toLocaleString('en-US')}</p>
${chart('revenue', { type: 'bar', data: { labels: months, datasets: [{ label: 'Revenue ($)', data: revenue, backgroundColor: '#4f46e5' }] }, options: { ...PRINT, maintainAspectRatio: false } })}
${chart('signups', { type: 'line', data: { labels: months, datasets: [{ label: 'Signups', data: signups, borderColor: '#059669' }] }, options: { ...PRINT, maintainAspectRatio: false } })}
</body></html>`;
const res = await fetch('https://shotmint.creasework.com/v1/pdf', {
method: 'POST',
headers: { 'X-API-Key': process.env.SHOTMINT_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ html, format: 'A4' }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile('report.pdf', Buffer.from(await res.arrayBuffer()));
console.log('saved report.pdf, credits left:', res.headers.get('x-credits-remaining'));
# report.py: same report from Python, standard library only
import json, os, urllib.request
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [12400, 19100, 7300, 15800, 22050, 30120]
config = {
"type": "bar",
"data": {"labels": months, "datasets": [{"label": "Revenue ($)", "data": revenue, "backgroundColor": "#4f46e5"}]},
"options": {"animation": False, "devicePixelRatio": 2, "maintainAspectRatio": False},
}
html = f"""<!doctype html><html><head><meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>.chart {{ width: 180mm; height: 85mm; break-inside: avoid; }}</style></head><body>
<h1>Revenue, H1 2026</h1>
<figure class="chart"><canvas id="revenue"></canvas></figure>
<script>new Chart(document.getElementById("revenue"), {json.dumps(config)});</script>
</body></html>"""
req = urllib.request.Request(
"https://shotmint.creasework.com/v1/pdf",
data=json.dumps({"html": html, "format": "A4"}).encode(),
headers={"X-API-Key": os.environ["SHOTMINT_KEY"], "Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as res, open("report.pdf", "wb") as f:
f.write(res.read())
print("saved report.pdf, credits left:", res.headers["X-Credits-Remaining"])
If the HTML comes from somewhere you don't control, let the animation finish before printing. delay waits after the page has loaded (up to 10000 ms); Chart.js's default animation lasts 1 second, so 1500 is enough. This sends an existing dashboard.html with jq:
jq -n --rawfile html dashboard.html '{html: $html, format: "A4", delay: 1500}' |
curl -s -X POST https://shotmint.creasework.com/v1/pdf \
-H "X-API-Key: $SHOTMINT_KEY" -H "Content-Type: application/json" \
--data-binary @- -o dashboard.pdf
A delay fixes half-drawn charts only. It does nothing for clipping or blur, and it adds time to every render.
To print a page that is already online, send url instead of html, with the same delay if its charts animate. If a login is needed, render a server-side HTML version of the report instead: the request carries no cookies.
The same three causes apply to any library that draws on a canvas: turn off its animation, raise its pixel ratio if it has one, and give the chart a fixed print-sized box. SVG charts (for example D3 output) stay sharp at any zoom, but still need animation off or a delay.
We ran the Node, Python and jq scripts above against the live API (Chart.js 4.4.4 from jsDelivr, A4): every bar reached its full height, the charts fit the page, and the canvases were embedded at 1360×642 pixels (192 ppi), checked with pdfimages -list. With Chart.js defaults, the same page gave near-empty bars; with animation off but no fixed size, the right-hand month was cut off. One PDF costs one credit.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).
All guides: guides · integrations · comparisons