Bubble chart

버블 차트

A scatter plot with a third variable added as circle size, so x, y, and magnitude all read at once.

Also known as: Bubble plot
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
:root{--viz-4:#e8a23a;--viz-5:#4098d7}
body{display:block}
.viz{position:relative;width:100%;height:100%}
svg{display:block;width:100%;height:100%;overflow:visible}
.axis{stroke:var(--line);stroke-width:1}
.ring{fill:none;stroke:var(--surface);stroke-width:2}
js
var svg = document.getElementById('svg');
var NS = 'http://www.w3.org/2000/svg';
function se(t){ return document.createElementNS(NS,t); }
function sa(e,o){ for (var k in o) e.setAttribute(k,o[k]); }
function R(a,b){ return a + Math.random()*(b-a); }

var COLORS = ['var(--accent)','var(--viz-4)','var(--viz-5)'];
var N = 11;
var items = [];
for (var i=0;i<N;i++) items.push({ x:R(0.1,0.9), y:R(0.1,0.9), v:R(6,90), c: COLORS[i%3], tx:0.5, ty:0.5, tv:40 });
items.forEach(function(it){ it.tx=it.x; it.ty=it.y; it.tv=it.v; });

var W=300, H=200;
function measure(){
  var r = svg.getBoundingClientRect();
  W = Math.max(r.width,10); H = Math.max(r.height,10);
  sa(svg,{ viewBox: '0 0 ' + W + ' ' + H });
}

function draw(){
  svg.innerHTML = '';
  var padL=16, padR=16, padT=14, padB=16;
  var pw=W-padL-padR, ph=H-padT-padB;
  var ax = se('line');
  sa(ax,{x1:padL,y1:H-padB,x2:W-padR,y2:H-padB,class:'axis'});
  svg.appendChild(ax);
  var ay = se('line');
  sa(ay,{x1:padL,y1:padT,x2:padL,y2:H-padB,class:'axis'});
  svg.appendChild(ay);
  var kmax = Math.min(pw,ph)*0.16;
  items.slice().sort(function(a,b){ return b.v-a.v; }).forEach(function(it){
    var r = Math.max(3, kmax*Math.sqrt(it.v/100));
    var cx = padL+it.x*pw, cy = padT+(1-it.y)*ph;
    var c = se('circle');
    sa(c,{cx:cx,cy:cy,r:r,fill:it.c,'fill-opacity':0.72});
    svg.appendChild(c);
    var ring = se('circle');
    sa(ring,{cx:cx,cy:cy,r:r,class:'ring'});
    svg.appendChild(ring);
  });
}

function animate(){
  var from = items.map(function(it){ return { x:it.x, y:it.y, v:it.v }; });
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/850);
    var e = 1-Math.pow(1-p,3);
    items.forEach(function(it,i){
      it.x = from[i].x+(it.tx-from[i].x)*e;
      it.y = from[i].y+(it.ty-from[i].y)*e;
      it.v = from[i].v+(it.tv-from[i].v)*e;
    });
    draw();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
setInterval(function(){
  items.forEach(function(it){ it.tx=R(0.1,0.9); it.ty=R(0.1,0.9); it.tv=R(6,92); });
  animate();
}, 3400);

The most common mistake is scaling radius linearly with value. We perceive a circle by its area, and area grows with the square of radius — a linear radius scale makes differences look far bigger than they are. The correct formula is radius = k × sqrt(value), so area (not radius) tracks the value.

Cramming more than three variables (x, y, size, plus color) into one chart gets dense fast. Cap grouping color at 2-3, and give overlapping bubbles some transparency plus a 2px surface-colored ring so they stay legible where they touch.

Small bubbles are hard to compare precisely by eye — if exact values matter, pair the chart with a table.

When to use

Use it to compare three continuous variables (x, y, size) at once. When size comparisons need to be precise, add a bar chart alongside.