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.