Scatter Plot — HTML CSS JS SVG Scatter Chart

Scatter Plot · Charts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Data-to-pixel scaling
sx()/sy() map [x, y] data into the padded plot area, with y inverted so up means more.
Least-squares trend line
A real linear regression computes the best-fit slope and intercept from the data.
Toggleable overlay
A checkbox shows/hides the trend line, which is drawn under the points so dots stay readable.
Gridlines and axes
Horizontal gridlines and solid x/y axes frame the plot for easy reading.
Per-point tooltips
Hovering any point shows its exact x and y values at the cursor.
Event delegation
One mousemove listener handles every point via closest().
Configurable axis ranges
XMAX/YMAX constants set the domains; change them for any data range.
Data-driven & no library
Renders entirely from a DATA array of [x, y] pairs — zero dependencies.

About this UI Snippet

Scatter Plot — SVG Point Plot with a Least-Squares Best-Fit Trend Line

Screenshot of the Scatter Plot snippet rendered live

A scatter plot reveals the relationship between two numeric variables — whether they correlate, how tightly, and in which direction. It's the chart you reach for to answer "does X affect Y?" This snippet builds a complete scatter plot in plain HTML, CSS, SVG, and vanilla JavaScript, including an optional least-squares trend line computed from the data itself, gridlines, axes, and hover tooltips — with no charting library.

Plotting points in pixel space

Each data point is an [x, y] pair, and two scaling functions turn data into pixels: sx(x) maps the x value across the padded plot width, and sy(y) maps the y value up the height — inverted, because SVG's y grows downward while the chart's grows upward. Every point becomes a small SVG circle at the scaled coordinates. Because points are real DOM nodes, each one is individually hoverable, which is what lets the tooltip report the exact values behind any dot.

A real best-fit line, not a hand-drawn one

The trend line is computed with least-squares linear regression — the standard method for fitting a straight line to scattered points. fit() accumulates the sums of x, y, x·y, and x² across the dataset, then solves for the slope and intercept with the closed-form formulas. The result is the line that minimises the squared vertical distance to every point: the genuine statistical trend, not an eyeballed approximation. Drawing it from the regression means it updates correctly for any data you feed in.

Toggleable and drawn underneath

A checkbox toggles the trend line on and off, re-rendering the plot. The line is drawn before the points so the dots sit on top of it, keeping them readable and hoverable where the line crosses through the cloud. The line uses a dashed stroke in a contrasting colour so it reads clearly as an overlay rather than as data.

Gridlines, axes, and tooltips

Faint horizontal gridlines and solid x/y axes frame the plot so positions are easy to judge. Hovering any point shows a tooltip with its exact x and y values, positioned at the cursor via getBoundingClientRect, with a single delegated mousemove listener handling every point through closest('.scp-pt'). The tooltip is pointer-events: none so it never steals its own hover.

Data-driven and drop-in

The plot renders from a DATA array of [x, y] pairs, with the axis maxima set by two constants. Swap in your own pairs and the points, scaling, and regression line all follow. Because it's dependency-free SVG, it's crisp at any size, themeable with CSS, and a clear reference for both the data-to-pixel scaling and the linear-regression math that power correlation visualisations.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to re-derive the regression formula yourself to understand it fully. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly why the least-squares fit() function's slope formula uses the sums of x, y, x times y, and x squared, and why that closed-form solution minimizes the total squared vertical distance to every point. The same assistant is useful for optimizing it — ask whether recomputing fit() and re-rendering the whole SVG on every checkbox toggle is wasteful for a much larger dataset, or whether the single delegated mousemove listener would still be efficient with thousands of points instead of seventeen. It is just as useful for extending the effect — ask it to add a second series with a different point color, support clicking a point to highlight related rows in a table, or compute and display the correlation coefficient alongside the trend line. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an SVG scatter plot with a computed trend line in plain HTML, CSS, and JavaScript using only inline SVG elements created with createElementNS — no canvas, no charting library.

Requirements:
- An SVG viewBox-scaled plot area with two pure functions, one mapping a data x value into pixel space across the padded plot width, and one mapping a data y value into pixel space up the plot height (inverted, since SVG y grows downward while chart y should grow upward).
- Render the dataset (an array of [x, y] pairs) as individual SVG circle elements at the scaled coordinates, plus horizontal gridlines and solid x/y axis lines built the same way.
- Compute an actual least-squares linear regression line from the data itself — accumulate the sums of x, y, x times y, and x squared across all points, solve the closed-form slope and intercept formulas, and draw the resulting line as a dashed SVG line spanning the full x domain. Do not hand-pick or hardcode the line's endpoints.
- Draw the trend line before the data points in the SVG so the points render on top of it and stay hoverable even where the line crosses through them.
- Add a checkbox that toggles the trend line's visibility and triggers a re-render.
- Implement hover tooltips using a single delegated mousemove listener on the SVG (using closest() to detect which point, if any, is under the cursor) rather than one listener per point, positioning the tooltip at the cursor with getBoundingClientRect and making it pointer-events: none so it cannot intercept its own hover.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA scatter plot renders with seventeen points and a best-fit trend line through them.
  2. 2
    Toggle the trend lineUse the checkbox to show or hide the least-squares regression line.
  3. 3
    Hover a pointMove over any dot to see its exact x and y values in a tooltip.
  4. 4
    Swap in your dataReplace the DATA array with your own [x, y] pairs and set XMAX/YMAX to your axis ranges.
  5. 5
    Restyle itChange point size, colour, the trend-line dash pattern, or the gridline density.
  6. 6
    Wire to an APIFetch your paired values, map them into [x, y] form, and call render() to draw the live chart.

Real-world uses

Common Use Cases

Correlation analysis
Show whether two metrics move together — pair with a bubble chart to add a size dimension.
A/B test and experiment data
Plot input vs. outcome to spot relationships, alongside a line chart for time series.
Scientific and lab results
Visualise measurement pairs with a statistically honest trend line.
Pricing and demand curves
Map price against units sold to see elasticity.
Education and performance data
Relate study time to scores, or effort to results, on a dashboard.
Learning linear regression
A clear reference for least-squares fitting and chart scaling — compare with a bar chart.

Got questions?

Frequently Asked Questions

It uses least-squares linear regression. fit() accumulates the sums of x, y, x·y, and x² across all points, then applies the closed-form formulas for slope m = (n·Σxy − Σx·Σy) / (n·Σx² − (Σx)²) and intercept b = (Σy − m·Σx) / n. The result is the line that minimises the total squared vertical distance to the points — the genuine statistical best fit, recomputed from whatever data you supply.

Draw order in SVG is paint order — later elements sit on top. Drawing the trend line first means the data points render over it, so where the line passes through the cloud the dots stay visible and hoverable rather than being hidden behind the line. The dashed stroke and contrasting colour further distinguish the line as an overlay rather than data.

Set the XMAX and YMAX constants to the maximum values of your two variables (or a bit above, for headroom). The scaling functions divide by these to map data into the plot area. If your data has a non-zero minimum, subtract the min before scaling and divide by the range (max − min) so the plot uses the full width and height.

As written it assumes a 0-based origin. For negative values, shift the domain: compute min and max, then map (value − min) / (max − min) across the axis, and draw the zero line wherever 0 falls in that range. The point and line drawing stay the same — only the scaling functions change to account for the offset origin.

In React, hold the data and trend-toggle in useState and render circles from .map(), or run render() in a useEffect with a ref; in Vue, use v-for or a template ref with onMounted; in Angular, use *ngFor or ViewChild with ngAfterViewInit. The scaling and regression math is framework-agnostic and ports unchanged — only state and event wiring move into the framework.