Stacked bar chart

누적 막대 차트

Stacks segments inside one bar so the total and its composition read at the same time.

Also known as: Stacked column 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}
.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}}
.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); }

var CATS = ['1분기','2분기','3분기','4분기'];
var SERIES = [
  { name: '온라인', color: 'var(--accent)' },
  { name: '매장', color: 'var(--viz-4)' },
  { name: '제휴', color: 'var(--viz-5)' }
];
lg.innerHTML = SERIES.map(function(s){ return '<b><i style="background:'+s.color+'"></i>'+s.name+'</b>'; }).join('');

var cur = CATS.map(function(){ return SERIES.map(function(){ return 0; }); });
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=20, padB=22;
  var pw=W-padL-padR, ph=H-padT-padB;
  var maxV=100, n=CATS.length;
  var fs = Math.max(8, Math.min(12, W/72));
  var gap = pw*0.2/n;
  var bw = Math.min(46,(pw-gap*(n-1))/n);
  var totalW = bw*n+gap*(n-1);
  var startX = padL+(pw-totalW)/2;
  var seg = 2;

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

  CATS.forEach(function(c,i){
    var vals = cur[i];
    var total = vals.reduce(function(a,b){ return a+b; },0);
    var x = startX+i*(bw+gap);
    var y = H-padB;
    vals.forEach(function(v,si){
      var segH = Math.max((v/maxV)*ph - seg, 0.5);
      y -= segH;
      var rect = se('rect');
      sa(rect,{x:x,y:y,width:bw,height:segH,rx:2.5,ry:2.5,fill:SERIES[si].color});
      svg.appendChild(rect);
      y -= seg;
    });
    var lab = se('text');
    sa(lab,{x:x+bw/2,y:Math.max(y-3,padT+fs),'text-anchor':'middle',class:'val','font-size':fs});
    lab.textContent = Math.round(total);
    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 = c;
    svg.appendChild(cat);
  });
}

function animate(target){
  var from = cur.map(function(a){ return a.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(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(){
  return CATS.map(function(){ return SERIES.map(function(){ return R(14,34); }); });
}

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

Bar height shows the total; segment size shows composition. It's good for "compare totals, get a rough sense of composition" and bad for "compare one middle segment across bars" — only the bottom segment shares a common zero baseline; every segment above it starts somewhere different bar to bar, so the eye can't compare their lengths directly.

Separate segments with a 2px surface-colored gap, not a border — a stroke is ink that isn't data and gets loud fast. A legend is mandatory once there are two or more series.

If you actually need to track one segment's value over time, grouped bars or small multiples read far more accurately than a stack.

When to use

Use it when the total and a rough composition both matter. For precise comparison of a single component, use grouped bars instead.