파이 · 도넛 차트

Pie and donut chart

전체를 100%로 두고 조각의 각도(면적)로 구성비를 보여주는 차트. "부분과 전체" 관계 하나만 잘 보여줍니다.

다른 이름: Pie chartDonut chart원형 차트
···
html
<div class="viz">
  <svg id="svg"></svg>
  <div class="legend" id="lg"></div>
</div>
css
:root{--viz-4:#e8a23a;--viz-5:#4098d7}
body{display:block}
.viz{display:flex;flex-direction:column;width:100%;height:100%}
svg{flex:1;min-height:0;display:block;width:100%;overflow:visible}
.legend{flex:none;display:flex;flex-wrap:wrap;gap:4px 10px;justify-content:center;padding:3px 4px;font:600 9px/1 system-ui,sans-serif;color:var(--muted)}
.legend b{display:inline-flex;align-items:center;gap:3px;font-weight:600}
.legend i{width:7px;height:7px;border-radius:2px;display:inline-block}
js
var svg = document.getElementById('svg');
var lg = document.getElementById('lg');
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 LABELS = ['A','B','C','D','E'];
var COLORS = ['var(--accent)','var(--viz-4)','var(--accent-3)','var(--viz-5)','var(--accent-2)'];
lg.innerHTML = LABELS.map(function(l,i){ return '<b><i style="background:'+COLORS[i]+'"></i>'+l+'</b>'; }).join('');

var cur = [30,24,20,15,11];
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 slicePath(cx,cy,r0,r1,a0,a1){
  var x0o=cx+Math.cos(a0)*r1, y0o=cy+Math.sin(a0)*r1;
  var x1o=cx+Math.cos(a1)*r1, y1o=cy+Math.sin(a1)*r1;
  var x1i=cx+Math.cos(a1)*r0, y1i=cy+Math.sin(a1)*r0;
  var x0i=cx+Math.cos(a0)*r0, y0i=cy+Math.sin(a0)*r0;
  var large = (a1-a0) > Math.PI ? 1 : 0;
  return 'M'+x0o+','+y0o+' A'+r1+','+r1+' 0 '+large+' 1 '+x1o+','+y1o+' L'+x1i+','+y1i+' A'+r0+','+r0+' 0 '+large+' 0 '+x0i+','+y0i+' Z';
}

function draw(){
  svg.innerHTML = '';
  var cx=W/2, cy=H/2;
  var r1 = Math.max(12, Math.min(W,H)/2 - 8);
  var r0 = r1*0.58;
  var fs = Math.max(9, Math.min(16, Math.min(W,H)/13));
  var total = cur.reduce(function(a,b){ return a+b; },0);
  var ga = 0.02;
  var a0 = -Math.PI/2;
  cur.forEach(function(v,i){
    var frac = v/total;
    var a1 = a0 + frac*Math.PI*2;
    var s0 = a0+ga, s1 = Math.max(s0, a1-ga);
    var p = se('path');
    sa(p,{d:slicePath(cx,cy,r0,r1,s0,s1), fill:COLORS[i]});
    svg.appendChild(p);
    if (frac >= 0.08){
      var mid=(a0+a1)/2, lr=(r0+r1)/2;
      var t = se('text');
      sa(t,{x:cx+Math.cos(mid)*lr, y:cy+Math.sin(mid)*lr+fs*0.28,'text-anchor':'middle',fill:'#fff','font-size':fs*0.62,'font-weight':700});
      t.textContent = Math.round(frac*100)+'%';
      svg.appendChild(t);
    }
    a0=a1;
  });
  var ct = se('text');
  sa(ct,{x:cx,y:cy-1,'text-anchor':'middle',fill:'var(--fg)','font-size':fs*1.15,'font-weight':800});
  ct.textContent = Math.round(total);
  svg.appendChild(ct);
  var cl = se('text');
  sa(cl,{x:cx,y:cy+fs*0.95,'text-anchor':'middle',fill:'var(--muted)','font-size':fs*0.5,'font-weight':700});
  cl.textContent='TOTAL';
  svg.appendChild(cl);
}

function animate(target){
  var from = cur.slice();
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/750);
    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();
setInterval(function(){
  var raw = LABELS.map(function(){ return R(6,32); });
  animate(raw);
}, 3600);

사람은 각도·면적보다 길이를 훨씬 정확하게 비교합니다. 그래서 조각이 4~5개를 넘거나 크기가 서로 비슷하면 어느 게 더 큰지 눈으로 가늠하기 어려워지고, 그 순간 막대 차트가 더 정직한 선택이 됩니다. 실무 기준으로는 슬라이스 5~6개, 그 이상은 "기타"로 묶거나 막대로 바꾸세요.

도넛(가운데를 뚫는 것)은 파이와 정보량은 같지만 가운데 빈 공간에 합계 같은 핵심 숫자를 하나 더 보여줄 수 있어 실무에서 더 많이 쓰입니다. 조각은 큰 것부터 시계 12시 방향에서 시작해 시계 방향으로 배치하는 게 관례입니다.

3D 기울임(원근 파이)은 쓰지 마세요 — 각도가 왜곡되어 앞쪽 조각이 실제보다 커 보입니다. 조각 사이에는 서지스 색 틈을 줘서 테두리 없이도 구분되게 합니다.

언제 쓰나

조각이 5~6개 이하이고 "전체 중 이게 몇 %"라는 한 가지 질문에만 답하면 될 때.