Put a chart in a PDF generated from HTML
The question behind this one is usually "does JavaScript run at all?" — because if it does not, every charting library is out and you are generating images on the server instead.
It runs, and it paints before the page is captured.
Verified
A <canvas> filled by an inline script, rendered through Renderpaper on
2026-08-08:
<!doctype html>
<html>
<head><style>@page { size: A4; margin: 15mm; } body { font: 12px sans-serif; }</style></head>
<body>
<h1>Chart</h1>
<canvas id="c" width="400" height="150"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
const data = [12, 29, 17, 44, 8, 31];
ctx.fillStyle = '#2b6cb0';
data.forEach((v, i) => ctx.fillRect(i * 64 + 10, 150 - v * 3, 44, v * 3));
</script>
</body>
</html>
The bars are in the PDF. The script executed, the canvas painted, and the capture happened afterwards.
Getting data into the chart
The script sees the rendered document, so merge the data into the markup and read it
from there. A data- attribute keeps it out of the script body, which matters
because the template is escaped as HTML and inline script content is not the place
for user data:
<canvas id="revenue" width="520" height="200"
data-series="{{.MonthlyRevenue}}"></canvas>
<script>
const el = document.getElementById('revenue');
const series = JSON.parse(el.dataset.series);
// draw from `series`
</script>
Send MonthlyRevenue as a JSON string in your data payload.
Which library fits
The template has a 512 KiB ceiling, and a self-contained document has to carry the library inside it. That decides the choice more than features do:
| Library | Minified size | Fits |
|---|---|---|
| uPlot | ~50 KB | comfortably |
| Chart.js | ~200 KB | yes, with room for the document |
| ECharts | ~1 MB | no |
Inline the library in a <script> tag in the template. Loading it from a CDN is not
an option — the renderer fetches nothing external, which is deliberate and is why a
document cannot be made to call out to an arbitrary URL.
Turn animation off
Charting libraries animate by default, which is meaningless in a PDF and is a race: the capture may happen mid-animation, giving you bars at 60% height.
new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
animation: false, // Chart.js
responsive: false, // fixed canvas size, no resize observer
},
});
For uPlot there is no animation to disable, which is one reason it suits this job.
Prefer plain CSS where you can
A bar chart, a progress bar or a sparkline made of <div>s needs no library, no
size budget and no timing assumptions:
<div class="bar" style="width: {{.PercentComplete}}%"></div>
.bar { height: 10px; background: #2b6cb0; border-radius: 5px; }
Reach for a canvas library when you genuinely need axes, scales and legends. For a single series in a table cell, CSS is smaller, faster and cannot race.