Getting Started
Setup & First Chart
Chart.js renders on a canvas element. The type property sets the chart type. data contains labels and datasets with values and styling.
<!-- include Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="myChart" width="400" height="400"></canvas>
<script>
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Green'],
datasets: [{
label: 'Votes',
data: [12, 19, 3],
backgroundColor: ['red', 'blue', 'green']
}]
}
});
</script>npm Install & ES Module Import
Use 'chart.js/auto' for the simplest setup — it imports Chart.js plus all controllers/elements and registers them automatically. For smaller production bundles prefer tree-shaking: import from 'chart.js' and register only the pieces you use.
# install
npm install chart.js
// import the auto-bundled build (registers everything)
import Chart from 'chart.js/auto';
const ctx = document.getElementById('myChart');
const chart = new Chart(ctx, {
type: 'line',
data: { /* ... */ },
options: { responsive: true }
});Canvas & 2D Context
Chart.js can take either the <canvas> element or its 2d rendering context. When responsive is true the canvas is resized to fill its container, so the width/height HTML attributes are only fallbacks. Each canvas can hold exactly one Chart instance.
// Chart.js accepts a canvas element OR a 2D context
const canvas = document.getElementById('myChart');
const chart = new Chart(canvas, { type: 'bar', data: {...} });
// equivalent: pass the 2d context
const ctx = canvas.getContext('2d');
const chart2 = new Chart(ctx, { type: 'bar', data: {...} });
// one chart per canvas; the canvas size is controlled by
// responsive options, NOT the width/height attributesAvailable Chart Types
Chart.js ships eight chart types. Bar, line, scatter and bubble use Cartesian axes; pie, doughnut and polarArea are circular; radar has its own radial axis. The 'type' field at the top level selects the controller used for every dataset unless a dataset overrides it with its own type.
// the 8 built-in chart types
const types = ['bar', 'line', 'pie', 'doughnut',
'radar', 'polarArea', 'bubble', 'scatter'];
// each type maps to a controller:
// bar/line -> Cartesian controllers
// pie/doughnut/polarArea -> circular controllers
// radar -> radar controller
// bubble/scatter -> cartesian with point parsing
new Chart(ctx, { type: 'polarArea', data: {...} });Tree-Shaking & Register
When importing from 'chart.js' (not /auto) nothing is registered, so you must register controllers, elements, scales and plugins yourself. This tree-shakeable approach can cut bundle size significantly. Forgetting to register a component is the most common 'scale is not a registered scale' error.
import {
Chart,
BarController,
BarElement,
CategoryScale,
LinearScale,
Legend,
Title,
Tooltip,
} from 'chart.js';
// register ONLY what you use -> smaller bundle
Chart.register(
BarController, BarElement,
CategoryScale, LinearScale,
Legend, Title, Tooltip,
);
new Chart(ctx, {
type: 'bar',
data: { labels: ['A', 'B'], datasets: [{ data: [1, 2] }] },
});Destroy & Lifecycle
Each Chart instance owns its canvas and attached event listeners. Calling destroy() tears everything down so the canvas can be reused — essential in SPAs and React/Vue effects. Creating a second chart on an active canvas leaks memory and produces double-rendered graphics.
const chart = new Chart(ctx, config);
// ...later, to re-render with new config:
chart.destroy(); // cleans up listeners and canvas
const fresh = new Chart(ctx, newConfig);
// never create two Chart instances on the same canvas;
// always destroy() the old one first.Bar Charts
Basic Bar Chart
A bar chart maps each label to a bar whose height equals the data value. backgroundColor sets bar fill, borderColor/borderWidth the outline. Set y.beginAtZero so bars always start from zero — without it Chart.js may autoscale the axis minimum and visually distort the differences.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [{
label: 'Revenue',
data: [12, 19, 7, 15, 22],
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: { scales: { y: { beginAtZero: true } } }
});Horizontal Bar (indexAxis)
In Chart.js v3+ horizontal bars are created with indexAxis: 'y' on a 'bar' type — the old 'horizontalBar' type was removed. The x scale becomes the value axis. Horizontal bars are ideal for long category labels that would overlap on a vertical chart.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Task A', 'Task B', 'Task C'],
datasets: [{ label: 'Hours', data: [5, 8, 3] }]
},
options: {
indexAxis: 'y', // horizontal bars (v3+ syntax)
scales: { x: { beginAtZero: true } }
}
});Grouped Bars (Multiple Datasets)
Multiple datasets in a bar chart are placed side by side (grouped) by default. Chart.js auto-assigns colors only if you omit backgroundColor, so set explicit colors per dataset for predictable output. Each dataset shares the same labels array on the x axis.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{ label: '2023', data: [20, 35, 30, 35],
backgroundColor: 'rgba(255,99,132,0.6)' },
{ label: '2024', data: [25, 32, 34, 40],
backgroundColor: 'rgba(54,162,235,0.6)' }
]
},
options: { scales: { y: { beginAtZero: true } } }
});Stacked Bars
Stacked bars require stacked: true on BOTH the x and y scales. Each dataset's bars are drawn on top of the previous one, summing to a total. Stacking shows composition, but makes the upper series harder to compare across categories than grouped bars.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['A', 'B', 'C'],
datasets: [
{ label: 'Men', data: [20, 35, 30], backgroundColor: '#36A2EB' },
{ label: 'Women', data: [25, 32, 34], backgroundColor: '#FF6384' }
]
},
options: {
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true }
}
}
});Bar Styling
borderRadius rounds bar corners (a single number applies to all corners; an object like {topLeft:8} targets specific corners). borderSkipped defaults to 'start' which omits one border — set false to draw all four. barPercentage and categoryPercentage together control bar thickness and spacing.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['A', 'B', 'C'],
datasets: [{
data: [10, 20, 15],
backgroundColor: '#FF6384',
borderColor: '#fff',
borderWidth: 2,
borderRadius: 8, // rounded corners (px or %)
borderSkipped: false, // draw border on all sides
barPercentage: 0.8, // bar width within category
categoryPercentage: 0.7 // category width
}]
}
});Floating Bars ([min, max])
Passing [min, max] pairs as data values produces floating bars that don't start at zero — perfect for ranges like daily temperature low-high, open-close, or confidence intervals. The y axis should NOT begin at zero for range bars. This also works on horizontal bars.
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Mon', 'Tue', 'Wed'],
datasets: [{
label: 'Low-High temp',
// each value is [min, max] -> a floating bar
data: [[10, 18], [12, 20], [8, 15]],
backgroundColor: 'rgba(75,192,192,0.6)'
}]
},
options: { scales: { y: { beginAtZero: false } } }
});Line Charts
Basic Line Chart
A line chart connects data points in order. borderColor is the line color; backgroundColor is used for the fill (if fill is enabled). tension (0-1) controls curve smoothness — 0 is straight segments, ~0.3-0.4 gives gentle bezier curves. Use 0 for precise data like financial prices.
new Chart(ctx, {
type: 'line',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [{
label: 'Visitors',
data: [30, 45, 28, 60, 52],
borderColor: 'rgba(75,192,192,1)',
backgroundColor: 'rgba(75,192,192,0.2)',
tension: 0.3
}]
},
options: { scales: { y: { beginAtZero: true } } }
});Multiple Lines
Each dataset in a line chart becomes its own line, auto-assigned a color from the default palette (overridable with borderColor). Lines are drawn in dataset order, so the last dataset renders on top. Pass a label to each dataset so the legend can identify them.
new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
datasets: [
{ label: 'Plan', data: [10, 20, 25, 30], borderColor: 'red' },
{ label: 'Actual', data: [8, 22, 24, 33], borderColor: 'blue' }
]
},
options: {
scales: { y: { beginAtZero: true } },
plugins: { legend: { position: 'top' } }
}
});Line Tension & Interpolation
tension adds bezier smoothing but can overshoot data points (creating dips below the minimum). cubicInterpolationMode: 'monotone' smooths without overshoot — preferred for monotonic data like stock prices. borderDash is a [dash, gap] pattern for dashed/dotted lines.
datasets: [{
data: [1, 5, 2, 6, 3],
tension: 0, // straight lines
// tension: 0.4, // smooth bezier
cubicInterpolationMode: 'monotone', // no overshoot
borderDash: [5, 5], // dashed line
borderWidth: 2,
borderColor: 'purple'
}]Filled Lines (fill)
fill controls the area under the line. true/'origin' fills down to the x axis. A number (1, -1, +1) fills toward another dataset by index, useful for band charts. A negative number fills toward the previous dataset. Set backgroundColor with alpha so the line stays visible through the fill.
datasets: [
{ label: 'A', data: [3,5,4,6], fill: true, backgroundColor: 'rgba(255,99,132,0.3)' },
{ label: 'B', data: [1,2,3,2], fill: 'origin', backgroundColor: 'rgba(54,162,235,0.3)' },
{ label: 'C', data: [2,4,3,5], fill: 1, backgroundColor: 'rgba(75,192,192,0.3)' }
]
// fill values: false | true/'origin' | 1/-1/+1 | '-1' | {target:...}Point Styling
Point properties accept a single value or an array (one per data point). pointStyle supports many shapes ('circle', 'rect', 'rectRot', 'triangle', 'star', 'cross', 'crossRot'). Set pointRadius: 0 to hide points entirely (useful for dense line charts), and pointHoverRadius larger for an interactive pop effect.
datasets: [{
data: [4, 6, 5, 7],
showLine: true,
pointRadius: 5, // point size (0 hides points)
pointHoverRadius: 9,
pointBackgroundColor: 'white',
pointBorderColor: 'black',
pointBorderWidth: 2,
pointStyle: 'rectRot', // 'circle','rect','triangle','star','cross'...
// per-point arrays also work:
// pointRadius: [0, 0, 6, 0],
}]Span Gaps & Missing Data
null, undefined and NaN represent missing data and break the line by default. spanGaps: true connects the points on either side of a gap with a single segment. Distinguish NaN (missing) from 0 (a real zero) — confusing them is a common source of misleading charts.
datasets: [{
data: [5, null, 7, NaN, 9, undefined, 11],
spanGaps: true, // connect across null/NaN gaps
// spanGaps: false -> the line breaks at missing points
}]
// also skip points entirely by setting them to NaN
// NaN is "no data"; 0 is a legitimate zero valuePie & Doughnut Charts
Basic Pie Chart
A pie chart shows parts of a whole: each value becomes a wedge whose angle is proportional to its share. Provide one backgroundColor per slice (an array), otherwise Chart.js cycles a default palette. Pie charts work best with 3-6 categories — more becomes unreadable.
new Chart(ctx, {
type: 'pie',
data: {
labels: ['Rent', 'Food', 'Fun', 'Save'],
datasets: [{
data: [1200, 600, 400, 800],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0']
}]
}
});Basic Doughnut Chart
A doughnut is a pie with a hole in the middle. The cutout property (percentage string or number of pixels) controls the hole size — '50%' is the classic donut look. The center hole can hold a summary label via a plugin, and ring length encodes value slightly better than wedge angle.
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['A', 'B', 'C'],
datasets: [{
data: [40, 35, 25],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'],
cutout: '50%' // size of the inner hole
}]
}
});Cutout, Rotation & Circumference
rotation sets the start angle in degrees (default 0 = right; -90 = top). circumference controls how much of the circle is drawn — 180 makes a half-doughnut, useful for gauge-style charts. Both live under options (not the dataset) in v3+, though cutout can appear in either.
new Chart(ctx, {
type: 'doughnut',
data: { labels: ['A','B','C'], datasets: [{ data: [30,40,30], backgroundColor: ['#f00','#0f0','#00f'] }] },
options: {
cutout: '60%',
rotation: -90, // start at top (degrees)
circumference: 360 // full circle; 180 = half doughnut
}
});Border & Offset
borderColor with borderWidth separates wedges visually (white borders give a clean segmented look). hoverOffset pushes a slice outward when the user hovers it, providing clear interactive feedback. offset is an array (one per slice) to permanently 'explode' specific wedges for emphasis.
datasets: [{
data: [30, 40, 30],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'],
borderColor: 'white',
borderWidth: 3,
hoverOffset: 16, // pop slice out on hover
offset: [0, 20, 0] // permanently offset 2nd slice
}]Half Doughnut (Gauge)
A half-doughnut (rotation: -90, circumference: 180) reads as a gauge/progress meter. Pair it with a center-text plugin to show the percentage, and disable the legend for a clean KPI look. The 'remaining' slice in a neutral gray conveys the uncompleted portion.
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Done', 'Remaining'],
datasets: [{
data: [72, 28],
backgroundColor: ['#4BC0C0', '#E0E0E0']
}]
},
options: {
rotation: -90,
circumference: 180,
plugins: { legend: { display: false } }
}
});Tooltip Percentage Callback
Pie/doughnut tooltips show raw values by default. Use the tooltip callbacks.label to compute and display percentages instead — ctx.parsed is the slice value and ctx.dataset.data holds all values for the sum. Template literals in callbacks must escape backticks and ${} when embedded in this cheatsheet's code string.
options: {
plugins: {
tooltip: {
callbacks: {
label: (ctx) => {
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
const pct = (ctx.parsed / total * 100).toFixed(1);
return `${ctx.label}: ${ctx.parsed} (${pct}%)`;
}
}
}
}
}Radar Charts
Basic Radar Chart
A radar chart plots each label as a vertex on a polygon, connecting them into a closed shape. fill: true shades the interior. The radial scale is configured under scales.r (not x/y). Radars compare multivariate profiles but can mislead with too many axes or non-comparable units.
new Chart(ctx, {
type: 'radar',
data: {
labels: ['Speed', 'Power', 'Range', 'Comfort', 'Price'],
datasets: [{
label: 'Model X',
data: [8, 7, 6, 9, 5],
borderColor: 'rgba(255,99,132,1)',
backgroundColor: 'rgba(255,99,132,0.2)',
fill: true
}]
},
options: { scales: { r: { beginAtZero: true } } }
});Radar Styling
Radar datasets accept the same styling as line charts: borderColor, fill, point styling, tension. A small tension (0.1) slightly rounds the vertices for a softer look; 0 keeps straight spikes. Use translucent backgroundColor so overlapping radar shapes remain visible.
datasets: [{
data: [7, 8, 6, 9, 5],
borderColor: 'blue',
backgroundColor: 'rgba(0,0,255,0.2)',
borderWidth: 2,
pointRadius: 4,
pointBackgroundColor: 'blue',
fill: true,
tension: 0.1 // slight curve between vertices
}]Radar Scale (r) Settings
The radial scale r controls rings and labels. min/max fix the value range across all axes (essential when comparing datasets). ticks.stepSize sets ring spacing; pointLabels styles the category names around the perimeter. Set ticks.backdropColor transparent so ring numbers don't obscure the gridlines.
options: {
scales: {
r: {
min: 0,
max: 10,
ticks: { stepSize: 2, backdropColor: 'transparent' },
pointLabels: { font: { size: 13 }, color: '#333' },
grid: { color: '#ccc' },
angleLines: { color: '#ccc' }
}
}
}Radar with Multiple Datasets
Multiple datasets overlay as concentric polygons, ideal for before/after or competitor comparisons. Always set the same min/max on r so the shapes are comparable, and use translucent fills so both profiles remain visible where they overlap.
new Chart(ctx, {
type: 'radar',
data: {
labels: ['A', 'B', 'C', 'D', 'E'],
datasets: [
{ label: 'Before', data: [5,6,4,7,5], borderColor: 'red', backgroundColor: 'rgba(255,0,0,0.15)', fill: true },
{ label: 'After', data: [7,8,6,9,7], borderColor: 'blue', backgroundColor: 'rgba(0,0,255,0.15)', fill: true }
]
}
});Radar Angle Lines & Grid
angleLines are the spokes from center to each vertex; grid is the concentric polygon rings. Hiding ticks (display:false) removes the numeric ring labels for a cleaner aesthetic while keeping the rings themselves. pointLabels controls only the category text at each vertex.
options: {
scales: {
r: {
angleLines: { display: true, color: 'rgba(0,0,0,0.2)', lineWidth: 1 },
grid: { display: true, color: 'rgba(0,0,0,0.1)' },
pointLabels: { display: true, color: '#000', font: { weight: 'bold' } },
ticks: { display: false } // hide numeric ring labels for a cleaner look
}
}
}Polar Area Charts
Basic Polar Area Chart
A polar area chart draws each value as a sector spanning the same angle (360/N each), with the radius encoding the value. Unlike pie (where angle = value), here the radius = value, so all slices have equal angular width but different lengths.
new Chart(ctx, {
type: 'polarArea',
data: {
labels: ['North', 'South', 'East', 'West'],
datasets: [{
data: [11, 16, 7, 14],
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB']
}]
},
options: { scales: { r: { beginAtZero: true } } }
});Polar Area vs Pie
The key difference: in a pie, each slice's ANGLE is proportional to its value; in a polar area, each slice's RADIUS is proportional to its value while angles are equal. Use polar area when you want every category equally visible but sized by magnitude.
// PIE: angle = value, radius = constant
new Chart(ctx, { type: 'pie', data: { datasets: [{ data: [10, 20, 30] }] } });
// POLAR: angle = constant, radius = value
new Chart(ctx, { type: 'polarArea', data: { datasets: [{ data: [10, 20, 30] }] } });
// polar area is better when values differ widely AND
// you want every category to occupy an equal angular sharePolar Scale (r)
Polar area uses a single radial scale r shared by all sectors — so all values are measured against the same maximum radius. Setting an explicit max makes the chart honest about scale; without it Chart.js autoscales to the largest value, which can exaggerate small differences.
options: {
scales: {
r: {
min: 0,
max: 20,
ticks: { stepSize: 5 },
grid: { color: 'rgba(0,0,0,0.15)' },
angleLines: { color: 'rgba(0,0,0,0.15)' }
}
}
}Polar Border Styling
Polar area sectors accept the same border/fill styling as pie/doughnut. White borders separate adjacent sectors clearly. hoverBackgroundColor changes a sector's fill on hover for interactive feedback. Unlike doughnut there is no cutout — the sectors meet at the center.
datasets: [{
data: [11, 16, 7, 14],
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB'],
borderColor: 'white',
borderWidth: 2,
hoverBackgroundColor: '#333'
}]Polar with Hover Offset
Polar area supports hoverBackgroundColor and hoverBorderColor but does NOT support hoverOffset / offset the same way doughnut does, because sectors share a radial scale. If 'popping out' a slice is essential, choose doughnut instead. Use color and tooltip changes for polar interactivity.
datasets: [{
data: [11, 16, 7, 14],
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB'],
hoverOffset: 12 // not supported on polarArea the same way as doughnut;
// use hoverBackgroundColor + tooltips for interactivity
}]
// for true "pop out" use 'doughnut' insteadBubble Charts
Basic Bubble Chart
Bubble chart data points are objects {x, y, r} where x and y are coordinates and r is the bubble RADIUS in pixels. Unlike scatter, the bubble size encodes a third variable. Note r is a radius (not area), so a 2x larger value is visually 4x the area — consider sqrt-scaling your size data.
new Chart(ctx, {
type: 'bubble',
data: {
datasets: [{
label: 'Products',
data: [
{ x: 10, y: 20, r: 15 },
{ x: 15, y: 10, r: 10 },
{ x: 7, y: 25, r: 8 }
],
backgroundColor: 'rgba(255,99,132,0.6)'
}]
},
options: { scales: { y: { beginAtZero: true } } }
});Bubble Size Mapping
Because humans perceive bubble AREA rather than radius, map your value through Math.sqrt before assigning r, so doubling the value doubles the visible area. Pick a divisor that keeps the largest bubble from overlapping too many neighbors. This makes the chart perceptually honest.
const raw = [{x:1,y:2,pop:1000}, {x:2,y:3,pop:5000}, {x:3,y:1,pop:20000}];
// map a real-world value to a pixel radius with sqrt scaling
// so bubble AREA (not radius) is proportional to the value
const data = raw.map(d => ({
x: d.x,
y: d.y,
r: Math.sqrt(d.pop) / 10 // tune the divisor for your dataset
}));
new Chart(ctx, { type: 'bubble', data: { datasets: [{ data }] } });Bubble Colors per Point
Like other Chart.js datasets, bubble styling properties accept arrays to style each bubble individually. Use this to color-code bubbles by category while size encodes a separate numeric value, giving a 4-dimensional chart (x, y, size, color).
datasets: [{
data: [
{ x: 10, y: 20, r: 15 },
{ x: 15, y: 10, r: 10 }
],
// arrays give per-point styling
backgroundColor: ['rgba(255,99,132,0.6)', 'rgba(54,162,235,0.6)'],
borderColor: ['rgba(255,99,132,1)', 'rgba(54,162,235,1)'],
borderWidth: 2
}]Multiple Bubble Datasets
Multiple datasets render as differently-colored bubble groups, each in the legend. Keep bubble radii modest when overlaying groups so bubbles don't fully obscure one another. alpha-transparent fills (0.5-0.6) help overlapping bubbles remain readable.