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.
new Chart(ctx, {
type: 'bubble',
data: {
datasets: [
{ label: 'Asia', data: [{x:1,y:2,r:10},{x:3,y:4,r:8}], backgroundColor: 'rgba(255,99,132,0.6)' },
{ label: 'Europe', data: [{x:2,y:5,r:12},{x:4,y:2,r:7}], backgroundColor: 'rgba(54,162,235,0.6)' }
]
}
});Bubble Hover Styling
Hover properties (hoverBackgroundColor, hoverBorderColor, hoverBorderWidth, hoverRadius) apply only to the bubble under the pointer. hoverRadius adds extra pixels to the radius on hover, making the targeted bubble visibly pop. Combined with tooltips this gives clear interactive focus.
datasets: [{
data: [{x:1,y:2,r:10}],
backgroundColor: 'rgba(75,192,192,0.5)',
hoverBackgroundColor: 'rgba(75,192,192,0.9)',
hoverBorderColor: 'black',
hoverBorderWidth: 3,
hoverRadius: 2 // EXTRA radius added on hover (v3+ uses hoverRadius)
}]Scatter Charts
Basic Scatter Chart
Scatter chart data points are {x, y} objects (no radius needed). The x scale should be type: 'linear' (NOT category) so numeric x values map to actual positions. Scatter is the standard chart for showing correlation between two continuous variables.
new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: 'Observations',
data: [
{ x: 1.2, y: 2.3 },
{ x: 1.8, y: 3.1 },
{ x: 2.5, y: 4.0 },
{ x: 3.1, y: 4.8 }
],
backgroundColor: 'rgba(75,192,192,0.7)'
}]
},
options: { scales: { x: { type: 'linear', position: 'bottom' } } }
});Scatter with Many Points
For thousands of points, shrink pointRadius (1-2px) and use semi-transparent backgroundColor (alpha 0.2-0.4) so overlapping points reveal density through darker regions. Disable point hover for very large datasets (pointHitRadius: 0) to keep interaction responsive.
const n = 2000;
const data = Array.from({ length: n }, () => ({
x: Math.random() * 100,
y: Math.random() * 100
}));
new Chart(ctx, {
type: 'scatter',
data: { datasets: [{ data, pointRadius: 1.5, backgroundColor: 'rgba(0,0,0,0.3)' }] }
});Scatter Point Styling
pointStyle supports many shapes beyond the default circle: 'rect', 'rectRot', 'triangle', 'star', 'cross', 'crossRot', 'dash'. pointRotation rotates non-circular shapes. All point* properties accept arrays for per-point styling — useful for highlighting specific observations.
datasets: [{
data: [{x:1,y:2},{x:2,y:3}],
pointRadius: 8,
pointHoverRadius: 12,
pointStyle: 'triangle', // 'circle','rect','star','cross',...
pointBackgroundColor: 'red',
pointBorderColor: 'darkred',
pointBorderWidth: 2,
pointRotation: 45 // rotation in degrees (for shapes)
}]Scatter with Trend Line
Chart.js has no built-in trend line, so compute a regression (least-squares here) and add a 'line' dataset with two endpoints and pointRadius:0. The line dataset's type overrides the chart type per-dataset — this is also how mixed charts work. fill:false keeps it as just a line.
// simple least-squares fit
const pts = [{x:1,y:2},{x:2,y:3.5},{x:3,y:4.2},{x:4,y:5.1}];
const n = pts.length;
const mX = pts.reduce((s,p)=>s+p.x,0)/n;
const mY = pts.reduce((s,p)=>s+p.y,0)/n;
const slope = pts.reduce((s,p)=>s+(p.x-mX)*(p.y-mY),0) / pts.reduce((s,p)=>s+(p.x-mX)**2,0);
const intercept = mY - slope*mX;
new Chart(ctx, {
type: 'scatter',
data: { datasets: [
{ data: pts, backgroundColor: 'blue' },
{ type: 'line', data: [{x:0,y:intercept},{x:5,y:slope*5+intercept}],
borderColor: 'red', pointRadius: 0, fill: false }
]}
});Scatter to Line (showLine)
showLine: true on a scatter dataset connects the points with a line, effectively turning scatter into a line chart that uses numeric x coordinates. This is the right choice when x values are real numbers (not category labels) but you still want connecting segments — e.g. time series with irregular spacing.
new Chart(ctx, {
type: 'scatter',
data: { datasets: [{
data: [{x:1,y:2},{x:2,y:3},{x:3,y:5}],
showLine: true, // connect points with a line
borderColor: 'green',
backgroundColor: 'green',
pointRadius: 4
}]}
});Data — Datasets
Datasets Structure
data holds labels (shared across all datasets) and datasets (an array of series). Each dataset has a label (shown in legend/tooltips), a data array aligned with labels, and styling properties. Properties set on the dataset apply to every point unless overridden by an array.
data: {
labels: ['A', 'B', 'C'], // shared category labels
datasets: [
{
label: 'Series 1', // legend + tooltip label
data: [10, 20, 30], // values (one per label)
backgroundColor: 'rgba(0,0,255,0.5)',
borderColor: 'blue',
borderWidth: 1,
// ... type-specific styling
}
]
}labels & data Alignment
Each dataset's data array is positionally aligned with the shared labels array — data[i] is the value for labels[i]. A shorter data array produces undefined values for the missing tail, which line charts treat as gaps. Always keep labels and data the same length to avoid surprising gaps.
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [
{ data: [10, 20, 30] }, // Jan=10, Feb=20, Mar=30
{ data: [5, 15] } // Jan=5, Feb=15, Mar=undefined
]
}
// data[i] always corresponds to labels[i]; a shorter data array
// leaves the remaining positions as undefined (missing data)Multiple Datasets
Multiple datasets render as parallel series (grouped bars, multiple lines, overlapping radars). They share one labels array. Set distinct colors per dataset — Chart.js only auto-colors when backgroundColor is omitted, and even then explicit colors are more predictable across versions.
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)' },
{ label: '2025', data: [28,38,36,45], backgroundColor: 'rgba(75,192,192,0.6)' }
]
}Dataset-Level Styling
Dataset-level style applies to every point in that dataset. hover* variants activate on hover. hidden:true starts the dataset hidden (the legend toggle can show it again). order controls z-order — lower numbers draw later (on top) — useful in mixed/overlapping charts.
datasets: [{
label: 'Revenue',
data: [10, 20, 30],
backgroundColor: 'rgba(54,162,235,0.6)',
borderColor: 'rgba(54,162,235,1)',
borderWidth: 2,
hoverBackgroundColor: 'rgba(54,162,235,0.9)',
hidden: false, // set true to hide from view + legend toggle
order: 0 // lower order draws on top
}]Parsing Object Data
When your data is an array of objects, tell Chart.js which fields hold the axis values via parsing: {xAxisKey, yAxisKey}. This avoids manual mapping to {x, y} pairs. The keys default to 'x' and 'y', so {x, y} objects work without a parsing config.
new Chart(ctx, {
type: 'bar',
data: {
datasets: [{
data: [
{ name: 'Red', votes: 12 },
{ name: 'Blue', votes: 19 }
],
parsing: { xAxisKey: 'name', yAxisKey: 'votes' }
}]
}
});Per-Point Styling Arrays
Any styling property can be a single value (applies to all points) or an array (one per point). This enables highlighting specific points — e.g. set most backgroundColor to gray and one to red to draw attention. Array length should match the data length; shorter arrays cycle.
datasets: [{
data: [10, 20, 30, 40],
// every styling property accepts an array (one value per point)
backgroundColor: ['red', 'green', 'blue', 'orange'],
borderColor: ['darkred', 'darkgreen', 'darkblue', 'darkorange'],
borderWidth: [1, 2, 3, 4],
pointRadius: [3, 6, 9, 12] // (line/scatter only)
}]Options — Responsive & Scales
Options Structure
options is the top-level configuration object. It groups concerns: plugins (legend/title/tooltip), scales (x/y/r), layout (padding), animation, interaction (hover mode), and events. Almost every Chart.js behavior is tuned here rather than on individual datasets.
new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: {
responsive: true,
maintainAspectRatio: true,
plugins: { legend: {}, title: {}, tooltip: {} },
scales: { x: {}, y: {} },
layout: { padding: 10 },
animation: { duration: 800 },
interaction: { mode: 'nearest', intersect: false },
events: ['mousemove', 'click', 'touchstart']
}
});Responsive & maintainAspectRatio
responsive:true makes the chart fill its container's width and re-render on resize. maintainAspectRatio:false releases the height so the container's CSS height is honored — set both and wrap the canvas in a sized <div>. With maintainAspectRatio:true (default) the chart keeps a fixed width/height ratio.
<div style="width:100%; height:400px;">
<canvas id="c"></canvas>
</div>
<script>
new Chart(document.getElementById('c'), {
type: 'line',
data: { /* ... */ },
options: {
responsive: true, // resize with container
maintainAspectRatio: false // let the container control height
}
});
</script>Scales Overview
scales configures each axis. The id (x, y, r) is the scale key; type selects the scale class. Common types: 'category' (text labels), 'linear' (numbers), 'logarithmic', 'time' (requires a date adapter), 'radialLinear' (radar/polar). title.display + text adds an axis label.
options: {
scales: {
x: { type: 'category', title: { display: true, text: 'Month' } },
y: { type: 'linear', title: { display: true, text: 'Value' },
beginAtZero: true },
r: { type: 'radialLinear' } // for radar/polarArea
}
}
// built-in scale types: category, linear, logarithmic, time,
// timeseries, radialLinearPlugins Overview
plugins groups the built-in title, legend and tooltip plugins plus any registered custom plugins' options by their id. title adds a chart title above the canvas. legend controls the dataset key. tooltip controls hover popups. Custom plugins read their options from this same plugins object.
options: {
plugins: {
title: { display: true, text: 'My Chart', font: { size: 18 } },
legend: { display: true, position: 'top' },
tooltip: { enabled: true, mode: 'index', intersect: false },
// a custom inline plugin:
myPlugin: { prop: 'value' }
}
}Layout (Padding)
layout.padding adds space inside the canvas between the chart area and the canvas edge — useful when titles, axis labels or large tooltips would otherwise be clipped. A single number pads all sides equally; the object form allows per-side values.
options: {
layout: {
padding: {
top: 10, right: 20, bottom: 10, left: 20
}
}
}
// or a single number for all four sides:
// layout: { padding: 15 }Global Defaults (Chart.defaults)
Chart.defaults holds the global default configuration — set values once at app startup and every chart inherits them. This is the right place for branding (font family, base color, default animation). Per-chart options always override defaults, so you can still customize individual charts.
// set once, applies to every chart created afterwards
Chart.defaults.font.family = "'Segoe UI', sans-serif";
Chart.defaults.font.size = 13;
Chart.defaults.color = '#333';
Chart.defaults.borderColor = '#ddd';
Chart.defaults.plugins.legend.labels.color = '#333';
Chart.defaults.animation.duration = 600;
// per-chart override still wins:
new Chart(ctx, { options: { font: { size: 16 } } });Axes — X & Y
Linear Axis
A linear axis maps numbers proportionally. beginAtZero forces the axis to start at 0 (recommended for bar charts). min/max set hard limits; suggestedMin/suggestedMax are soft hints that Chart.js may expand to fit ticks. ticks.callback customizes the label text (e.g. append a unit).
options: {
scales: {
y: {
type: 'linear',
beginAtZero: true,
min: 0, max: 100, // hard limits
ticks: { stepSize: 10, callback: (v) => v + '%' }
}
}
}Category Axis
A category axis shows text labels at fixed positions. It's the default x axis for bar/line charts. ticks.autoSkip:false forces every label to show (otherwise Chart.js drops overlapping ones). maxRotation tilts labels to prevent overlap when there are many categories.
options: {
scales: {
x: {
type: 'category',
labels: ['Mon','Tue','Wed'], // optional; usually from data.labels
ticks: { autoSkip: false, maxRotation: 45, minRotation: 0 }
}
}
}Time Axis
A time axis plots Date objects or ISO strings proportionally to time, with gaps where there is no data. It requires a date adapter package (chartjs-adapter-date-fns, -luxon, -dayjs). time.unit fixes the tick granularity ('day','month','year'); without it Chart.js auto-picks a unit.
# install a date adapter
npm install chart.js date-fns date-fns-tz
import 'chartjs-adapter-date-fns';
new Chart(ctx, {
type: 'line',
data: { datasets: [{ data: [
{x: new Date('2024-01-01'), y: 10},
{x: new Date('2024-02-01'), y: 20},
{x: new Date('2024-04-01'), y: 15}
]}]},
options: { scales: { x: { type: 'time', time: { unit: 'month' } } } }
});Logarithmic Axis
A logarithmic axis spaces ticks by powers of 10, essential for data spanning orders of magnitude (population, prices, frequencies). It cannot display 0 or negative values. ticks.callback lets you format the powers-of-ten labels compactly (e.g. scientific notation).
new Chart(ctx, {
type: 'line',
data: { datasets: [{ data: [1, 10, 100, 1000, 10000] }] },
options: {
scales: {
y: { type: 'logarithmic',
ticks: { callback: (v) => v >= 1 ? v.toExponential(0) : v } }
}
}
});Axis Title & Ticks Callback
title.display + title.text add an axis label (always off by default — set display:true). ticks.callback receives each tick value and returns the display string, letting you format numbers as currency, percentages, k/M abbreviations, or any custom text without altering the underlying data.
options: {
scales: {
y: {
title: { display: true, text: 'Revenue (USD)', color: '#333', font: { size: 14, weight: 'bold' } },
ticks: {
callback: function(value) {
if (value >= 1000) return (value/1000) + 'k';
return value;
}
}
}
}
}Min/Max vs suggestedMin/Max
min/max are HARD limits: the axis stays within them even if data exceeds the range (data is clipped). suggestedMin/suggestedMax are SOFT hints: Chart.js uses them as a baseline but may expand to produce round tick values. Prefer suggested* when you want nice round numbers; use hard min/max for fixed scales like percentages.
options: {
scales: {
y: {
// hard limits — chart never goes outside this range
min: 0, max: 100,
// OR soft hints — Chart.js may expand to fit nice ticks:
// suggestedMin: 0, suggestedMax: 100,
ticks: { stepSize: 20 }
}
}
}Legend
Legend Basics
The legend lists each dataset's label with a color swatch. display:true shows it (default), display:false hides it. position accepts 'top', 'bottom', 'left', 'right' or 'center' (chartArea). Clicking a legend item toggles that dataset's visibility by default — a built-in filtering interaction.
new Chart(ctx, {
type: 'bar',
data: { labels: ['A','B'], datasets: [{ label: 'Sales', data: [10,20] }] },
options: {
plugins: {
legend: { display: true, position: 'top' }
}
}
});Legend Position & Align
position sets the legend's side; align controls its placement along that side ('start'/'center'/'end'). labels configures the swatch (boxWidth/boxHeight) and text styling. maxWidth limits the legend area so very long legends wrap instead of pushing the chart aside.
plugins: {
legend: {
position: 'bottom',
align: 'center', // 'start' | 'center' | 'end'
maxWidth: 300,
labels: {
boxWidth: 20,
boxHeight: 20,
padding: 15,
color: '#333',
font: { size: 12 }
}
}
}Legend onClick (Custom Toggle)
Override legend.onClick to customize click behavior. The default toggles dataset visibility. The signature is (event, legendItem, legend). Calling chart.update() after changing state re-renders. Use this to confirm before hiding, sync with external UI, or implement single-select (radio) behavior.
plugins: {
legend: {
onClick(e, item, legend) {
const chart = legend.chart;
const dataset = chart.data.datasets[item.datasetIndex];
// custom: toggle visibility AND log the action
dataset.hidden = !dataset.hidden;
console.log('Toggled', dataset.label, '->', dataset.hidden);
chart.update();
}
}
}Legend Labels Callback
generateLabels returns an array of label objects that fully control what appears in the legend. Each item's text, swatch colors, and hidden state can be customized — useful for adding counts, units, or deriving a legend from non-dataset data. The hidden flag and datasetIndex link clicks back to the right dataset.
plugins: {
legend: {
labels: {
generateLabels(chart) {
return chart.data.datasets.map((ds, i) => ({
text: `${ds.label} (${ds.data.length} pts)`,
fillStyle: ds.backgroundColor,
strokeStyle: ds.borderColor,
lineWidth: ds.borderWidth,
hidden: ds.hidden,
datasetIndex: i
}));
}
}
}
}Hide Legend & External Legend
For full styling control, disable the built-in legend (display:false) and render your own HTML legend. Iterate chart.data.datasets, render buttons/swatches, and toggle dataset.hidden + chart.update() on click. This is common when matching a design system where the default canvas legend won't fit in.
// hide the built-in legend entirely
options: { plugins: { legend: { display: false } } }
// build your own external legend HTML, then wire it to the chart:
const chart = new Chart(ctx, { /* ... */, options: { plugins: { legend: { display: false } } } });
chart.data.datasets.forEach((ds, i) => {
const btn = document.createElement('button');
btn.textContent = ds.label;
btn.onclick = () => { ds.hidden = !ds.hidden; chart.update(); };
document.getElementById('legend').appendChild(btn);
});Tooltips
Tooltip Basics
Tooltips appear on hover showing the dataset label and value. enabled:true by default. Styling properties (backgroundColor, titleColor, bodyColor, padding, cornerRadius) control the look. displayColors toggles the small color swatches next to each item — useful when many datasets overlap.
options: {
plugins: {
tooltip: {
enabled: true, // show tooltips
backgroundColor: 'rgba(0,0,0,0.8)',
titleColor: 'white',
bodyColor: 'white',
borderColor: 'gray',
borderWidth: 1,
padding: 10,
cornerRadius: 4,
displayColors: true // show color boxes
}
}
}Tooltip label Callback
callbacks.label returns the text for each tooltip row. ctx.dataset.label is the series name, ctx.parsed.y holds the value (use ctx.parsed for pie/radar). Format currency, append units, or compute totals here. The returned string (or array of strings) becomes the tooltip body.
plugins: {
tooltip: {
callbacks: {
label: (ctx) => {
const label = ctx.dataset.label || '';
const value = ctx.parsed.y ?? ctx.parsed;
return `${label}: $${value}`;
}
}
}
}Tooltip Title Callback
callbacks.title controls the tooltip header (default: the x-axis label). The function receives an array of tooltip items (useful when mode:'index' groups multiple datasets). Returning an empty string '' suppresses the title entirely. Combine title and label callbacks for fully custom tooltip content.
plugins: {
tooltip: {
callbacks: {
title: (items) => {
// items[0].label is the x-axis label / category
return 'Period: ' + items[0].label;
},
label: (ctx) => ctx.dataset.label + ': ' + ctx.formattedValue
}
}
}External HTML Tooltip
external lets you render the tooltip as real HTML — for rich styling, images, or links impossible on canvas. Set enabled:false so the default tooltip doesn't double-render. The callback receives a context with a tooltip object holding caretX/caretY (position) and body lines. Position the HTML element at those coordinates.
plugins: {
tooltip: {
enabled: false, // disable default canvas tooltip
external(context) {
const { tooltip } = context;
let el = document.getElementById('tt');
if (!el) { el = document.createElement('div'); el.id = 'tt'; document.body.appendChild(el); }
if (tooltip.opacity === 0) { el.style.opacity = 0; return; }
el.innerHTML = tooltip.body.map(b => b.lines.join('')).join('<br>');
el.style.position = 'fixed';
el.style.left = tooltip.caretX + 'px';
el.style.top = tooltip.caretY + 'px';
el.style.opacity = 1;
}
}
}Tooltip Mode (interaction)
interaction.mode controls what the tooltip targets. 'point' shows only the hovered point; 'index' shows all datasets at the same x (great for grouped bars/lines); 'nearest' finds the closest single point. intersect:false triggers the tooltip when the pointer is anywhere in the column, not just on a point — usually what users expect.
options: {
interaction: {
mode: 'index', // 'point' | 'nearest' | 'index' | 'x' | 'y' | 'dataset'
intersect: false // trigger even when not directly over a point
},
plugins: { tooltip: { mode: 'index', intersect: false } }
}Tooltip Styling & Filters
filter skips tooltip rows that don't meet a condition (e.g. hide zero/empty values). callbacks.footer adds a summary row after all items — perfect for showing a total. bodyFont/titleFont style the text; caretSize and caretPadding control the little pointer arrow.
plugins: {
tooltip: {
filter: (item) => item.parsed.y !== 0, // hide zero values
callbacks: { footer: (items) => 'Total: ' + items.reduce((s,i)=>s+i.parsed.y,0) },
bodyFont: { size: 13, weight: 'bold' },
titleFont: { size: 14 },
caretSize: 6,
caretPadding: 8
}
}Animation
Animation Duration
animation.duration sets the total animation length in milliseconds (default 1000). easing controls the acceleration curve — 'easeOutQuart' (the default) starts fast and decelerates, which feels responsive. Shorten duration (200-400ms) for data-dashboards that update frequently.
options: {
animation: {
duration: 1000, // total duration in ms
easing: 'easeOutQuart'
}
}
// easing options: 'linear','easeInQuad','easeOutQuad','easeInOutQuad',
// 'easeInCubic','easeOutCubic','easeInOutCubic','easeOutBounce',...Disable Animation
Set animation:false to turn off all animations entirely — best for performance-critical dashboards or when feeding rapid updates. animation:{duration:0} keeps the animation system active but instant (hover still works). The plural animations object lets you disable specific properties like y while animating others.
// disable ALL animations
options: { animation: false }
// disable only the initial draw, keep hover animations:
options: { animation: { duration: 0 } }
// per-property: animate everything except the y axis
options: {
animations: {
y: { duration: 0 }
}
}Easing Functions
Easing changes how an animation progresses over time, dramatically affecting feel. 'easeOutQuart' (default) feels professional and responsive. 'easeOutBounce' or 'easeInOutBack' are playful but can feel gimmicky on data charts. Match the easing to your app's overall motion language.
options: {
animation: {
duration: 1200,
easing: 'easeInOutBack' // slight overshoot for a playful feel
}
}
// common easings:
// 'linear' constant speed (mechanical)
// 'easeOutQuart' default; snappy start, smooth end
// 'easeInOutCubic' symmetric in/out
// 'easeOutBounce' bouncy landing
// 'easeInOutBack' overshoots at both endsAnimation onComplete
onComplete fires when an animation finishes — useful for chaining actions like enabling an export button or triggering a follow-up render. onProgress fires every animation frame with the step count. Avoid heavy work in onProgress as it runs 60x/second.
options: {
animation: {
onComplete: function() {
console.log('Animation finished');
// e.g. enable a "download PNG" button now that the chart is stable
document.getElementById('downloadBtn').disabled = false;
},
onProgress: function(ctx) {
// fired each frame; ctx.currentStep / ctx.numSteps
}
}
}Per-Property Animation (animations)
The plural animations object configures individual properties independently. Each entry can specify from/to (or a function of the parsing context), duration, easing, and loop. Animating tension from 1 to 0.3 produces a 'line settles' effect; animating y from the axis baseline produces a 'grow up' effect.
options: {
animations: {
tension: {
duration: 1500,
easing: 'easeOutBounce',
from: 1, // start very curved
to: 0.3, // settle at a gentle curve
loop: false
},
colors: { duration: 800 },
y: { from: ctx => ctx.chart.scales.y.getPixelForValue(0) }
}
}Events & Clicks
onClick Handler
options.onClick fires when the user clicks anywhere on the chart. The elements argument is an array of clicked chart elements (bars/points), each with .datasetIndex and .index. If elements is empty the click hit blank canvas. This is the standard way to make charts interactive (drill-down, selection).
new Chart(ctx, {
type: 'bar',
data: { labels: ['A','B','C'], datasets: [{ data: [10,20,30] }] },
options: {
onClick(event, elements, chart) {
if (elements.length > 0) {
const el = elements[0];
const label = chart.data.labels[el.index];
const value = chart.data.datasets[el.datasetIndex].data[el.index];
alert(`Clicked ${label} = ${value}`);
}
}
}
});getElementsAtEventForMode
getElementsAtEventForMode finds the chart elements under a native mouse event, with the same mode options as interaction ('nearest', 'index', 'x', etc.). The 4th arg useFinalPosition should be true for hit-testing the final rendered positions. Use this when you need element lookups outside options.onClick.
const chart = new Chart(ctx, { /* ... */ });
canvas.addEventListener('click', (e) => {
const points = chart.getElementsAtEventForMode(
e, 'nearest', { intersect: true }, true // (event, mode, options, useFinalPosition)
);
if (points.length) {
const p = points[0];
console.log('dataset', p.datasetIndex, 'index', p.index);
}
});onHover
options.onHover fires on every mouse move over the chart. Use it for cheap UI feedback like changing the cursor (as shown) or highlighting related external elements. Avoid expensive operations here — it fires very frequently. For heavier logic, use onClick or debounce the handler.
options: {
onHover(event, elements, chart) {
// change cursor to a pointer when hovering a bar
event.native.target.style.cursor =
elements.length ? 'pointer' : 'default';
}
}Hover & Active Elements
The hover config controls how the chart highlights elements under the pointer. mode:'nearest' + intersect:true highlights the single nearest point. animationDuration smooths the highlight transition. chart.setActiveElements programmatically triggers the hover state — useful for syncing chart highlights with external UI.
options: {
hover: {
mode: 'nearest',
intersect: true,
animationDuration: 200 // smooth transition when hover target changes
}
}
// programmatically set the hovered element:
chart.setActiveElements([{ datasetIndex: 0, index: 2 }]);
chart.update();Events List
events limits which DOM events the chart listens to. Reducing the list (e.g. to just 'click') disables hover effects for a static feel and improves performance on dashboards. For touch-only mobile contexts, include touchstart/touchmove. An empty array disables all interaction.
options: {
events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']
// restrict to just click for a non-interactive "static" feel:
// events: ['click']
}
// supported events: mousemove, mouseout, click, touchstart, touchmove,
// touchend, pointer events (Chart.js v3.4+)Drill-Down Example
A common pattern: clicking a bar swaps the dataset to a more detailed view (drill-down). Capture the clicked index, replace chart.data.labels and chart.data.datasets[...].data, then call chart.update() to animate the transition. Keep a stack of views if you want a back button.
const monthly = { labels: ['Jan','Feb','Mar'], data: [100,120,90] };
const daily = { labels: Array.from({length:28},(_,i)=>'D'+(i+1)),
data: Array.from({length:28},()=>Math.random()*50) };
const chart = new Chart(ctx, {
type: 'bar',
data: { labels: monthly.labels, datasets: [{ data: monthly.data }] },
options: { onClick(evt, els) {
if (!els.length) return;
const idx = els[0].index;
chart.data.labels = daily.labels;
chart.data.datasets[0].data = daily.data;
chart.update();
}}
});Mixed Charts
Bar + Line Mixed Chart
Mix chart types by setting type on individual datasets — it overrides the chart-level type for that series. A bar + line combo is the most common mix (e.g. revenue as bars, growth rate as a line). Use a second y axis (y1) when the series have different units or scales.
new Chart(ctx, {
type: 'bar', // default type for all datasets
data: {
labels: ['Jan','Feb','Mar','Apr'],
datasets: [
{ type: 'bar', label: 'Revenue', data: [20,35,30,40], backgroundColor: '#36A2EB', yAxisID: 'y' },
{ type: 'line', label: 'Growth', data: [5,8,6,12], borderColor: '#FF6384', yAxisID: 'y1', fill: false }
]
},
options: { scales: { y: { position: 'left' }, y1: { position: 'right', grid: { drawOnChartArea: false } } } }
});Dataset type Override
Any dataset can override the chart-level type, letting you combine bar, line and scatter in one chart. Datasets without an explicit type inherit the chart's type. Mind the z-order: datasets are drawn in array order, so place background series (bars) before foreground series (lines).
new Chart(ctx, {
type: 'line', // chart-level default
data: {
labels: ['A','B','C'],
datasets: [
{ data: [1,2,3] }, // inherits 'line'
{ type: 'bar', data: [3,2,1] }, // becomes a bar
{ type: 'scatter', data: [{x:0,y:2},{x:2,y:3}], pointRadius: 5 } // scatter points
]
}
});Multiple Y Axes (y, y1, y2)
Define multiple y axes with custom ids (y, y1, y2) and assign datasets via yAxisID. grid.drawOnChartArea:false prevents the second axis from drawing gridlines over the first. position:'right' stacks on the right; add offset:true to a third axis so it doesn't overlap the second. Color each axis to match its series.
options: {
scales: {
y: { type: 'linear', position: 'left', title: { display: true, text: 'Price ($)' } },
y1: { type: 'linear', position: 'right', title: { display: true, text: 'Volume' }, grid: { drawOnChartArea: false } },
y2: { type: 'linear', position: 'right', offset: true, title: { display: true, text: '%' } }
}
}
// assign each dataset: yAxisID: 'y' | 'y1' | 'y2'Mixed Data with Different Shapes
Mixed charts can combine a value series (bars) with reference lines (constant target, trend). Set pointRadius:0 and fill:false on the line datasets so they render as pure lines without markers or fills. This pattern is common in performance dashboards (actual vs target).
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Mon','Tue','Wed','Thu','Fri'],
datasets: [
{ type:'bar', label:'Tasks', data:[8,6,7,9,5], backgroundColor:'rgba(54,162,235,0.6)' },
{ type:'line', label:'Target',data:[7,7,7,7,7], borderColor:'red', borderWidth:2, pointRadius:0, fill:false },
{ type:'line', label:'Trend', data:[6,6.5,7,8,8.5], borderColor:'green', borderDash:[5,5], pointRadius:0, fill:false }
]
}
});Mixed Styling Tips
In mixed charts, set dataset.order so the line draws above the bars (lower order = on top). Color-code each axis's ticks and title to match its series so readers know which scale belongs to which line. Disable the second axis's grid (drawOnChartArea:false) to avoid double gridlines.
// 1. assign order so lines draw on top of bars
datasets: [
{ type:'bar', data:[...], order: 2 },
{ type:'line', data:[...], order: 1 } // lower order draws later (on top)
]
// 2. give each axis a matching color
options: { scales: {
y: { ticks: { color: '#36A2EB' }, title: { text: 'Revenue', color: '#36A2EB' } },
y1: { ticks: { color: '#FF6384' }, title: { text: 'Growth', color: '#FF6384' }, position: 'right', grid: { drawOnChartArea: false } }
}}Plugins
Built-in Plugins
Chart.js ships three built-in plugins: Title (chart title above the canvas), Legend (the dataset key), and Tooltip (hover popups). They are configured under options.plugins by their id and are enabled by default (except Title, which needs display:true to show its text).
// three built-in plugins (always available):
// Legend -> options.plugins.legend
// Title -> options.plugins.title
// Tooltip -> options.plugins.tooltip
new Chart(ctx, {
type: 'line',
data: { /* ... */ },
options: {
plugins: {
title: { display: true, text: 'Sales 2024', font: { size: 18 } },
legend: { position: 'bottom' },
tooltip:{ mode: 'index', intersect: false }
}
}
});Custom Inline Plugin
Pass an array of plugin objects to a single chart via the plugins option. Each plugin has an id and hook functions (beforeInit, afterInit, beforeDraw, afterDraw, beforeUpdate, etc.). Inline plugins apply only to that chart instance. ctx.save()/restore() bracket any canvas state changes you make.
new Chart(ctx, {
type: 'line',
data: { /* ... */ },
options: { /* ... */ },
plugins: [{
id: 'centerText',
afterDraw(chart) {
const { ctx, chartArea } = chart;
ctx.save();
ctx.font = 'bold 24px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Hello', (chartArea.left+chartArea.right)/2,
(chartArea.top+chartArea.bottom)/2);
ctx.restore();
}
}]
});Plugin Hooks (lifecycle)
Plugin hooks run at every stage of the chart lifecycle: init, update, render, draw, resize, destroy. before* hooks can cancel an action by returning false. The most common for custom drawing is afterDraw (draw on top of the chart) or beforeDraw (draw behind the data). Each hook receives the chart instance.
const plugin = {
id: 'myHook',
beforeInit(chart) { console.log('before init'); },
afterInit(chart) { console.log('chart ready'); },
beforeUpdate(chart){ console.log('about to update'); },
beforeDraw(chart) { console.log('before draw'); },
afterDraw(chart) { console.log('after draw'); },
beforeRender(chart){ console.log('before first render'); },
resize(chart, size){ console.log('resized', size); },
destroy(chart) { console.log('destroyed'); }
};Plugin: Vertical Line at Index
A common custom plugin draws a vertical guideline at the tooltip position (like the default crosshair). afterDraw reads the active tooltip element's x coordinate and strokes a dashed line from top to bottom of the chart area. Wrap canvas state in save()/restore() so the dashed line style doesn't leak.
const verticalLine = {
id: 'verticalLine',
afterDraw(chart) {
if (chart.tooltip?._active?.length) {
const x = chart.tooltip._active[0].element.x;
const { top, bottom } = chart.chartArea;
const ctx = chart.ctx;
ctx.save();
ctx.beginPath();
ctx.moveTo(x, top);
ctx.lineTo(x, bottom);
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.restore();
}
}
};Plugin: Center Text for Doughnut
Drawing centered summary text is the classic use case for a doughnut plugin. afterDatasetsDraw runs after the wedges so text sits on top. Compute the total (or any summary) and render it at the geometric center of the chartArea. This turns a doughnut into an informative KPI widget.
const centerText = {
id: 'centerText',
afterDatasetsDraw(chart) {
const { ctx, data } = chart;
const total = data.datasets[0].data.reduce((a,b)=>a+b, 0);
ctx.save();
ctx.font = 'bold 28px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const { left, right, top, bottom } = chart.chartArea;
ctx.fillText(total, (left+right)/2, (top+bottom)/2);
ctx.restore();
}
};
new Chart(ctx, { type:'doughnut', data:{...}, plugins:[centerText] });Register Plugin Globally
Chart.register(plugin) makes a plugin apply to every chart created afterwards — ideal for app-wide plugins like watermarks, branding, or analytics. For a one-off chart use the inline plugins array instead. Chart.unregister removes it. Registered plugins read their config from options.plugins[id].
import { Chart } from 'chart.js';
const myPlugin = { id: 'watermark', afterDraw(chart) { /* ... */ } };
// register once -> applies to EVERY chart
Chart.register(myPlugin);
// or unregister later
Chart.unregister(myPlugin);
// per-chart-only: skip Chart.register and pass via options.plugins insteadUpdate & Destroy
chart.update()
After mutating chart.data or chart.options, call chart.update() to re-render. By default the change animates from the previous state. Pass 'none' as the mode to skip the animation (useful for high-frequency updates). Mutating then updating is far cheaper than destroying and recreating the chart.
const chart = new Chart(ctx, config);
// mutate the data/config, then update:
chart.data.labels.push('Jun');
chart.data.datasets[0].data.push(28);
chart.options.scales.y.max = 50;
chart.update(); // animate to the new state
// chart.update('none'); // skip animationUpdate Modes
update(mode) accepts a mode string controlling behavior. 'none' disables animation for a silent refresh. 'reset' rewinds to the pre-animation values then re-animates (good for replaying the entrance). 'active' updates only hovered elements for cheap hover re-renders. The default (no arg) animates normally.
chart.update(); // default: animates the change
chart.update('none'); // no animation
chart.update('reset'); // reset to pre-animation state, then animate
chart.update('resize'); // re-compute layout (call after container resize)
chart.update('show'); // show currently-hidden dataset animations
chart.update('hide'); // hide dataset animations
chart.update('active'); // update only the active (hovered) elementsUpdate Data & Labels
You can replace data/labels wholesale or mutate them in place (push/shift). For live-streaming charts (e.g. a 60-second window), append the new value and shift the oldest, then update('none') for a smooth non-animated refresh — calling the default animated update on every tick causes a janky lag.
// replace the entire data array
chart.data.datasets[0].data = [5, 10, 15, 20];
chart.data.labels = ['A','B','C','D'];
chart.update();
// or append streaming data (and trim the oldest):
const arr = chart.data.datasets[0].data;
arr.push(newValue);
if (arr.length > 60) arr.shift();
chart.update('none'); // instant update for live datachart.destroy()
destroy() tears down the chart: it removes event listeners, cancels animations, clears the canvas and frees memory. Always call destroy when a chart's container is removed (React useEffect cleanup, route change). Failing to destroy leaks memory and can throw 'Canvas is already in use' errors when reusing a canvas.
const chart = new Chart(ctx, config);
// when done (route change, component unmount, etc.):
chart.destroy();
// after destroy the canvas is free; a new Chart can reuse it:
const fresh = new Chart(ctx, newConfig);
// destroy also removes event listeners and frees memorychart.resize() & clear()
chart.resize() recomputes the chart size — with responsive:true this happens automatically on window resize, so manual calls are rare. chart.clear() blanks the canvas without destroying the chart (the next update redraws). chart.resize(width, height) forces specific pixel dimensions, overriding responsive sizing.
const chart = new Chart(ctx, config);
// manually trigger a resize (rarely needed; responsive:true auto-resizes)
chart.resize();
// clear the canvas (chart stays alive, just blank until next render):
chart.clear();
chart.draw(); // re-draw without recomputing layout
// resize the canvas to explicit dimensions:
chart.resize(800, 400);Responsive & Device Pixel Ratio
Responsive Container
For a responsive chart, wrap the canvas in a positioned container with explicit width/height and set responsive:true + maintainAspectRatio:false. The canvas fills the container and re-renders on window resize. Without maintainAspectRatio:false the chart keeps a fixed aspect ratio and ignores the container's height.
<!-- container controls the size -->
<div style="position: relative; width: 100%; height: 400px;">
<canvas id="c"></canvas>
</div>
<script>
new Chart(document.getElementById('c'), {
type: 'line',
data: { /* ... */ },
options: { responsive: true, maintainAspectRatio: false }
});
</script>maintainAspectRatio
maintainAspectRatio:true (default) keeps the canvas at a fixed width/height ratio set by aspectRatio (default 2 = 2:1). maintainAspectRatio:false releases the height to the container. Pie/doughnut default to aspectRatio:1 (square) since circles look wrong in a wide rectangle.
// default: chart keeps a 2:1 (width:height) aspect ratio
options: { responsive: true, maintainAspectRatio: true, aspectRatio: 2 }
// let the container control height:
options: { responsive: true, maintainAspectRatio: false }
// square charts:
options: { responsive: true, maintainAspectRatio: true, aspectRatio: 1 }aspectRatio
aspectRatio = width / height. With maintainAspectRatio:true the canvas dimensions follow this ratio. Use a higher ratio (3-4) for wide dashboard tiles, lower (1-1.5) for tall panels. Pie/doughnut default to 1 because circles need a square canvas to avoid looking like ellipses.
new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 3 // width:height = 3:1 (wide, short chart)
}
});
// common ratios:
// 2 -> default for bar/line
// 1 -> pie/doughnut (square)
// 3 -> wide dashboards
// 16/9 -> video-likechart.resize() Method
Chart.js listens to window resize events automatically when responsive:true, but layout changes from CSS transitions, sidebar toggles, or tab switches may not fire a window resize. In those cases call chart.resize() manually after the container's size has settled (e.g. in a setTimeout or transitionend handler).
const chart = new Chart(ctx, { /* responsive: true */ });
// manually trigger a re-layout (e.g. after a sidebar collapses)
window.dispatchEvent(new Event('resize'));
// or:
chart.resize();
// explicit size:
chart.resize(600, 300);devicePixelRatio (sharp rendering)
devicePixelRatio scales the canvas backing store so lines stay sharp on high-DPI/retina displays (default uses window.devicePixelRatio, usually 2 or 3). Capping at 2 keeps text crisp while limiting the pixel workload on 3x phones. Setting 1 produces blurry output on retina but maximizes render speed.
// let Chart.js use the screen's full pixel density (default):
new Chart(ctx, { options: { devicePixelRatio: window.devicePixelRatio } });
// cap DPR for performance on retina displays (fewer pixels to render):
options: { devicePixelRatio: 2 }
// ignore DPR (renders at 1x — blurry on retina but fastest):
options: { devicePixelRatio: 1 }ResizeObserver Pattern
A ResizeObserver on the chart's container catches size changes from CSS flexbox/grid, collapsible sidebars, and tab switches that window resize misses. Disconnect the observer and call chart.destroy() in your teardown to prevent leaks. This is the most robust responsive pattern for complex layouts.
const canvas = document.getElementById('c');
const chart = new Chart(canvas, config);
// observe the CONTAINER, not the canvas:
const ro = new ResizeObserver(() => chart.resize());
ro.observe(canvas.parentElement);
// cleanup on destroy:
function teardown() {
ro.disconnect();
chart.destroy();
}Related Chart.js snippets
Copy-paste ready code for common tasks.
Bar Chart
Vertical bar chart with custom colors and rounded corners.
Line Chart
Smooth line chart with fill, tension, and hover styling.
Pie Chart
Pie chart with per-slice colors and legend positioning.
Doughnut Chart
Doughnut chart with cutout control and centered title.
Radar Chart
Multi-series radar chart for comparing entities across dimensions.
Responsive Chart
Chart that fills its container with maintainAspectRatio disabled.
Options Configuration
Title, legend, axis formatting, animations, and live updates.
Tooltips
Custom-styled tooltips with title, label, and footer callbacks.
Was this helpful?