Modules 05–07 covered the fundamentals of Chart.js: configuration, styling, live data. This module pushes further into the features you will need for a real analytics dashboard — mixed-type charts that combine bars and lines on the same canvas, stacked bars for composition, annotation plugins for threshold bands, data labels, and PNG export so stakeholders can paste charts into slide decks and emails.
Run: Open the HTML files directly in a browser. All use CDN-hosted Chart.js — no npm install needed.
A dashboard often needs to show two related but differently-scaled metrics together. The classic example: pageviews (a count, shown as bars) and bounce rate (a percentage, shown as a line). Putting them on the same chart with dual y-axes lets the viewer correlate the two series without flipping between charts.
Chart.js supports this natively. Each dataset can declare its own type, and you assign datasets to different y-axes by referencing yAxisID:
const chart = new Chart(ctx, {
type: 'bar', // default type for datasets that don't specify one
data: {
labels: ['Jan 1', 'Jan 2', 'Jan 3', /* ... */],
datasets: [
{
label: 'Pageviews',
type: 'bar',
data: [1250, 1340, 1180, /* ... */],
backgroundColor: 'rgba(52, 152, 219, 0.7)',
yAxisID: 'y' // left axis
},
{
label: 'Bounce Rate',
type: 'line',
data: [42.3, 38.7, 45.1, /* ... */],
borderColor: '#e74c3c',
yAxisID: 'y1' // right axis
}
]
},
options: {
scales: {
y: {
position: 'left',
title: { display: true, text: 'Pageviews' },
beginAtZero: true
},
y1: {
position: 'right',
title: { display: true, text: 'Bounce Rate (%)' },
min: 0,
max: 100,
grid: { drawOnChartArea: false } // don't overlap grids
}
}
}
});
Key configuration details:
yAxisID: 'y' and yAxisID: 'y1' assign datasets to axes. The axis IDs must match the keys in options.scales.grid: { drawOnChartArea: false } on the right axis prevents a confusing double grid.type on each dataset overrides the chart-level type. You can mix bar, line, scatter, and bubble types on a single chart.See mixed-chart.html for a complete working example with 14 days of data.
A stacked bar chart shows how a total breaks down into categories. For analytics, the common use case is traffic by browser, traffic by device type, or traffic by referral source over time. Each bar represents a day, and the segments show how much each browser contributed.
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [
{
label: 'Chrome',
data: [420, 435, 410, 450, 440, 320, 280],
backgroundColor: '#4285F4'
},
{
label: 'Safari',
data: [180, 175, 190, 185, 170, 210, 230],
backgroundColor: '#34A853'
},
{
label: 'Firefox',
data: [95, 100, 88, 92, 105, 75, 65],
backgroundColor: '#FF7139'
},
{
label: 'Edge',
data: [60, 55, 65, 58, 62, 45, 40],
backgroundColor: '#0078D4'
},
{
label: 'Other',
data: [25, 30, 22, 28, 18, 20, 15],
backgroundColor: '#999'
}
]
},
options: {
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true }
}
}
});
Setting stacked: true on both the x and y scales tells Chart.js to stack the bars rather than placing them side by side. The segments are drawn from bottom to top in dataset order.
See stacked-bar.html for the full demo with custom colors.
Production dashboards almost always need reference lines — fixed horizontal lines or bands that mark thresholds. The classic example in web performance is Core Web Vitals (CWV): Google defines three zones for metrics like Largest Contentful Paint (LCP):
| Zone | LCP Range | Color |
|---|---|---|
| Good | 0–2500ms | Green |
| Needs Improvement | 2500–4000ms | Yellow |
| Poor | 4000ms+ | Red |
Chart.js does not include annotation support in its core. You need the chartjs-plugin-annotation plugin:
<!-- Load Chart.js first, then the annotation plugin -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
Once loaded, annotations are configured in options.plugins.annotation:
options: {
plugins: {
annotation: {
annotations: {
goodZone: {
type: 'box',
yMin: 0,
yMax: 2500,
backgroundColor: 'rgba(76, 175, 80, 0.1)',
borderWidth: 0,
label: {
display: true,
content: 'Good',
position: 'start',
color: '#4CAF50',
font: { weight: 'bold' }
}
},
needsImprovementZone: {
type: 'box',
yMin: 2500,
yMax: 4000,
backgroundColor: 'rgba(255, 193, 7, 0.1)',
borderWidth: 0,
label: {
display: true,
content: 'Needs Improvement',
position: 'start',
color: '#FF9800',
font: { weight: 'bold' }
}
},
poorZone: {
type: 'box',
yMin: 4000,
yMax: 6000,
backgroundColor: 'rgba(244, 67, 54, 0.1)',
borderWidth: 0,
label: {
display: true,
content: 'Poor',
position: 'start',
color: '#F44336',
font: { weight: 'bold' }
}
}
}
}
}
}
The annotation plugin supports several annotation types:
| Type | Use Case |
|---|---|
line |
Horizontal or vertical reference line (e.g., "target: 2.5s") |
box |
Shaded rectangular region (e.g., CWV threshold bands) |
point |
Annotated data point with label |
label |
Free-floating text label at arbitrary coordinates |
ellipse |
Elliptical annotation region |
See annotations.html for a complete demo showing LCP values with color-coded threshold bands.
By default, Chart.js shows values only in tooltips (on hover). For printed reports or static screenshots, you often want the values displayed directly on the bars or points. The chartjs-plugin-datalabels plugin handles this:
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
// Register the plugin globally
Chart.register(ChartDataLabels);
const chart = new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: {
plugins: {
datalabels: {
anchor: 'end', // position at the end of the bar
align: 'top', // place label above the anchor
color: '#333',
font: { weight: 'bold', size: 12 },
formatter: (value) => value.toLocaleString()
}
}
}
});
Key options for datalabels:
anchor: where on the bar the label is anchored ('start', 'center', 'end')align: direction from the anchor to place the text ('top', 'bottom', 'left', 'right')formatter: function to format the displayed value (e.g., add commas, suffixes, or percentage signs)display: can be a function that returns true/false per data point, so you can hide labels for very small values that would overlapChart.js animates charts by default — bars grow upward, lines draw from left to right. You can customize the animation timing and behavior:
options: {
animation: {
duration: 1500, // ms for the initial animation
easing: 'easeInOutQuart', // easing function
delay: (context) => {
// Stagger bars: each bar starts 100ms after the previous
return context.dataIndex * 100;
}
},
transitions: {
active: {
animation: { duration: 200 } // faster animation on hover
}
}
}
Available easing functions include 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuart', 'easeOutBounce', and many more. The delay callback receives a context object with dataIndex and datasetIndex, allowing staggered animations where each bar or point animates in sequence.
animation: false) or keeping durations short. The initial load animation is a nice touch; continuous re-render animations can be distracting and wasteful.
Chart.js renders to a <canvas> element, which means you can export the chart as a PNG image using the native Canvas API. This is one of the most-requested features for analytics dashboards — stakeholders want to paste charts into emails, slide decks, and Confluence pages.
function downloadChart(canvas, filename) {
// Get the image data as a base64-encoded PNG
const imageURL = canvas.toDataURL('image/png');
// Create a temporary download link
const link = document.createElement('a');
link.download = filename || 'chart.png';
link.href = imageURL;
// Trigger the download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Wire it up to a button
document.getElementById('downloadBtn').addEventListener('click', () => {
const canvas = document.getElementById('myChart');
downloadChart(canvas, 'analytics-chart.png');
});
The process is simple:
canvas.toDataURL('image/png') serializes the canvas pixels to a base64 PNG string<a> element with a download attributehref to the data URLModern browsers also support copying canvas content directly to the clipboard, which is even more convenient for pasting into documents:
async function copyChartToClipboard(canvas) {
try {
// Convert canvas to a Blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
// Write to clipboard
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
alert('Chart copied to clipboard!');
} catch (err) {
console.error('Clipboard write failed:', err);
alert('Copy failed. Try the download button instead.');
}
}
navigator.clipboard.write() method requires a secure context (HTTPS) and user activation (must be triggered by a click handler). It also requires clipboard-write permission, which most browsers grant automatically for user-initiated actions. Safari has stricter requirements and may not support ClipboardItem with image blobs in all versions.
By default, toDataURL() captures the canvas as-is. If your chart has a transparent background (the default for Chart.js), the exported PNG will have a transparent background, which looks odd when pasted on a white slide. You can use the Chart.js beforeDraw plugin to fill the background:
const bgPlugin = {
id: 'customCanvasBackgroundColor',
beforeDraw: (chart) => {
const ctx = chart.canvas.getContext('2d');
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, chart.canvas.width, chart.canvas.height);
ctx.restore();
}
};
// Register when creating the chart
new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: { /* ... */ },
plugins: [bgPlugin]
});
See export-png.html for a complete working example with both download and clipboard copy buttons.
A typical analytics dashboard panel might combine several of these features:
The pattern is: a mixed chart (bar + line) on top for the headline metrics, stacked bars below for composition breakdown, annotation bands if you have thresholds (like CWV), and an export button in the corner. This is the standard layout for analytics tools like Google Analytics, Mixpanel, and Plausible.
Chart.js is excellent for standard chart types with standard interactions. It covers bar, line, area, pie, doughnut, radar, scatter, bubble, and polar area charts. With plugins, it handles annotations, data labels, zoom, and streaming data.
But sometimes you need a visualization that does not fit any standard chart type:
These visualizations require imperative control over every visual element — you need to decide exactly what SVG elements to create, where to position them, and how they respond to interaction. That is what D3.js provides.
yAxisIDstacked: true on both x and y scalescanvas.toDataURL('image/png') with a programmatic download linkcanvas.toBlob() and navigator.clipboard.write()