폭포 차트

Waterfall chart

시작값에서 끝값까지 오르내리는 단계를 떠있는 막대로 이어 보여주는 차트. "어떤 항목들이 이 변화를 만들었나"에 답합니다.

다른 이름: Bridge chart브리지 차트
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
body{display:block}
.viz{position:relative;width:100%;height:100%}
svg{display:block;width:100%;height:100%;overflow:visible}
.axis{stroke:var(--line);stroke-width:1}
.tick{fill:var(--muted);font-weight:600}
.val{fill:var(--fg);font-weight:700}
.minor{display:none}
@media (min-width:460px){.minor{display:inline}}
.conn{stroke:var(--muted);stroke-width:1;opacity:.5}
.up{fill:var(--accent)}
.down{fill:var(--accent-2)}
.tot{fill:var(--muted)}
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 = ['시작','1월','2월','3월','4월','합계'];

function genSteps(){
  var start = R(38,55);
  var d1=R(-14,20), d2=R(-14,20), d3=R(-18,16), d4=R(-10,22);
  var cum = [start, start+d1, start+d1+d2, start+d1+d2+d3, start+d1+d2+d3+d4];
  var total = cum[4];
  return [
    { from:0, to:start, kind:'tot' },
    { from:cum[0], to:cum[1], kind:d1>=0?'up':'down' },
    { from:cum[1], to:cum[2], kind:d2>=0?'up':'down' },
    { from:cum[2], to:cum[3], kind:d3>=0?'up':'down' },
    { from:cum[3], to:cum[4], kind:d4>=0?'up':'down' },
    { from:0, to:total, kind:'tot' }
  ];
}
var cur = genSteps();
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 draw(){
  svg.innerHTML = '';
  var padL=26, padR=10, padT=18, padB=22;
  var pw=W-padL-padR, ph=H-padT-padB;
  var n=cur.length;
  var fs = Math.max(8, Math.min(11, W/95));
  var gap = pw*0.1/n;
  var bw = Math.min(34,(pw-gap*(n-1))/n);
  var totalW = bw*n+gap*(n-1);
  var startX = padL+(pw-totalW)/2;
  var maxV = 100;
  function Y(v){ return padT+ph-(Math.max(0,Math.min(100,v))/maxV)*ph; }

  var base = se('line');
  sa(base,{x1:padL,y1:H-padB,x2:W-padR,y2:H-padB,class:'axis'});
  svg.appendChild(base);

  var prevX2 = null, prevY = null;
  cur.forEach(function(s,i){
    var x = startX+i*(bw+gap);
    var top = Y(Math.max(s.from,s.to)), bot = Y(Math.min(s.from,s.to));
    if (prevX2 !== null){
      var conn = se('line');
      sa(conn,{x1:prevX2,y1:prevY,x2:x,y2:prevY,class:'conn'});
      svg.appendChild(conn);
    }
    var rect = se('rect');
    sa(rect,{x:x,y:top,width:bw,height:Math.max(bot-top,1),rx:3,class:s.kind});
    svg.appendChild(rect);
    var lab = se('text');
    sa(lab,{x:x+bw/2,y:Math.max(top-5,padT+fs),'text-anchor':'middle',class:'val','font-size':fs});
    lab.textContent = i===0 || i===cur.length-1 ? Math.round(s.to) : (s.to>=s.from?'+':'')+Math.round(s.to-s.from);
    svg.appendChild(lab);
    var cat = se('text');
    sa(cat,{x:x+bw/2,y:H-padB+14,'text-anchor':'middle',class:'tick','font-size':fs-1});
    cat.textContent = LABELS[i];
    svg.appendChild(cat);
    prevX2 = x+bw; prevY = Y(s.to);
  });
}

function animate(target){
  var from = cur;
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/750);
    var e = 1-Math.pow(1-p,3);
    cur = from.map(function(s,i){ var to=target[i]; return { from:s.from+(to.from-s.from)*e, to:s.to+(to.to-s.to)*e, kind:to.kind }; });
    draw();
    if (p<1) requestAnimationFrame(step); else cur = target;
  }
  requestAnimationFrame(step);
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
setInterval(function(){ animate(genSteps()); }, 3800);

막대가 0에서 시작하지 않고 직전까지의 누적값에서 떠서 시작합니다. 증가는 위로, 감소는 아래로 뻗고, 다음 막대는 항상 이전 막대가 끝난 지점에서 이어집니다 — 그래서 시작값과 끝값 사이를 "무엇이 채웠는지" 눈으로 분해해볼 수 있습니다. 매출 브리지(작년→올해, 그 사이 항목별 증감), 손익 구조 분해에 전형적으로 씁니다.

시작 막대와 끝(합계) 막대는 0부터 시작하는 "진짜" 막대이고, 나머지는 전부 떠 있는 막대입니다 — 이 둘을 색으로 구분해야 "이건 절대값, 나머지는 증감분"이라는 게 헷갈리지 않습니다. 증가·감소도 서로 다른 색으로 나눕니다.

막대 사이를 얇은 연결선으로 이어주면 "이전 막대 끝 = 다음 막대 시작"이라는 관계가 명확해집니다. 이 선을 빼면 막대들이 서로 무관해 보여서 흐름을 놓치기 쉽습니다.

언제 쓰나

시작값과 끝값 사이의 변화를 항목별 증감으로 쪼개 설명하고 싶을 때.