선버스트

Sunburst chart

중첩된 계층 구조를 도넛처럼 겹친 고리로 펼쳐서, 조각의 각도로 상위 항목 대비 비중을 보여주는 차트.

다른 이름: Ring chartRadial treemap방사형 트리맵
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
:root{--viz-4:#e8a23a}
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); }
function sum(arr){ return arr.reduce(function(a,b){ return a+b; },0); }

var ROOTS = [
  { color: 'var(--accent)' },
  { color: 'var(--viz-4)' },
  { color: 'var(--accent-3)' }
];
var LEAF_ROOT = [0,0,1,1,1,2,2];
var cur = LEAF_ROOT.map(function(){ return 20; });
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 arcPath(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 rMax = Math.max(30, Math.min(W,H)/2 - 8);
  var r0=rMax*0.26, r1=rMax*0.58, r2=rMax*0.98;
  var grand = sum(cur);
  var ga = 0.03;
  var a = -Math.PI/2;
  ROOTS.forEach(function(root,ri){
    var rootTotal = 0;
    cur.forEach(function(v,li){ if (LEAF_ROOT[li]===ri) rootTotal += v; });
    var a1 = a + (rootTotal/grand)*Math.PI*2;
    var s0=a+ga, s1=Math.max(s0,a1-ga);
    var p1 = se('path');
    sa(p1,{ d: arcPath(cx,cy,r0,r1,s0,s1), fill: root.color });
    svg.appendChild(p1);
    var la = a, idx = 0;
    cur.forEach(function(v,li){
      if (LEAF_ROOT[li]!==ri) return;
      var la1 = la + (v/rootTotal)*(a1-a);
      var lg = ga*0.5;
      var ls0=la+lg, ls1=Math.max(ls0,la1-lg);
      var p2 = se('path');
      sa(p2,{ d: arcPath(cx,cy,r1,r2,ls0,ls1), fill: root.color, 'fill-opacity': idx%2===0?1:0.55 });
      svg.appendChild(p2);
      la = la1; idx++;
    });
    a = a1;
  });
  var fs = Math.max(10, Math.min(18, rMax/5));
  var ct = se('text');
  sa(ct,{ x:cx, y:cy-1, 'text-anchor':'middle', fill:'var(--fg)', 'font-size':fs, 'font-weight':800 });
  ct.textContent = Math.round(grand);
  svg.appendChild(ct);
  var cl = se('text');
  sa(cl,{ x:cx, y:cy+fs*0.85, 'text-anchor':'middle', fill:'var(--muted)', 'font-size':fs*0.42, '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)/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(LEAF_ROOT.map(function(){ return R(8,45); }));
setInterval(function(){
  animate(LEAF_ROOT.map(function(){ return R(6,48); }));
}, 3600);

트리맵과 같은 질문("이 계층에서 어디가 큰가")에 답하지만, 트리맵이 사각형 면적을 쓰는 반면 선버스트는 각도를 씁니다. 안쪽 고리가 상위 레벨이고 바깥으로 갈수록 하위 레벨이며, 같은 부모를 공유하는 조각은 부모와 같은 색조의 명도 차이로 묶어야 "이게 저 부모 밑에 있다"는 관계가 한눈에 보입니다.

중심에서부터의 각도가 곧 비율이므로 파이 차트의 함정을 그대로 물려받습니다 — 바깥 고리로 갈수록 조각이 좁아져 라벨을 넣을 자리가 사라집니다. 실무에서는 3단계(고리 3개)를 넘기지 않고, 라벨은 hover 시에만 보여주거나 가장 큰 조각에만 답니다.

흔한 실수는 형제 조각을 서로 무관한 색으로 칠하는 것입니다 — 그러면 계층이 안 보이고 알록달록한 도넛으로만 보입니다. 특정 가지를 클릭해 확대하는 인터랙션(zoomable sunburst) 없이 4단계 이상을 욱여넣으면 바깥 고리가 실 한 가닥처럼 얇아지므로, 그 정도 깊이라면 트리맵이나 아이시클 차트가 낫습니다.

언제 쓰나

계층이 2~3단계이고 각 단계에서 부모 대비 비중을 강조하고 싶을 때. 계층이 깊거나 정밀 비교가 필요하면 트리맵을 쓰세요.