스몰 멀티플

Small multiples

같은 형태의 작은 차트를 같은 축으로 여러 개 나란히 늘어놓는 방식. 한 차트에 다 우겨넣는 대신 "펼쳐서" 비교합니다.

다른 이름: Trellis chart패널 차트격자 차트
···
html
<div class="viz" id="viz"></div>
css
body{display:block}
.viz{width:100%;height:100%;display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:2px;box-sizing:border-box;padding:4px}
.cell{position:relative;border:1px solid var(--line);border-radius:4px;overflow:hidden}
.cell svg{display:block;width:100%;height:100%}
.cell .ttl{position:absolute;top:2px;left:4px;font:700 8px/1 system-ui,sans-serif;color:var(--muted);z-index:1}
@media (min-width:460px){ .cell .ttl{font-size:11px} }
js
var viz = document.getElementById('viz');
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 PANELS = ['1월','2월','3월','4월','5월','6월'];
var HL = 0;
var Nm = 6;
var svgs = [];
PANELS.forEach(function(name){
  var cell = document.createElement('div');
  cell.className = 'cell';
  var ttl = document.createElement('div');
  ttl.className = 'ttl';
  ttl.textContent = name;
  var svg = se('svg');
  cell.appendChild(ttl);
  cell.appendChild(svg);
  viz.appendChild(cell);
  svgs.push(svg);
});

var series = PANELS.map(function(){
  var v=50, out=[];
  for (var i=0;i<Nm;i++){ v=Math.max(10,Math.min(90, v+R(-20,20))); out.push(v); }
  return out;
});
var cur = series.map(function(a){ return a.slice(); });

function drawPanel(svg, vals, isHL){
  svg.innerHTML = '';
  var r = svg.getBoundingClientRect();
  var W = Math.max(r.width,10), H = Math.max(r.height,10);
  sa(svg,{ viewBox:'0 0 '+W+' '+H });
  var padL=4, padR=4, padT=13, padB=4;
  var pw=W-padL-padR, ph=H-padT-padB;
  var base = se('line');
  sa(base,{x1:padL,y1:padT+ph,x2:W-padR,y2:padT+ph,stroke:'var(--line)','stroke-width':1});
  svg.appendChild(base);
  var pts = vals.map(function(v,i){ return { x:padL+(pw*i)/(Nm-1), y:padT+ph-(v/100)*ph }; });
  var d = pts.map(function(p,i){ return (i===0?'M':'L')+p.x.toFixed(1)+','+p.y.toFixed(1); }).join(' ');
  var path = se('path');
  sa(path,{ d:d, fill:'none', stroke: isHL?'var(--accent)':'var(--muted)', 'stroke-width': isHL?2.2:1.4, 'stroke-linecap':'round', 'stroke-linejoin':'round', opacity: isHL?1:0.55 });
  svg.appendChild(path);
}

function drawAll(){
  svgs.forEach(function(svg,i){ drawPanel(svg, cur[i], i===HL); });
}

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

new ResizeObserver(function(){ drawAll(); }).observe(viz);
drawAll();
setInterval(function(){
  var target = PANELS.map(function(){
    var v=50, out=[];
    for (var i=0;i<Nm;i++){ v=Math.max(10,Math.min(90, v+R(-20,20))); out.push(v); }
    return out;
  });
  animate(target);
}, 3800);

에드워드 터프티가 "가장 강력한 설계 원리 중 하나"라고 부른 방식입니다. 선 5개가 겹친 스파게티 차트 대신, 같은 y축 범위를 공유하는 작은 차트 5개를 격자로 늘어놓으면 각 시리즈의 모양이 서로 가려지지 않고 다 보입니다.

핵심 규칙은 "축을 공유한다"입니다 — 패널마다 y축 범위가 다르면 작은 변화도 커 보이고 큰 변화도 작아 보여서 패널 간 비교 자체가 무너집니다. 축(눈금·범위)은 반드시 통일하고, 패널마다 굳이 다시 그리지 않아도 되게 첫 패널이나 여백에 한 번만 표기합니다.

특정 패널을 강조하고 싶으면 그 패널만 진한 색을 쓰고 나머지는 옅게 죽입니다 — 색을 다 다르게 칠할 필요는 없습니다. 어차피 위치와 제목이 이미 각 패널을 구분해주기 때문입니다.

언제 쓰나

시리즈가 4개를 넘어 한 차트에 겹치면 스파게티가 될 때. 같은 척도로 나란히 비교하고 싶을 때.