Treemap

트리맵

Encodes value as rectangle area, tiling the whole plot with no wasted space — built for hierarchical part-to-whole data.

Also known as: Squarified treemap사각형 트리맵
···
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}
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 LABELS = ['A','B','C','D','E','F','G'];
var COLORS = ['var(--accent)','var(--viz-4)','var(--accent-3)','var(--viz-5)'];
var vals = LABELS.map(function(){ return R(8,42); });
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 worstRatio(row, side){
  var sum=0, max=-Infinity, min=Infinity;
  row.forEach(function(v){ sum+=v; if (v>max) max=v; if (v<min) min=v; });
  var s2=side*side, sum2=sum*sum;
  return Math.max((s2*max)/sum2, sum2/(s2*min));
}
function squarify(list, x, y, w, h, out){
  if (!list.length) return;
  if (list.length===1){ out.push({ item:list[0], x:x, y:y, w:w, h:h }); return; }
  var side = Math.min(w,h);
  var row = [list[0].area];
  var i = 1;
  while (i<list.length){
    var next = row.concat([list[i].area]);
    if (worstRatio(next,side) <= worstRatio(row,side)) { row=next; i++; } else break;
  }
  var rowSum = row.reduce(function(a,b){ return a+b; },0);
  if (w>=h){
    var colW = rowSum/h, yy=y;
    for (var k=0;k<row.length;k++){ var rh=row[k]/colW; out.push({item:list[k],x:x,y:yy,w:colW,h:rh}); yy+=rh; }
    squarify(list.slice(row.length), x+colW, y, w-colW, h, out);
  } else {
    var rowH = rowSum/w, xx=x;
    for (var k2=0;k2<row.length;k2++){ var rw=row[k2]/rowH; out.push({item:list[k2],x:xx,y:y,w:rw,h:rowH}); xx+=rw; }
    squarify(list.slice(row.length), x, y+rowH, w, h-rowH, out);
  }
}

function computeRects(valsArr){
  var padL=6, padR=6, padT=6, padB=6;
  var pw = W-padL-padR, ph = H-padT-padB;
  var total = valsArr.reduce(function(a,b){ return a+b; },0);
  var items = LABELS.map(function(l,i){ return { label:l, color:COLORS[i%4], area:(valsArr[i]/total)*pw*ph }; })
    .sort(function(a,b){ return b.area-a.area; });
  var out = [];
  squarify(items, padL, padT, pw, ph, out);
  var map = {};
  out.forEach(function(rc){ map[rc.item.label] = { x:rc.x, y:rc.y, w:rc.w, h:rc.h, color:rc.item.color, label:rc.item.label }; });
  return map;
}

function drawFrom(map){
  svg.innerHTML = '';
  var fs = Math.max(8, Math.min(13, W/70));
  LABELS.forEach(function(l){
    var rc = map[l];
    var rect = se('rect');
    sa(rect,{ x:rc.x+1, y:rc.y+1, width:Math.max(rc.w-2,0), height:Math.max(rc.h-2,0), rx:3, fill:rc.color });
    svg.appendChild(rect);
    if (rc.w>26 && rc.h>17){
      var t = se('text');
      sa(t,{ x:rc.x+6, y:rc.y+fs+4, fill:'#fff', 'font-size':fs, 'font-weight':700 });
      t.textContent = l;
      svg.appendChild(t);
    }
  });
}

var curMap = computeRects(vals);
function draw(){ curMap = computeRects(vals); drawFrom(curMap); }

function animateTo(target){
  var fromMap = curMap;
  var toMap = computeRects(target);
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/800);
    var e = 1-Math.pow(1-p,3);
    var mid = {};
    LABELS.forEach(function(l){
      var f=fromMap[l], to=toMap[l];
      mid[l] = { x:f.x+(to.x-f.x)*e, y:f.y+(to.y-f.y)*e, w:f.w+(to.w-f.w)*e, h:f.h+(to.h-f.h)*e, color:to.color, label:l };
    });
    curMap = mid;
    drawFrom(mid);
    if (p<1) requestAnimationFrame(step); else { vals = target; curMap = toMap; }
  }
  requestAnimationFrame(step);
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
setInterval(function(){
  animateTo(LABELS.map(function(){ return R(6,44); }));
}, 3800);

The goal is the same as a pie chart's (part-to-whole), but a treemap uses 100% of the plot area and still has room when there are dozens of items. The "squarify" algorithm places each rectangle so its area matches its value while keeping rectangles close to square, filling row by row.

The catch: area comparisons are far less accurate than length comparisons. A ranking that's obvious on a bar chart can be genuinely hard to eyeball on a treemap. So reach for bars when precise comparison matters, and use a treemap when the job is "fit the whole structure on one screen and spot the big chunks."

Skip the label when a rectangle is too small to hold it. Shrinking the font past readability, or cropping it with overflow:hidden, is worse than no label — if there's no room, let that item's name live only in the legend or tooltip.

When to use

Use it for hierarchical part-to-whole data with many items (10+) that all need to fit on one screen.