Ridgeline plot

리지라인 플롯

Stacks each group’s density curve with a slight overlap, laying distribution shapes out like a mountain range so groups compare at a glance.

Also known as: JoyplotJoy Division plot
···
html
<div class="viz">
  <svg id="svg"></svg>
</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}
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 CATS = ['20대','30대','40대','50대','60대'];
var COLORS = ['var(--accent)','var(--viz-4)','var(--accent-3)','var(--viz-5)','var(--accent-2)'];
var cur = CATS.map(function(){ return { mu:0.5, sigma:0.15, amp:0.5 }; });
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 gauss(f,p){ return p.amp*Math.exp(-((f-p.mu)*(f-p.mu))/(2*p.sigma*p.sigma)); }

function draw(){
  svg.innerHTML = '';
  var padL=34,padR=10,padT=10,padB=6;
  var pw=W-padL-padR, ph=H-padT-padB;
  var n=CATS.length;
  var rowGap = ph/(n+0.6);
  var maxH = rowGap*2.4;
  var M=32;
  var fs = Math.max(8,Math.min(11,W/38));
  CATS.forEach(function(name,i){
    var baseline = padT + rowGap*(i+1);
    var d = 'M'+padL.toFixed(1)+','+baseline.toFixed(1);
    for (var k=0;k<=M;k++){
      var f = k/M;
      var v = gauss(f,cur[i]);
      var x = padL+f*pw, y = baseline - v*maxH;
      d += ' L'+x.toFixed(1)+','+y.toFixed(1);
    }
    d += ' L'+(padL+pw).toFixed(1)+','+baseline.toFixed(1)+' Z';
    var area = se('path');
    sa(area,{ d:d, fill:'var(--bg)', stroke:COLORS[i%5], 'stroke-width':1.8 });
    svg.appendChild(area);
    var lab = se('text');
    sa(lab,{ x:padL-6, y:baseline+2, 'text-anchor':'end', fill:'var(--muted)', 'font-size':fs, 'font-weight':600 });
    lab.textContent = name;
    svg.appendChild(lab);
  });
}

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

function nextTarget(){
  return CATS.map(function(){ return { mu:R(0.25,0.75), sigma:R(0.09,0.2), amp:R(0.4,0.95) }; });
}

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

When comparing distributions across several groups, it shows more than a box plot — a box plot compresses each group into five numbers, while a ridgeline keeps the actual curve, so a bimodal shape (two humps) or a skew is visible directly instead of hidden behind a median line.

The trick is deliberate overlap: space the baselines closer together than the curves are tall, so a curve's peak intrudes into the row above it, and the row drawn "in front" naturally occludes part of the one behind — a cheap, effective sense of depth. The fill needs to be an opaque background color for that occlusion to work; a translucent fill lets overlapping curves show through each other and turns the whole thing muddy.

Past around ten groups, the overlap gets so heavy that curves further back nearly disappear — fold groups together or switch to small multiples instead. Group order should also mean something: stacking categories that have no natural order (unlike time or size) into a "mountain range" shape implies a trend that isn't actually there.

When to use

Use it to compare the shape of several groups’ distributions — peak count, skew — on one screen. Past ten groups, or when exact numbers matter, use a box or violin plot instead.