Streamgraph

스트림그래프

A stacked area chart inflated around a central baseline instead of zero, so several time series read as an organic, river-like flow.

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

It plots the same data as a stacked area chart — several categories' share over time — but centers the baseline instead of anchoring it at zero, inflating the stack roughly symmetrically. That trades exact readability for a much stronger sense of *rhythm*: which series is swelling or thinning when. Without a shared zero baseline, don't use it where anyone needs to read a precise value.

The technique Byron and Wattenberg originally proposed optimizes the baseline itself at every point to minimize how much the whole band wiggles. This demo uses the simpler "silhouette" baseline — just subtracting half the total at each step — which looks similar but lets the whole stack wobble more along the time axis than the wiggle-minimized version would.

Past about five series, thin bands start blending into each other and become unreadable. Ordering matters too: putting a high-variance series at the very top or bottom edge makes even the calm series in the middle look like they're wobbling, an illusion caused by their neighbor.

When to use

Use it to emphasize the relative flow and rhythm of several time series over their exact values. For precise comparison, use a stacked area or line chart instead.