롤리팝 차트

Lollipop chart

두꺼운 막대 대신 얇은 줄기와 끝의 원으로 값을 표시하는 막대 차트의 변형. 잉크는 줄이고 끝점의 정확한 위치는 그대로 남깁니다.

다른 이름: Stem chart
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
body{display:block}
.viz{position:relative;width:100%;height:100%}
svg{display:block;width:100%;height:100%;overflow:visible}
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 CATS = ['A','B','C','D','E','F'];
var cur = CATS.map(function(){ return 0; });
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=22,padR=30,padT=10,padB=8;
  var pw=W-padL-padR, ph=H-padT-padB;
  var n=CATS.length;
  var rowH = ph/n;
  var fs = Math.max(8,Math.min(11,W/40));
  var axis = se('line');
  sa(axis,{ x1:padL, y1:padT, x2:padL, y2:padT+ph, stroke:'var(--line)', 'stroke-width':1 });
  svg.appendChild(axis);
  CATS.forEach(function(name,i){
    var y = padT+rowH*(i+0.5);
    var x = padL+(cur[i]/100)*pw;
    var lab = se('text');
    sa(lab,{ x:padL-6, y:y+3, 'text-anchor':'end', fill:'var(--muted)', 'font-size':fs, 'font-weight':600 });
    lab.textContent = name;
    svg.appendChild(lab);
    var stem = se('line');
    sa(stem,{ x1:padL, y1:y, x2:x, y2:y, stroke:'var(--accent)', 'stroke-width':2 });
    svg.appendChild(stem);
    var dot = se('circle');
    sa(dot,{ cx:x, cy:y, r:5, fill:'var(--accent)' });
    svg.appendChild(dot);
    var val = se('text');
    sa(val,{ x:x+8, y:y+3, fill:'var(--fg)', 'font-size':fs, 'font-weight':700 });
    val.textContent = Math.round(cur[i]);
    svg.appendChild(val);
  });
}

function animate(target){
  var from = cur.slice();
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/700);
    var e = 1-Math.pow(1-p,3);
    cur = from.map(function(v,i){ return v+(target[i]-v)*e; });
    draw();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
animate(CATS.map(function(){ return R(20,92); }));
setInterval(function(){ animate(CATS.map(function(){ return R(12,95); })); }, 3400);

막대 차트와 같은 질문("어느 항목이 더 큰가")에 같은 정확도로 답하지만, 굵은 사각형 대신 가는 줄기 하나만 그립니다. 에드워드 터프티가 말한 "데이터-잉크 비율"을 극단으로 끌어올린 형태로 볼 수 있습니다 — 값을 나타내는 데 꼭 필요한 건 끝점의 위치뿐이고, 그 사이를 두껍게 채우는 건 잉크만 쓰고 정보는 더하지 않습니다.

항목이 많거나(15개 이상) 값이 서로 비슷해서 막대들이 다닥다닥 붙으면, 두꺼운 막대들은 경계가 뭉개져 하나의 덩어리로 보이기 쉽지만 얇은 줄기는 간격이 좁아도 각자 구분됩니다. 그래서 카테고리가 많은 랭킹 목록에 특히 잘 맞습니다.

줄기를 너무 굵게 그리면 결국 얇은 막대 차트가 되어버려 애초에 이 형태를 쓰는 이유(잉크 절약, 조밀한 항목에서도 구분)가 사라집니다. 끝의 원도 너무 작으면 클릭·hover 타겟이나 시각적 강조점 역할을 못 하므로, 줄기보다 눈에 띄게 키워야 합니다.

언제 쓰나

범주가 많거나(랭킹 목록 등) 값이 서로 가까워 막대가 뭉쳐 보일 때. 막대 자체의 굵기가 브랜드·강조 요소로 필요하면 일반 막대 차트를 쓰세요.