Server-side Chart.js to PNG looks simple until you actually try it. The canonical path runs through node-canvas, which needs Cairo, Pango, libpng, jpeg, giflib, and librsvg — native dependencies that break on macOS and exceed serverless function size limits [1][2]. Chart.js issue #8831 proposed node-canvas as the fix, but that path still leaves you fighting install errors, blurry output, and plugins that don't render [3][4][5][6]. The easier route: write the chart as HTML, send it to a browser engine via API, and get back a pixel-perfect PNG buffer.
Chart.js is built on the HTML5 canvas element, so using it in Node.js requires mimicking that element [4]. The standard wrapper, chartjs-node-canvas, inherits node-canvas's native dependency stack [7]. Users hit "Error: Cannot find module 'canvas'" repeatedly, and the package's own readme states it is limited by the upstream dependency canvas [8]. Installing those native libraries on macOS requires Homebrew packages like cairo and pango [2].
chartjs-node-canvas also disables Chart.js animation and responsive resize because Node lacks the browser APIs that drive them [9]. Gradients render differently than in a real browser, and some plugins don't work at all [6]. A Stack Overflow user reported that images generated by the plugin were too blurred [4], and a Reddit user hit "'candlestick' is not a registered controller" when using chartjs-node-canvas with financial chart types [5].
On Vercel, a function using chartjs-node-canvas exceeded the 50MB limit at 50.55MB, with the canvas dependency alone taking 42.7MB [1]. AWS Lambda requires workarounds like building on an Amazon Linux 2 EC2 instance and checking compiled node_modules into git, which defeats the purpose of serverless [10][11].
Install the client and initialize it with your API key [12]:
npm install markupgo-node
import MarkupGo from "markupgo-node";
const markupgo = new MarkupGo({
API_KEY: process.env.MARKUPGO_API_KEY,
});
The template needs an HTML canvas element, Chart.js loaded from a CDN in the libraries field, and your chart configuration passed via context [13]. Here's the structure:
const templateData = {
id: "YOUR_TEMPLATE_ID", // create this in the MarkupGo dashboard
context: {
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
values: [12, 19, 8, 15, 22],
title: "Monthly Active Users"
},
autoHeight: true
};
<div style="width: 800px; padding: 40px; background: white;">
<canvas id="chart"></canvas>
</div>
<script>
const ctx = document.getElementById('chart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: {{ labels }},
datasets: [{
label: {{ title }},
data: {{ values }},
backgroundColor: 'rgba(99, 102, 241, 0.6)'
}]
},
options: {
responsive: false,
animation: false,
plugins: {
legend: { display: true }
}
}
});
</script>
The libraries field in your template includes https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js [13]. Set autoHeight: true so the output size fits your content rather than clipping to a fixed height [13].
Call image.fromTemplate with your template ID, context, and format set to png [12].
import * as fs from "fs";
// Get back a buffer and write to file
const buffer = await markupgo.image.fromTemplate(templateData, {
properties: {
format: "png",
width: 880,
height: 600
}
}).buffer();
fs.writeFileSync("chart.png", Buffer.from(buffer));
Supported output formats are PNG, JPEG, and WebP [12]. The same template drives PDF generation too — swap image.fromTemplate for pdf.fromTemplate to embed the chart in a full report [12]:
const pdfBuffer = await markupgo.pdf.fromTemplate(templateData, {
properties: {
printBackground: true,
landscape: true
}
}).buffer();
fs.writeFileSync("report.pdf", Buffer.from(pdfBuffer));
For no-code use cases, enable the Magic Template URL on your template and generate images from query parameters without writing any code [14]. The URL structure is https://render.markupgo.com/template/{templateId}.png?labels=Jan,Feb,Mar&values=12,19,8&title=MAU. Responses cache for 15 days, so reusing the same URL doesn't consume additional credits [14].
MarkupGo runs a perpetual free trial of 100 credits per month with no credit card required [15]. Paid plans start at Lite for $29 per month with 2,000 credits and 5 templates, Plus at $49 per month with 10,000 credits and 30 templates, and Pro at $99 per month with 20,000 credits and unlimited templates [15].
Sign up for the free tier, create a template with Chart.js in the libraries field, and call image.fromTemplate from your Node service. You'll skip the native dependency headaches entirely and get browser-accurate renders every time.