Skip to content
Chart.js Scatter & Bubble Charts Explained — Visualize Three Variables

Chart.js Scatter & Bubble Charts Explained — Visualize Three Variables

DodaTech Updated Jun 6, 2026 15 min read

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.

Prerequisites: You should understand Chart.js basics — the configuration object structure. Understanding line and bar charts helps but isn’t required.

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:

  1. type: "scatter" tells Chart.js to use the scatter renderer
  2. Each data point is an object with x and y properties — not an array of values
  3. Both axes have type: "linear" because both are numeric (not categories)
  4. 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:

  1. type: "bubble" instead of "scatter"
  2. Each data point has three properties: { x, y, r }
  3. The r value controls the bubble radius in pixels
  4. Tooltip callbacks access context.raw to 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:

  1. After the chart draws (afterDraw), it computes the regression line from the data
  2. It gets the pixel coordinates of the left and right edges of the chart area
  3. 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

  1. 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.

  2. 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.

  3. Why might you set hitRadius to 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.

  4. What happens if you use showLine: true on 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.

  5. 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

What is the difference between scatter and bubble charts?

Scatter charts visualize 2 numeric variables (x, y). Bubble charts add a third dimension — the radius r — encoding size as an additional variable. Use scatter for correlation analysis, bubble for three-variable comparison.

Can scatter charts use date values on the x-axis?

Yes, set x.type: "time" and use JavaScript Date objects or timestamps for x values. You’ll need the chartjs-adapter-date-fns adapter library.

How do I make overlapping scatter points visible?

Use semi-transparent colors: backgroundColor: "rgba(54, 162, 235, 0.3)". Areas with more overlap appear darker, revealing density patterns that solid colors hide.

What does hitRadius do exactly?

It extends the click/hover detection area around each point beyond the visible radius. A point with radius: 3 and hitRadius: 10 responds to hover within a 10px radius, even though it looks 3px wide.

Can bubble charts use negative values?

x and y can be negative, but the radius r must always be positive. Use Math.abs() or a minimum offset when scaling radius values.

How do I add custom annotations like a target line?

Use a custom plugin with the afterDraw hook. The plugin has full access to the Canvas 2D context and can draw lines, shapes, and text anywhere on the chart surface.

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

TutorialWhat You’ll Learn
Chart.js AdvancedAnimation, custom plugins, tooltip styling, interactions
Chart.js Reference & CheatsheetComplete 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