Marimekko chart

마리메꼬 차트

A stacked bar where both width and height carry meaning — width shows each column’s overall scale, height its internal composition, at the same time.

Also known as: Mekko chartMosaic plot모자이크 플롯
···
html
<div class="viz">
  <svg id="svg"></svg>
  <div class="legend" id="lg"></div>
</div>
css
:root{--viz-4:#e8a23a}
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 CATS = ['수도권','영남','호남','기타'];
var SERIES = [
  { name: '온라인', color: 'var(--accent)' },
  { name: '오프라인', color: 'var(--viz-4)' },
  { name: '제휴', color: 'var(--accent-3)' }
];
lg.innerHTML = SERIES.map(function(s){ return '<b><i style="background:'+s.color+'"></i>'+s.name+'</b>'; }).join('');

var cur = { weight: CATS.map(function(){ return 25; }), share: CATS.map(function(){ return SERIES.map(function(){ return 1; }); }) };
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=6,padR=6,padT=8,padB=20;
  var pw=W-padL-padR, ph=H-padT-padB;
  var gap=2;
  var totalW = sum(cur.weight);
  var fs = Math.max(7,Math.min(10,W/48));
  var x = padL;
  CATS.forEach(function(name,ci){
    var w = Math.max(2,(cur.weight[ci]/totalW)*(pw-gap*(CATS.length-1)));
    var shareRow = cur.share[ci];
    var rowSum = sum(shareRow);
    var y = padT+ph;
    shareRow.forEach(function(v,si){
      var segH = (v/rowSum)*ph;
      y -= segH;
      var rect = se('rect');
      sa(rect,{ x:x, y:y, width:w, height:Math.max(segH-1,0.5), fill:SERIES[si].color });
      svg.appendChild(rect);
    });
    var lab = se('text');
    sa(lab,{ x:x+w/2, y:padT+ph+13, 'text-anchor':'middle', fill:'var(--muted)', 'font-size':fs, 'font-weight':600 });
    lab.textContent = name;
    svg.appendChild(lab);
    x += w+gap;
  });
}

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

function nextTarget(){
  return {
    weight: CATS.map(function(){ return R(10,42); }),
    share: CATS.map(function(){ return SERIES.map(function(){ return R(6,85); }); })
  };
}

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

A stacked bar chart keeps every bar the same width, but a marimekko turns that width into data too — a column's width shows its category's overall scale (say, revenue by region), and each segment's height inside it shows composition within that category (say, channel mix). One picture answers "which category is biggest" and "what makes it up" together.

Both dimensions are relative percentages: every column's width sums to 100% of the chart, and every column's segment heights sum to 100% of that column. That makes a column's *area* only a rough stand-in for its absolute value (say, revenue in dollars) — not something to compare precisely. If precise comparison matters, pair it with a table.

Narrow columns run out of room for segment labels fast. Past 5-6 columns, or 4 segments per column, the colors pack in tight enough that boundaries get hard to read — beyond that, keep the top categories and fold the rest into "Other."

When to use

Use it to show each category’s overall scale together with its internal composition. For precise absolute-value comparison, pair it with a table or grouped bars.