webdevpuneet.com

Bubble Chart — HTML CSS JS SVG Bubble Chart

Bubble Chart · Charts · Plain HTML, CSS & JS · Live preview

CategoryCharts

Copy & paste

Source Code

<div class="bbc-card">
  <div class="bbc-head">
    <h3>Markets — reach vs. growth</h3>
    <span class="bbc-sub">bubble size = revenue</span>
  </div>
  <svg class="bbc-svg" id="bbcSvg" viewBox="0 0 360 240" role="img" aria-label="Bubble chart of markets"></svg>
  <div class="bbc-axislabels"><span>Reach →</span><span class="bbc-y">↑ Growth</span></div>
  <div class="bbc-tip" id="bbcTip" hidden></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Three data dimensions
Each bubble encodes x position, y position, and size — three variables in one view.
Data-to-pixel scaling
sx()/sy() map data values into the padded plot area, with y inverted so up means more.
Area-proportional sizing
Radius uses sqrt scaling so a bubble's perceived area — not its radius — matches the value.
Gridlines and axes
Faint gridlines and solid x/y axes give the plot a readable frame.
Largest-first draw order
Bubbles are sorted big-to-small so small ones paint on top and stay hoverable.
Centred labels
Each bubble carries a label with pointer-events:none so it never blocks the hover.
Three-value tooltips
Hovering shows the name plus all three metrics, positioned at the cursor.
Data-driven & no library
Renders entirely from a DATA array in plain HTML/CSS/SVG/JS — zero dependencies.

About this UI Snippet

Bubble Chart — Three-Variable SVG Scatter with Area-Proportional Bubbles

Screenshot of the Bubble Chart snippet rendered live

A bubble chart packs three dimensions of data into one view: two via the x and y position of each point, and a third via the size of the bubble. It's the right chart when you want to compare items across two metrics while also weighting them by a third — markets by reach and growth, sized by revenue; products by price and rating, sized by sales. This snippet builds a complete bubble chart in plain HTML, CSS, SVG, and vanilla JavaScript, with gridlines, axes, labelled bubbles, and hover tooltips — no charting library.

Mapping data space to pixel space

The core of any plotted chart is the scaling functions. sx(x) maps a data value on the x-axis (0–100 here) into a pixel x-coordinate inside the padded plot area, and sy(y) does the same for y — but inverted, because SVG's y-axis grows downward while a chart's grows upward. Getting that inversion right is what makes "up" mean "more." The padding keeps bubbles and axes off the very edge so labels and large bubbles aren't clipped.

Area-proportional sizing, not radius-proportional

The third dimension — bubble size — uses sqrt scaling: the radius is proportional to the square root of the value, not the value itself. This is the single most important detail in a bubble chart, because a circle's visual weight is its area, and area grows with the square of the radius. If you scaled the radius linearly, a value twice as large would look four times as big and badly mislead the reader. Square-root scaling makes the perceived area proportional to the value, which is the honest way to size bubbles.

Gridlines, axes, and draw order

The chart draws faint gridlines and solid x/y axes first, then the bubbles on top. Bubbles are sorted largest-first before drawing so smaller bubbles paint over big ones and stay hoverable — otherwise a large bubble would sit on top and swallow the clicks meant for a small one behind it. Each bubble carries a short label centred inside it (text-anchor: middle), and the label is pointer-events: none so it never blocks the bubble's own hover.

Tooltips with all three values

Hovering a bubble shows a tooltip with the name and all three data values — the x metric, the y metric, and the size metric — so the reader can read the exact numbers behind the position and size. The tooltip follows the cursor using getBoundingClientRect to translate page coordinates into card offsets, and a single delegated mousemove listener on the SVG handles every bubble via closest('.bbc-bubble').

Data-driven and drop-in

Everything renders from a DATA array of { name, x, y, r, color }. Swap in your own three-variable data and the positions, sizes, gridlines, and tooltips all follow. Because it's dependency-free SVG, it scales crisply, themes with CSS, and is a clear reference for the data-to-pixel scaling and area-proportional sizing that underpin every scatter and bubble visualisation.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA bubble chart renders with five market bubbles positioned by reach and growth, sized by revenue.
  2. 2
    Hover a bubbleMove over any bubble to see a tooltip with its name and all three values (x, y, and size).
  3. 3
    Swap in your dataReplace the DATA array with { name, x, y, r, color } items on your own 0–100 scales.
  4. 4
    Adjust the scalesIf your data isn't 0–100, change the divisor in sx()/sy() or normalise your values first.
  5. 5
    Tune bubble sizesEdit the base size and multiplier in radius() to make bubbles larger or smaller overall.
  6. 6
    Wire to an APIFetch your records, map them into the DATA shape, and call render() to draw the live chart.

Real-world uses

Common Use Cases

Market and segment analysis
Plot markets by two metrics sized by a third — pair with a bar chart for single-metric views.
Product portfolio mapping
Position products by price and rating, sized by sales, alongside a scatter plot for two-variable data.
Risk vs. reward quadrants
Map investments or projects across two axes with a weighting dimension.
Performance dashboards
Show teams or campaigns by effort and impact next to a stat comparison card.
Scientific and survey data
Visualise three-variable datasets without a heavy plotting library.
Learning chart scaling
A reference for data-to-pixel mapping and area-proportional sizing — compare with a line chart.
Related: Control Chart with Upper/Lower Control Limits
See the Control Chart with Upper/Lower Control Limits for a related charts pattern worth pairing with this one.
Related: Violin Plot Chart
See the Violin Plot Chart for a related charts pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A circle's visual weight is its area, and area grows with the square of the radius. If you set radius directly proportional to the value, a value twice as large would render with four times the area and mislead the reader. Scaling the radius by the square root of the value makes the perceived area proportional to the value — the honest, standard way to size bubbles.

SVG coordinates grow downward (y=0 is the top), but a chart's y-axis should grow upward (more = higher). sy() subtracts the scaled value from the bottom of the plot area so larger data values map to smaller pixel-y values, placing them higher on screen. Getting this inversion right is what makes "up" mean "more" on the chart.

If a big bubble is drawn last, it sits on top and intercepts hover/click events meant for smaller bubbles behind it. Sorting the data largest-first before drawing puts small bubbles on top in the paint order, so every bubble stays individually hoverable. The labels are pointer-events:none for the same reason — so they never block their own bubble.

The scaling functions sx()/sy() assume a 0–100 domain. Either change the divisor (100) to your data's maximum, or normalise your values to 0–100 before plotting. For axes with a non-zero minimum, subtract the min and divide by the range (max − min). The radius() function already normalises against the dataset's max, so bubble sizes adapt automatically.

In React, hold the data in useState and render circles/labels from .map(), or run the imperative render() in a useEffect with a ref to the SVG; in Vue, use v-for or a template ref with onMounted; in Angular, use *ngFor or ViewChild with ngAfterViewInit. The sx()/sy()/radius() scaling math is framework-agnostic and ports unchanged.