스트림그래프

Streamgraph

누적 영역 차트를 중심선 기준 위아래로 부풀려, 강물처럼 흐르는 여러 시계열의 상대적 크기를 보여주는 차트.

다른 이름: ThemeRiver Flow chart
···
html
<div class="viz">
  <svg id="svg"></svg>
  <div class="legend" id="lg"></div>
</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}
.legend{position:absolute;top:4px;right:6px;display:flex;gap:8px;font:600 9px/1 system-ui,sans-serif;color:var(--muted)}
.legend b{display:inline-flex;align-items:center;gap:3px;font-weight:600}
.legend i{width:7px;height:7px;border-radius:2px;display:inline-block}
js
var svg = document.getElementById('svg');
var lg = document.getElementById('lg');
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); }
function sum(arr){ return arr.reduce(function(a,b){ return a+b; },0); }

var SERIES = [
  { name: '검색', color: 'var(--accent)' },
  { name: '추천', color: 'var(--viz-4)' },
  { name: '다이렉트', color: 'var(--accent-3)' },
  { name: '소셜', color: 'var(--viz-5)' }
];
lg.innerHTML = SERIES.map(function(s){ return '<b><i style="background:'+s.color+'"></i>'+s.name+'</b>'; }).join('');

var N = 12;
var cur = [];
for (var i=0;i<N;i++) cur.push(SERIES.map(function(){ return 20; }));
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 smoothPath(pts, cmd){
  var d = cmd+pts[0].x.toFixed(1)+','+pts[0].y.toFixed(1);
  for (var i=1;i<pts.length;i++){
    var mx=(pts[i-1].x+pts[i].x)/2, my=(pts[i-1].y+pts[i].y)/2;
    d += ' Q'+pts[i-1].x.toFixed(1)+','+pts[i-1].y.toFixed(1)+' '+mx.toFixed(1)+','+my.toFixed(1);
  }
  var last = pts[pts.length-1];
  d += ' L'+last.x.toFixed(1)+','+last.y.toFixed(1);
  return d;
}

function draw(){
  svg.innerHTML = '';
  var padL=8,padR=8,padT=16,padB=14;
  var pw=W-padL-padR, ph=H-padT-padB;
  var midY = padT+ph/2;
  var scale = ph/170;
  var xs=[];
  for (var i=0;i<N;i++) xs.push(padL+pw*i/(N-1));
  var totals = cur.map(sum);
  var bases = totals.map(function(t){ return -t/2; });
  SERIES.forEach(function(s,si){
    var top=[], bottom=[];
    for (var i=0;i<N;i++){
      var b = bases[i];
      for (var k=0;k<si;k++) b += cur[i][k];
      var t = b + cur[i][si];
      bottom.push({ x:xs[i], y: midY - b*scale });
      top.push({ x:xs[i], y: midY - t*scale });
    }
    var d = smoothPath(top,'M') + ' ' + smoothPath(bottom.slice().reverse(),'L') + ' Z';
    var el = se('path');
    sa(el,{ d:d, fill:s.color, 'fill-opacity':0.88 });
    svg.appendChild(el);
  });
}

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

function nextTarget(){
  var t = [];
  var v = SERIES.map(function(){ return R(10,40); });
  for (var i=0;i<N;i++){
    v = v.map(function(x){ return Math.max(4,Math.min(60, x+R(-7,7))); });
    t.push(v.slice());
  }
  return t;
}

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

누적 영역 차트와 같은 데이터(시간에 따른 여러 범주의 구성)를 쓰지만, 기준선을 0이 아니라 "중앙"에 두어 위아래로 대칭에 가깝게 부풀립니다. 그 결과 절대값보다 "이 시기에 어느 시리즈가 굵어지고 가늘어지는가"라는 상대적인 리듬이 훨씬 잘 보입니다 — 대신 공통 기준선이 없으므로 정확한 수치를 읽는 용도로는 맞지 않습니다.

원래 리 바이런과 마틴 워튼버그가 제안한 스트림그래프는 밴드 전체가 최대한 덜 출렁이도록(wiggle 최소화) 기준선 자체를 매 순간 최적화합니다. 이 데모는 그 정교한 계산 대신 "매 순간 합계의 절반을 빼는" 단순한 대칭(실루엣) 기준선을 씁니다 — 보기엔 비슷해도 시간 축을 따라 밴드 전체가 더 출렁여 보일 수 있다는 차이가 있습니다.

시리즈가 5개를 넘으면 얇은 밴드끼리 색이 섞여 보여 오히려 못 읽습니다. 순서도 중요합니다 — 변동이 큰 시리즈를 맨 위/아래 가장자리에 두면 중앙의 안정적인 시리즈까지 흔들려 보이는 착시가 생깁니다.

언제 쓰나

여러 시계열의 절대값보다 "상대적인 흐름과 리듬"을 강조하고 싶을 때. 정확한 값 비교가 필요하면 누적 영역이나 선 차트를 쓰세요.