Data-ink ratio

데이터-잉크 비율

Edward Tufte’s ratio of ink that actually represents data to total ink used — everything else is a candidate for deletion.

Also known as: 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);

The same data, drawn two ways. The left version has a different color per bar, heavy borders, a fake drop shadow, a dense grid, and redundant labels. The right version keeps only what's needed to show the values — one bar hue, one baseline, one label. Both charts carry exactly the same information.

This isn't a call to "make everything minimal." The test for deleting ink is whether removing it loses information. It doesn't for decorative shadows, an over-dense grid, or a duplicated label, so those go. It does for axis labels or a legend, so those stay.

In practice, the biggest wins are usually gridlines (fewer, thinner), borders (replace with a surface-colored gap between bars instead), background decoration, and labels that just restate what the chart already shows.

When to use

When polishing any chart, check every element against one question: does removing it lose information?