Chart.js Scatter & Bubble Charts Explained — Visualize Three Variables
Scatter and bubble charts reveal relationships between numeric variables — showing not just what the values are, but how they relate to each other. A scatter plot answers “does X go up when Y goes down?” A bubble chart adds a third dimension: size.
What You’ll Learn
By the end of this tutorial, you’ll build scatter plots with custom point styling and multi-series comparison, create bubble charts that encode three variables (x, y, size), configure linear axes with proper scaling and tick formatting, compute and draw trend lines using custom plugins, and build a market analysis dashboard with interactive tooltips.
Why Scatter and Bubble Charts Matter
A bar chart can show sales by product. But it can’t answer “does raising the price reduce sales?” A scatter plot can — by putting price on the x-axis and sales on the y-axis, you see the relationship instantly.
This is called correlation analysis, and it’s foundational in data science, finance, and security analytics.
Real-world use: Durga Antivirus Pro uses scatter plots to analyze threat detection patterns. Each point is a detected file — x-axis is file size, y-axis is detection confidence. Suspicious outliers (large files with low confidence) appear as isolated points far from the cluster, flagging them for manual review.
Where This Fits in Your Learning Path
flowchart LR
A["Chart.js Basics"] --> B["Line, Bar & Mixed Charts"]
B --> C["Pie, Doughnut, Radar & Polar"]
C --> D["**Scatter & Bubble Charts**"]
D --> E["Chart.js Advanced"]
style D fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#22c55e
Scatter Chart
A scatter plot places individual data points on an X/Y grid. Each point has two values: its x-coordinate and its y-coordinate. This reveals patterns — clusters, outliers, and correlations — that summary statistics hide.
The Key Difference from Other Charts
In a bar or line chart, the x-axis uses categories (labels like “January” or “Product A”). In a scatter chart, both axes use numbers. This is crucial: you’re plotting one numeric variable against another.
new Chart(ctx, {
type: "scatter",
data: {
datasets: [{
label: "Price vs Sales",
data: [
{ x: 10, y: 100 },
{ x: 20, y: 85 },
{ x: 30, y: 72 },
{ x: 40, y: 60 },
{ x: 50, y: 45 }
],
backgroundColor: "rgba(54, 162, 235, 0.7)",
borderColor: "rgb(54, 162, 235)",
pointRadius: 6,
pointHoverRadius: 10
}]
},
options: {
responsive: true,
scales: {
x: {
type: "linear",
position: "bottom",
title: { display: true, text: "Price ($)" },
beginAtZero: true
},
y: {
type: "linear",
title: { display: true, text: "Sales (units)" },
beginAtZero: true
}
},
plugins: {
title: { display: true, text: "Price vs Sales" },
tooltip: {
callbacks: {
label: function(context) {
return "Price: $" + context.parsed.x + ", Sales: " + context.parsed.y;
}
}
}
}
}
});What’s happening here:
type: "scatter"tells Chart.js to use the scatter renderer- Each data point is an object with
xandyproperties — not an array of values - Both axes have
type: "linear"because both are numeric (not categories) - The tooltip callback formats the display to show both x and y values clearly
Look at the data pattern: as price increases from $10 to $50, sales decrease from 100 to 45. This is a negative correlation — the chart makes this relationship obvious in one glance.
Why Both Axes Must Be Linear
If you forget to set type: "linear" on the x-axis, Chart.js treats it as a category axis. It will space the points evenly regardless of their x values — completely destroying the visual relationship. Try it yourself: remove that line and watch the points rearrange themselves into equal spacing. The pattern disappears.
Multi-Series Scatter: Comparing Groups
Often you want to compare two groups on the same plot — for example, Product A vs Product B:
datasets: [
{
label: "Product A",
data: [
{ x: 10, y: 100 },
{ x: 20, y: 85 },
{ x: 30, y: 72 }
],
backgroundColor: "rgba(54, 162, 235, 0.7)"
},
{
label: "Product B",
data: [
{ x: 10, y: 80 },
{ x: 20, y: 75 },
{ x: 30, y: 70 }
],
backgroundColor: "rgba(255, 99, 132, 0.7)"
}
]Product A drops steeply with price increases. Product B is flatter. This tells you Product A customers are more price-sensitive — a valuable business insight from a single chart.
Connecting Scatter Points with a Line
Sometimes you want to show both the points and the path between them (in order):
datasets: [{
label: "Connected Points",
data: [
{ x: 10, y: 100 },
{ x: 20, y: 85 },
{ x: 30, y: 72 }
],
showLine: true,
borderColor: "rgb(54, 162, 235)",
backgroundColor: "rgba(54, 162, 235, 0.1)",
tension: 0.4
}]Critical: The data must be sorted by x when using showLine: true. The line connects points in array order, not x-value order. If your data is [{x: 50, y: 45}, {x: 10, y: 100}], the line will draw from right to left, creating a zigzag mess.
Generating Random Scatter Data for Testing
When building scatter charts, you often need test data. Here’s a utility function:
function generateScatterData(count, xMin, xMax, yMin, yMax) {
return Array.from({ length: count }, function() {
return {
x: Math.random() * (xMax - xMin) + xMin,
y: Math.random() * (yMax - yMin) + yMin
};
});
}
// Generate 50 points between x: 0-100, y: 0-1000
var testData = generateScatterData(50, 0, 100, 0, 1000);Bubble Chart
A bubble chart works exactly like a scatter plot, but each point has a third value: radius (r). This lets you visualize three dimensions at once:
- x-axis: One variable (e.g., price)
- y-axis: Another variable (e.g., sales)
- Radius: A third variable (e.g., market share)
Think of it as a scatter plot where bigger bubbles represent more important or larger values.
new Chart(ctx, {
type: "bubble",
data: {
datasets: [{
label: "Market Analysis",
data: [
{ x: 10, y: 100, r: 8 },
{ x: 20, y: 85, r: 12 },
{ x: 30, y: 72, r: 15 },
{ x: 40, y: 60, r: 10 },
{ x: 50, y: 45, r: 20 }
],
backgroundColor: "rgba(54, 162, 235, 0.6)",
borderColor: "rgb(54, 162, 235)",
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
x: {
type: "linear",
position: "bottom",
title: { display: true, text: "Price ($)" },
beginAtZero: true
},
y: {
title: { display: true, text: "Sales (units)" },
beginAtZero: true
}
},
plugins: {
title: { display: true, text: "Market Analysis" },
tooltip: {
callbacks: {
label: function(context) {
var d = context.raw;
return "Price: $" + d.x + ", Sales: " + d.y + ", Size: " + d.r;
}
}
}
}
}
});What’s different from scatter:
type: "bubble"instead of"scatter"- Each data point has three properties:
{ x, y, r } - The
rvalue controls the bubble radius in pixels - Tooltip callbacks access
context.rawto get all three values
Multi-Series Bubble Charts
Compare different categories — for example, products from different years:
datasets: [
{
label: "2024 Products",
data: [
{ x: 30, y: 200, r: 14 },
{ x: 50, y: 150, r: 10 }
],
backgroundColor: "rgba(54, 162, 235, 0.6)"
},
{
label: "2023 Products",
data: [
{ x: 25, y: 180, r: 12 },
{ x: 45, y: 130, r: 8 }
],
backgroundColor: "rgba(255, 99, 132, 0.6)"
}
]Scaling Bubble Radii Correctly
This is the most common challenge with bubble charts. If your data has market share values like 0.05, 0.12, 0.30 (5%, 12%, 30%), those numbers are too small to see as pixel radii. You need to scale them:
var maxRadius = 30; // Maximum bubble size in pixels
var maxValue = Math.max(...data.map(function(d) { return d.marketShare; }));
var scale = maxRadius / maxValue;
var scaledData = data.map(function(d) {
return {
x: d.price,
y: d.sales,
r: d.marketShare * scale
};
});Rule of thumb: Bubble radii between 5 and 30 pixels work well on most charts. Smaller than 5 and they’re hard to see. Larger than 30 and they overlap too much.
Bubble Interaction Options
Bubble charts give you extra interaction controls because bubbles can overlap:
datasets: [{
// ... data
backgroundColor: "rgba(54, 162, 235, 0.4)", // Semi-transparent
borderColor: "rgb(54, 162, 235)",
borderWidth: 2,
hoverBackgroundColor: "rgba(54, 162, 235, 0.8)",
hoverBorderColor: "rgb(54, 162, 235)",
hoverBorderWidth: 3,
hoverRadius: 4, // Extra radius on hover
hitRadius: 10 // Makes small bubbles easier to hover
}]The hitRadius option is especially important. Small bubbles are hard to hover over precisely. Increasing hitRadius creates an invisible detection zone around each bubble, making interaction easier.
Axis Configuration for Scatter and Bubble
Both chart types share the same axis needs — two linear axes with proper titles and scaling:
scales: {
x: {
type: "linear",
position: "bottom",
min: 0,
max: 100,
title: { display: true, text: "X Variable", font: { size: 14 } },
ticks: {
stepSize: 10,
callback: function(value) { return value + " units"; }
},
grid: { color: "rgba(0,0,0,0.05)" }
},
y: {
type: "linear",
position: "left",
title: { display: true, text: "Y Variable" },
beginAtZero: true,
grid: { color: "rgba(0,0,0,0.05)" }
}
}When to use beginAtZero: For scatter plots, it depends on your data. If all x values are between 50 and 100, setting beginAtZero wastes chart space — the points will be crammed into the top-right corner. Instead, omit beginAtZero and let Chart.js auto-scale to your data range.
Trend Line (Linear Regression)
Chart.js doesn’t include built-in regression lines, but you can compute one and draw it with a custom plugin. This is a powerful feature for data analysis.
Computing the Regression
Linear regression finds the line that best fits your data points. The formula gives you a slope (how steep the line is) and an intercept (where it crosses the y-axis):
function linearRegression(data) {
var n = data.length;
var sumX = data.reduce(function(s, d) { return s + d.x; }, 0);
var sumY = data.reduce(function(s, d) { return s + d.y; }, 0);
var sumXY = data.reduce(function(s, d) { return s + d.x * d.y; }, 0);
var sumX2 = data.reduce(function(s, d) { return s + d.x * d.x; }, 0);
var slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
var intercept = (sumY - slope * sumX) / n;
return { slope: slope, intercept: intercept };
}Drawing the Trend Line with a Plugin
Once you have the slope and intercept, draw the line using a custom plugin:
var regressionPlugin = {
id: "regression",
afterDraw: function(chart) {
var dataset = chart.data.datasets[0].data;
if (!dataset || dataset.length < 2) return;
var reg = linearRegression(dataset);
var xScale = chart.scales.x;
var yScale = chart.scales.y;
chart.ctx.beginPath();
chart.ctx.strokeStyle = "red";
chart.ctx.lineWidth = 2;
chart.ctx.setLineDash([5, 5]);
chart.ctx.moveTo(xScale.left, yScale.getPixelForValue(reg.slope * xScale.min + reg.intercept));
chart.ctx.lineTo(xScale.right, yScale.getPixelForValue(reg.slope * xScale.max + reg.intercept));
chart.ctx.stroke();
chart.ctx.setLineDash([]);
}
};What this plugin does:
- After the chart draws (
afterDraw), it computes the regression line from the data - It gets the pixel coordinates of the left and right edges of the chart area
- It draws a dashed red line from the regression value at the left edge to the value at the right edge
Why a plugin? Chart.js renders on Canvas, not DOM. You can’t add a line element with CSS. Custom plugins hook into the Canvas drawing pipeline, letting you draw anything — lines, annotations, background colors, logos — directly onto the chart surface.
Common Mistakes
1. Scatter Points Not Visible
If your points are outside the visible area, either the axis bounds or beginAtZero is pushing them out of view. Check your data range and adjust min/max on the scales.
2. Bubble Radius Too Large or Too Small
Bubble r values are pixel units, not data units. A value of r: 100 creates a massive bubble that overlaps everything. Scale your data so radii fall between 5–30 pixels.
3. Using showLine: true Without Sorting Data
The line connects points in array index order, not x-value order. Always sort:
data.sort(function(a, b) { return a.x - b.x; });4. Forgetting type: "linear" on Axes
Scatter charts need both axes to have type: "linear". Without it, Chart.js uses the default category axis, spacing points evenly regardless of their numeric values.
5. Overlapping Data Points
With dense datasets, points pile on top of each other. Solutions: reduce point radius, use semi-transparent colors (rgba(..., 0.3) so overlaps darken), or use a heatmap approach instead of a scatter chart.
6. Bubble Radius Must Be Positive
The r value must always be positive (greater than 0). If your data includes zero or negative values, use Math.abs() or add a minimum offset before scaling.
Practice Questions
What is the key difference between a scatter chart’s axes and a bar chart’s axes? A scatter chart uses numeric axes on both x and y (
type: "linear"), while a bar chart uses a category axis for x and a linear axis for y.How many variables does a bubble chart visualize, and what does each represent? Three: x (horizontal position), y (vertical position), and r (bubble radius). The radius encodes the magnitude of a third variable.
Why might you set
hitRadiusto a larger value on a bubble chart? To make small bubbles easier to hover over. The hit radius extends the detection zone beyond the visible bubble edge.What happens if you use
showLine: trueon unsorted scatter data? The line connects points in array order rather than x-value order, creating a zigzag pattern that doesn’t represent the actual relationship.How do you compute a trend line for scatter data in Chart.js? Calculate the linear regression (slope and intercept) from the data points, then draw the line using a custom plugin that hooks into the Canvas drawing pipeline.
Challenge
Build a scatter chart that analyzes the relationship between advertising spend (x) and revenue (y) for 20 products. Add a regression line plugin. Color-code points: green if revenue exceeds spend by 3x, yellow if 2-3x, red if below 2x. Show custom tooltips with ROI percentage.
FAQ
Try It Yourself: Market Analysis Bubble Chart
Experiment with three product categories and see how price, sales, and market share interact:
<!DOCTYPE html>
<html>
<head>
<title>Market Analysis Bubble Chart</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.chart-box { width: 700px; max-width: 100%; margin: 20px 0; }
</style>
</head>
<body>
<h2>Market Analysis — Price vs Sales vs Market Share</h2>
<p>Bubble size = market share. Hover for details.</p>
<div class="chart-box">
<canvas id="bubbleChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
var ctx = document.getElementById("bubbleChart").getContext("2d");
new Chart(ctx, {
type: "bubble",
data: {
datasets: [
{
label: "Electronics",
data: [
{ x: 50, y: 200, r: 20 },
{ x: 80, y: 150, r: 15 },
{ x: 120, y: 100, r: 25 },
{ x: 200, y: 60, r: 18 }
],
backgroundColor: "rgba(255, 99, 132, 0.6)",
borderColor: "rgb(255, 99, 132)"
},
{
label: "Clothing",
data: [
{ x: 20, y: 300, r: 30 },
{ x: 40, y: 250, r: 22 },
{ x: 60, y: 180, r: 28 },
{ x: 100, y: 120, r: 20 }
],
backgroundColor: "rgba(54, 162, 235, 0.6)",
borderColor: "rgb(54, 162, 235)"
},
{
label: "Food",
data: [
{ x: 5, y: 500, r: 35 },
{ x: 10, y: 450, r: 32 },
{ x: 15, y: 400, r: 30 },
{ x: 25, y: 350, r: 28 }
],
backgroundColor: "rgba(75, 192, 192, 0.6)",
borderColor: "rgb(75, 192, 192)"
}
]
},
options: {
responsive: true,
scales: {
x: {
type: "linear",
position: "bottom",
title: { display: true, text: "Price ($)" },
beginAtZero: true
},
y: {
title: { display: true, text: "Sales (units)" },
beginAtZero: true
}
},
plugins: {
title: { display: true, text: "Market Analysis by Category" },
tooltip: {
callbacks: {
label: function(context) {
var d = context.raw;
return context.dataset.label +
": $" + d.x + ", " + d.y + " units, share: " +
Math.round(d.r * 3.33) + "%";
}
}
}
}
}
});
</script>
</body>
</html>What to observe: Electronics are high price, low sales, medium share. Clothing is mid-price, mid-sales. Food is low price, high sales, large share. The bubble sizes make the third variable (market share) instantly comparable — food has the largest bubbles.
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| Chart.js Advanced | Animation, custom plugins, tooltip styling, interactions |
| Chart.js Reference & Cheatsheet | Complete API reference for daily development |
Related topics: JavaScript array methods like map() and reduce() are essential for preparing scatter data. D3.js is an alternative for custom visualizations beyond what Chart.js offers.
What’s Next
Congratulations on completing this Chartjs Scatter Bubble tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro