Calendar heatmap

캘린더 히트맵

Lays days out as a grid of week columns and weekday rows, coloring each cell by intensity to reveal when activity clusters over time.

Also known as: Contribution graph기여 그래프
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
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]); }

var COLS = 18, ROWSN = 7;
var cur = []; for (var i=0;i<COLS*ROWSN;i++) cur.push(0.12);
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 pad=10, legendH=16;
  var pw=W-pad*2, ph=H-pad*2-legendH;
  var gap=Math.max(1, Math.min(3,W/220));
  var cw=(pw-gap*(COLS-1))/COLS;
  var ch=(ph-gap*(ROWSN-1))/ROWSN;
  var size=Math.max(1,Math.min(cw,ch));
  for (var c=0;c<COLS;c++){
    for (var r=0;r<ROWSN;r++){
      var idx=c*ROWSN+r;
      var v=cur[idx];
      var rect=se('rect');
      sa(rect,{ x:pad+c*(size+gap), y:pad+r*(size+gap), width:size, height:size, rx:1.5, fill:'var(--accent)', 'fill-opacity': Math.max(0.08, v*0.95) });
      svg.appendChild(rect);
    }
  }
  var ly = pad+ph+legendH-2;
  var lt = se('text');
  sa(lt,{ x:pad, y:ly, fill:'var(--muted)', 'font-size':8, 'font-weight':600 });
  lt.textContent = '적음';
  svg.appendChild(lt);
  for (var k=0;k<5;k++){
    var sw=se('rect');
    sa(sw,{ x:pad+26+k*11, y:ly-8, width:8, height:8, rx:1.5, fill:'var(--accent)', 'fill-opacity':0.12+k*0.22 });
    svg.appendChild(sw);
  }
  var rt = se('text');
  sa(rt,{ x:pad+26+5*11+4, y:ly, fill:'var(--muted)', 'font-size':8, 'font-weight':600 });
  rt.textContent = '많음';
  svg.appendChild(rt);
}

function animate(target){
  var from = cur.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(v,i){ return v+(target[i]-v)*e; });
    draw();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

function nextTarget(){
  var t=[];
  for (var i=0;i<COLS*ROWSN;i++) t.push(Math.random()<0.14 ? 0.03 : Math.pow(Math.random(),1.6));
  return t;
}

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

Best known as GitHub's commit-activity grid. Where a line chart answers "how much has this grown," a calendar heatmap answers "when did it cluster" — weekday habits (quiet on weekends), periodic spikes (end of month), or one unusually active day all show up just from where they land on the grid.

Color must be a sequential scale that only varies one hue's lightness — magnitude here is a matter of "how dark," not a different category. A rainbow-style scale that shifts hue instead of lightness breaks the ability to tell at a glance which of two adjacent cells is bigger just by looking at color.

Cells are small enough (a few pixels each) that reading an exact value normally requires a hover — the grid itself is for spotting roughly when activity ran high, not for precise lookups. And when comparing totals across months, account for the different day counts (28-31) — counting dark cells without adjusting for that skews longer months to look busier (or quieter) than they really are.

When to use

Use it to show when daily activity clusters by weekday or cycle. For precise value comparison or a short-range trend, a line or bar chart works better.