버블 차트

Bubble chart

산점도에 원의 크기라는 세 번째 변수를 더한 차트. x·y·크기 세 값을 한 번에 보여줍니다.

다른 이름: 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);

가장 흔한 실수는 값에 "반지름"을 그대로 비례시키는 것입니다. 사람 눈은 원을 면적으로 지각하는데, 반지름을 선형으로 키우면 면적은 제곱으로 커져서 실제보다 훨씬 큰 차이로 보입니다. 올바른 공식은 반지름 = k × sqrt(값) 입니다 — 그래야 면적이 값에 비례합니다.

점이 3개를 넘는 변수(색까지 추가)를 한 차트에 담으면 정보가 너무 빽빽해집니다. 색은 그룹 2~3개까지만 쓰고, 겹치는 버블은 반투명 처리 + 서지스 색 테두리(2px 링)로 구분합니다.

작은 버블일수록 정확한 값 비교가 어려워지므로, 정밀 비교가 필요하면 버블 차트 옆에 표를 같이 두는 게 안전합니다.

언제 쓰나

세 개의 연속형 변수(x, y, 크기)를 동시에 비교하고 싶을 때. 크기 비교가 정밀해야 하면 막대 차트를 따로 두세요.