Contour plot

등고선 플롯

Connects points of equal value like a topographic map’s contour lines, showing how a value rises and falls across a 2D plane.

Also known as: Isoline plotLevel curves
···
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]); }
function R(a,b){ return a + Math.random()*(b-a); }

var GX=30, GY=18;
var W=300, H=200;
var BLOBS_CUR = [
  { x:0.3, y:0.35, amp:1, s:0.22 },
  { x:0.68, y:0.4, amp:0.85, s:0.2 },
  { x:0.5, y:0.75, amp:0.7, s:0.18 }
];

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 fieldAt(x,y,blobs){
  var v=0;
  for (var i=0;i<blobs.length;i++){
    var b=blobs[i], dx=x-b.x, dy=y-b.y;
    v += b.amp*Math.exp(-(dx*dx+dy*dy)/(2*b.s*b.s));
  }
  return v;
}

function lerpPt(x0,y0,v0,x1,y1,v1,th){
  var t = (v1===v0) ? 0.5 : (th-v0)/(v1-v0);
  t = Math.max(0,Math.min(1,t));
  return { x: x0+(x1-x0)*t, y: y0+(y1-y0)*t };
}

function marchingSquares(field, th, toPx){
  var segs=[];
  for (var j=0;j<GY;j++){
    for (var i=0;i<GX;i++){
      var v00=field[j][i], v10=field[j][i+1], v11=field[j+1][i+1], v01=field[j+1][i];
      var c00=v00>=th, c10=v10>=th, c11=v11>=th, c01=v01>=th;
      var p00=toPx(i,j), p10=toPx(i+1,j), p11=toPx(i+1,j+1), p01=toPx(i,j+1);
      var edges=[];
      if (c00!==c10) edges.push({ side:'t', pt: lerpPt(p00.x,p00.y,v00,p10.x,p10.y,v10,th) });
      if (c10!==c11) edges.push({ side:'r', pt: lerpPt(p10.x,p10.y,v10,p11.x,p11.y,v11,th) });
      if (c01!==c11) edges.push({ side:'b', pt: lerpPt(p01.x,p01.y,v01,p11.x,p11.y,v11,th) });
      if (c00!==c01) edges.push({ side:'l', pt: lerpPt(p00.x,p00.y,v00,p01.x,p01.y,v01,th) });
      if (edges.length===2){ segs.push([edges[0].pt, edges[1].pt]); }
      else if (edges.length===4){
        var center=(v00+v10+v11+v01)/4;
        var bySide={}; edges.forEach(function(e){ bySide[e.side]=e.pt; });
        if (center>=th){ segs.push([bySide.t,bySide.l]); segs.push([bySide.r,bySide.b]); }
        else { segs.push([bySide.t,bySide.r]); segs.push([bySide.l,bySide.b]); }
      }
    }
  }
  return segs;
}

function draw(){
  svg.innerHTML = '';
  var padL=6,padT=6, pw=W-12, ph=H-12;
  var field=[];
  for (var j=0;j<=GY;j++){
    var row=[];
    for (var i=0;i<=GX;i++) row.push(fieldAt(i/GX, j/GY, BLOBS_CUR));
    field.push(row);
  }
  function toPx(gx,gy){ return { x: padL+(gx/GX)*pw, y: padT+(gy/GY)*ph }; }
  var LEVELS=[0.15,0.3,0.45,0.6,0.75,0.9];
  LEVELS.forEach(function(th,li){
    var segs = marchingSquares(field, th, toPx);
    if (!segs.length) return;
    var d='';
    segs.forEach(function(s){ d += 'M'+s[0].x.toFixed(1)+','+s[0].y.toFixed(1)+'L'+s[1].x.toFixed(1)+','+s[1].y.toFixed(1)+' '; });
    var p = se('path');
    sa(p,{ d:d, stroke:'var(--accent)', 'stroke-width':1.4, fill:'none', 'stroke-opacity': 0.35+0.55*(li/(LEVELS.length-1)) });
    svg.appendChild(p);
  });
}

function animate(target){
  var from = BLOBS_CUR.map(function(b){ return { x:b.x, y:b.y, amp:b.amp, s:b.s }; });
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/900);
    var e = 1-Math.pow(1-p,3);
    BLOBS_CUR = from.map(function(b,i){
      return { x:b.x+(target[i].x-b.x)*e, y:b.y+(target[i].y-b.y)*e, amp:b.amp+(target[i].amp-b.amp)*e, s:b.s+(target[i].s-b.s)*e };
    });
    draw();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

function nextBlobs(){
  return [
    { x:R(0.15,0.4), y:R(0.15,0.45), amp:R(0.7,1.1), s:R(0.16,0.24) },
    { x:R(0.55,0.85), y:R(0.2,0.55), amp:R(0.6,1), s:R(0.14,0.22) },
    { x:R(0.3,0.7), y:R(0.6,0.88), amp:R(0.5,0.95), s:R(0.14,0.22) }
  ];
}

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

A scatter plot or heatmap shows value point by point or cell by cell; a contour plot instead connects "points of equal value" into lines, revealing the shape of the terrain itself. Tightly spaced contour lines mean the value changes steeply there; widely spaced lines mean it's gentle — exactly like a topographic map, where dense contours mark a cliff and sparse ones a plain.

This demo uses the marching squares algorithm: divide the plane into a small grid, and for each cell compare its four corner values against a threshold. Wherever an edge's two corners fall on opposite sides of that threshold, linearly interpolate along the edge to find the exact crossing point, then connect those points into the contour segment passing through that cell. Repeat for every cell at several thresholds and the full set of contour lines falls out.

Too coarse a grid (large cells) turns smooth curves into an angular polygon; too fine wastes computation for a difference nobody can see. Uneven spacing between threshold levels — say, packing them tightly only in the low range — is also a common mistake: line density then reflects "what the author wanted to emphasize" instead of the actual gradient, quietly distorting the map.

When to use

Use it to show where a value changes steeply or gently across a 2D plane. When the exact value at individual points matters more, use a heatmap or scatter plot instead.