In Module 05 we built charts using Chart.js defaults — the library chose the colors, font sizes, tooltip formatting, and legend placement. That is fine for quick prototypes, but production dashboards demand precise visual control. This module teaches you how Chart.js’s options system works and how to use it to build polished, accessible charts.
Run: Open any HTML file directly in a browser — all demos use Chart.js from CDN.
Chart.js resolves options through a three-level hierarchy. Lower levels override higher ones:
| Level | Scope | Where You Set It |
|---|---|---|
| 1. Global Defaults | Every chart on the page | Chart.defaults |
| 2. Chart-Level Options | One specific chart instance | options in the config object |
| 3. Dataset-Level Options | One specific dataset within a chart | Properties inside the dataset object |
This means you can set sensible defaults once and override them where needed:
// Level 1: Global defaults — affect every chart on the page
Chart.defaults.font.family = "'Segoe UI', Roboto, sans-serif";
Chart.defaults.font.size = 14;
Chart.defaults.color = '#555';
Chart.defaults.plugins.legend.position = 'bottom';
// Level 2: Chart-level options — affect only this chart
const myChart = new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: {
plugins: {
legend: { position: 'top' } // overrides global 'bottom'
}
}
});
// Level 3: Dataset-level — affect only this dataset
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'Revenue',
data: [1200, 1900, 3000],
backgroundColor: '#16a085', // only this dataset
borderWidth: 2 // only this dataset
}]
}
Colors are the most common customization. Each dataset accepts several color-related properties:
datasets: [{
label: 'Pageviews',
data: [1200, 1900, 3000, 2500, 2200],
// Fill color (bars, area under line, pie slices)
backgroundColor: 'rgba(22, 160, 133, 0.7)',
// Border/outline color
borderColor: 'rgba(22, 160, 133, 1)',
// Border width in pixels
borderWidth: 2,
// Hover state colors
hoverBackgroundColor: 'rgba(22, 160, 133, 0.9)',
hoverBorderColor: '#0e7c6b',
hoverBorderWidth: 3
}]
For bar and pie charts, you can pass an array of colors to color each bar or slice individually:
backgroundColor: [
'#16a085', // bar 1
'#2980b9', // bar 2
'#8e44ad', // bar 3
'#e67e22', // bar 4
'#e74c3c' // bar 5
]
For line charts, backgroundColor fills the area under the line (when fill: true), and borderColor sets the line color. You can also use a canvas gradient for the fill:
// Create a gradient fill for a line chart
const ctx = document.getElementById('myChart').getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(22, 160, 133, 0.4)');
gradient.addColorStop(1, 'rgba(22, 160, 133, 0.0)');
datasets: [{
label: 'Traffic',
data: [120, 190, 300, 250, 220],
fill: true,
backgroundColor: gradient,
borderColor: '#16a085',
tension: 0.3 // slight curve on the line
}]
Scales control the axes — their titles, tick marks, ranges, and formatting. In Chart.js v4, scales live under options.scales with keys like x and y:
options: {
scales: {
x: {
title: {
display: true,
text: 'Month',
font: { size: 14, weight: 'bold' }
},
grid: {
display: false // hide vertical grid lines
}
},
y: {
title: {
display: true,
text: 'Pageviews'
},
beginAtZero: true,
suggestedMax: 5000, // hint for upper bound (data can exceed it)
ticks: {
// Format tick labels with a callback
callback: function(value) {
if (value >= 1000) {
return (value / 1000).toFixed(1) + 'k';
}
return value;
},
stepSize: 1000
},
grid: {
color: 'rgba(0, 0, 0, 0.05)' // subtle horizontal grid
}
}
}
}
| Property | What It Does |
|---|---|
beginAtZero |
Forces the axis to start at 0 (critical for honest bar charts) |
suggestedMax |
Suggests a maximum value; the axis will expand beyond it if data requires |
max |
Hard maximum — data beyond this value is clipped (use sparingly) |
ticks.callback |
Function to format tick labels (currency, percentages, abbreviations) |
ticks.stepSize |
Forces a specific interval between ticks |
grid.display |
Show or hide grid lines for this axis |
title.display |
Show or hide the axis title |
stacked |
Stack datasets on this axis (set on both x and y for stacked bars) |
beginAtZero: true on a bar chart, Chart.js may start the y-axis at a value close to your minimum data point. This exaggerates differences between bars and misleads readers. Always set beginAtZero: true for bar charts. Line charts showing trends may legitimately start above zero, but be deliberate about the choice.
Chart.js “plugins” are built-in features like the legend, title, and tooltip. They are configured under options.plugins:
options: {
plugins: {
legend: {
position: 'bottom', // 'top', 'bottom', 'left', 'right'
align: 'start', // 'start', 'center', 'end'
labels: {
usePointStyle: true, // circles instead of rectangles
padding: 20,
font: { size: 13 }
}
}
}
}
For single-dataset charts (one bar series, one line), you typically hide the legend entirely since it adds no information:
plugins: {
legend: { display: false }
}
plugins: {
title: {
display: true,
text: 'Monthly Pageviews',
font: { size: 18, weight: 'bold' },
padding: { bottom: 20 }
}
}
Tooltips are where callback functions become essential. The default tooltip shows the dataset label and raw value, but production charts need formatting — currency symbols, percentages, custom titles:
plugins: {
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleFont: { size: 14 },
bodyFont: { size: 13 },
padding: 12,
cornerRadius: 6,
callbacks: {
// Custom title (shown at top of tooltip)
title: function(tooltipItems) {
return 'Week of ' + tooltipItems[0].label;
},
// Custom body label (main value line)
label: function(tooltipItem) {
const value = tooltipItem.raw;
return ' Revenue: $' + value.toLocaleString();
},
// Footer (shown below the value)
footer: function(tooltipItems) {
const total = tooltipItems[0].dataset.data.reduce((a, b) => a + b, 0);
const pct = ((tooltipItems[0].raw / total) * 100).toFixed(1);
return pct + '% of total';
}
}
}
}
See tooltip-callbacks.html for a complete working example.
Chart.js is responsive by default — responsive: true is the default setting. The chart canvas resizes to fit its container. But there are nuances:
options: {
responsive: true,
maintainAspectRatio: false // let the container control the height
}
When maintainAspectRatio is true (the default), Chart.js preserves a 2:1 width-to-height ratio. This is fine for standalone charts, but in a dashboard grid you typically want the chart to fill its container completely. Set maintainAspectRatio: false and control the height through CSS on the container:
<div style="position: relative; height: 300px;">
<canvas id="myChart"></canvas>
</div>
<canvas> element must have a defined height when maintainAspectRatio is false. If the parent has no explicit height, the chart may collapse to zero height or behave unpredictably. Use a fixed height, a percentage height with an ancestor that has a fixed height, or CSS Grid/Flexbox with explicit row sizing.
For dashboard layouts, CSS Grid with fixed row heights works well:
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.chart-card {
background: white;
border-radius: 8px;
padding: 20px;
height: 350px; /* fixed height for chart cards */
position: relative; /* required for Chart.js sizing */
}
See styled-dashboard.html for a full 2x2 dashboard layout.
<canvas> element renders pixels, not DOM elements. Screen readers cannot read bar heights, line trends, or pie slice percentages from a canvas. Always pair every chart with a data table — this is an accessibility requirement, not optional polish.
A minimal accessible pattern:
<figure role="img" aria-label="Bar chart showing monthly pageviews from January to May">
<canvas id="pageviewChart"></canvas>
</figure>
<!-- Accessible data table (can be visually hidden if needed) -->
<table>
<caption>Monthly Pageviews</caption>
<thead>
<tr><th>Month</th><th>Pageviews</th></tr>
</thead>
<tbody>
<tr><td>January</td><td>1,200</td></tr>
<tr><td>February</td><td>1,900</td></tr>
<tr><td>March</td><td>3,000</td></tr>
<tr><td>April</td><td>2,500</td></tr>
<tr><td>May</td><td>2,200</td></tr>
</tbody>
</table>
Additional accessibility practices:
<h3> above each chart so users know what they are looking at without hovering.Here is a complete chart configuration that uses every technique from this module:
// Global defaults (set once for the whole page)
Chart.defaults.font.family = "'Segoe UI', Roboto, sans-serif";
Chart.defaults.font.size = 13;
Chart.defaults.color = '#555';
const ctx = document.getElementById('revenueChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [{
label: 'Revenue ($)',
data: [12400, 19800, 30200, 25100, 22700],
backgroundColor: 'rgba(22, 160, 133, 0.75)',
borderColor: '#16a085',
borderWidth: 1,
borderRadius: 4, // rounded bar corners
hoverBackgroundColor: 'rgba(22, 160, 133, 0.95)'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: {
display: true,
text: 'Monthly Revenue',
font: { size: 18, weight: 'bold' },
color: '#333'
},
tooltip: {
callbacks: {
label: function(ctx) {
return ' $' + ctx.raw.toLocaleString();
}
}
}
},
scales: {
x: {
grid: { display: false }
},
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + (value / 1000).toFixed(0) + 'k';
}
},
grid: { color: 'rgba(0,0,0,0.05)' }
}
}
}
});