데이터-잉크 비율

Data-ink ratio

에드워드 터프티의 개념 — 전체 잉크 중 실제로 데이터를 나타내는 잉크의 비율. 나머지는 지울수록 좋습니다.

다른 이름: Chartjunk차트정크
···
html
<div class="viz">
  <div class="panel bad">
    <div class="ttl">Before</div>
    <svg id="svgBad"></svg>
  </div>
  <div class="panel good">
    <div class="ttl">After</div>
    <svg id="svgGood"></svg>
  </div>
</div>
css
:root{--viz-4:#e8a23a;--viz-5:#4098d7}
body{display:block}
.viz{display:flex;width:100%;height:100%}
.panel{flex:1;min-width:0;display:flex;flex-direction:column}
.ttl{flex:none;text-align:center;font:700 9px/1 system-ui,sans-serif;padding:4px 0;color:var(--muted)}
.panel svg{flex:1;min-height:0;width:100%;display:block}
.bad{background:color-mix(in srgb, var(--line) 55%, var(--bg))}
.good{background:var(--bg)}
js
var svgBad = document.getElementById('svgBad');
var svgGood = document.getElementById('svgGood');
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'];
var JUNK = ['var(--accent)','var(--viz-4)','var(--viz-5)','var(--accent-2)','var(--accent)'];
var cur = CATS.map(function(){ return 0; });

function size(svg){
  var r = svg.getBoundingClientRect();
  var W = Math.max(r.width,10), H = Math.max(r.height,10);
  sa(svg,{ viewBox:'0 0 '+W+' '+H });
  return { W:W, H:H };
}

function layout(W,H){
  var padL=20, padR=8, padT=10, padB=16;
  var pw=W-padL-padR, ph=H-padT-padB;
  var n=CATS.length, gap=pw*0.12/n;
  var bw=(pw-gap*(n-1))/n;
  return { padL:padL, padT:padT, pw:pw, ph:ph, bw:bw, gap:gap };
}

function drawBad(){
  svgBad.innerHTML = '';
  var d = size(svgBad), W=d.W, H=d.H;
  var L = layout(W,H);
  var fs = Math.max(7, Math.min(10, W/62));
  for (var g=0; g<=100; g+=10){
    var y = L.padT+L.ph-(g/100)*L.ph;
    var line = se('line');
    sa(line,{x1:L.padL,y1:y,x2:W-8,y2:y,stroke:'var(--fg)','stroke-width':0.7,opacity:0.28});
    svgBad.appendChild(line);
  }
  CATS.forEach(function(c,i){
    var v = cur[i];
    var bh = (v/100)*L.ph;
    var x = L.padL+i*(L.bw+L.gap);
    var y = L.padT+L.ph-bh;
    var shadow = se('rect');
    sa(shadow,{x:x+3,y:y+3,width:L.bw,height:Math.max(bh,0.5),fill:'#000',opacity:0.22});
    svgBad.appendChild(shadow);
    var rect = se('rect');
    sa(rect,{x:x,y:y,width:L.bw,height:Math.max(bh,0.5),fill:JUNK[i],stroke:'var(--fg)','stroke-width':1.6});
    svgBad.appendChild(rect);
    var bevel = se('rect');
    sa(bevel,{x:x,y:y,width:L.bw,height:Math.max(Math.min(bh,8),0),fill:'#fff',opacity:0.28});
    svgBad.appendChild(bevel);
    [y-4, y+11].forEach(function(ly){
      var lab = se('text');
      sa(lab,{x:x+L.bw/2,y:ly,'text-anchor':'middle',fill:'var(--fg)','font-size':fs,'font-weight':800,stroke:'var(--surface)','stroke-width':2.4,'paint-order':'stroke'});
      lab.textContent = Math.round(v);
      svgBad.appendChild(lab);
    });
  });
}

function drawGood(){
  svgGood.innerHTML = '';
  var d = size(svgGood), W=d.W, H=d.H;
  var L = layout(W,H);
  var fs = Math.max(7, Math.min(10, W/62));
  var base = se('line');
  sa(base,{x1:L.padL,y1:L.padT+L.ph,x2:W-8,y2:L.padT+L.ph,stroke:'var(--line)','stroke-width':1});
  svgGood.appendChild(base);
  CATS.forEach(function(c,i){
    var v = cur[i];
    var bh = (v/100)*L.ph;
    var x = L.padL+i*(L.bw+L.gap);
    var y = L.padT+L.ph-bh;
    var rect = se('rect');
    sa(rect,{x:x,y:y,width:L.bw,height:Math.max(bh,0.5),rx:2,fill:'var(--accent)'});
    svgGood.appendChild(rect);
    var lab = se('text');
    sa(lab,{x:x+L.bw/2,y:Math.max(y-4,L.padT+fs),'text-anchor':'middle',fill:'var(--fg)','font-size':fs,'font-weight':700});
    lab.textContent = Math.round(v);
    svgGood.appendChild(lab);
  });
}

function drawBoth(){ drawBad(); drawGood(); }

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; });
    drawBoth();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

new ResizeObserver(drawBoth).observe(svgBad);
new ResizeObserver(drawBoth).observe(svgGood);
drawBoth();
animate(CATS.map(function(){ return R(30,95); }));
setInterval(function(){ animate(CATS.map(function(){ return R(15,98); })); }, 3600);

같은 데이터를 두 방식으로 그렸습니다. 왼쪽은 막대마다 다른 색, 두꺼운 테두리, 가짜 입체 그림자, 촘촘한 그리드, 중복된 라벨까지 다 들어간 버전이고, 오른쪽은 값을 표현하는 데 꼭 필요한 선(막대 하나, 기준선 하나, 라벨 하나)만 남긴 버전입니다. 두 차트가 전달하는 정보는 완전히 같습니다.

"미니멀하게 만들라"는 뜻이 아닙니다 — 잉크를 지우는 기준은 "그 잉크가 없으면 정보가 줄어드는가"입니다. 줄어들지 않는다면(장식용 그림자, 불필요하게 촘촘한 그리드, 중복 라벨) 지우는 게 맞고, 줄어든다면(축 라벨, 범례) 지우면 안 됩니다.

실무에서 가장 많이 지울 수 있는 건 그리드 선(굵기와 개수를 줄이기), 테두리(막대 사이는 서지스 간격으로 대체), 배경 장식, 그리고 "이미 그래프가 보여주는 걸 다시 말로 반복하는" 라벨입니다.

언제 쓰나

차트를 다듬을 때 "이 요소를 지우면 정보가 줄어드는가"를 기준으로 매 요소를 점검하세요.