Scatter plot

산점도

Plots two continuous variables as x/y points to reveal correlation and clustering that a summary number would hide.

Also known as: Scatter chartXY 차트
···
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}
.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:50%;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 GROUPS = [
  { name: '그룹 A', color: 'var(--accent)', n: 22, cx: 0.35, cy: 0.62, spread: 0.16 },
  { name: '그룹 B', color: 'var(--viz-4)', n: 16, cx: 0.68, cy: 0.32, spread: 0.14 }
];
lg.innerHTML = GROUPS.map(function(g){ return '<b><i style="background:'+g.color+'"></i>'+g.name+'</b>'; }).join('');

var pts = [];
GROUPS.forEach(function(g,gi){
  for (var i=0;i<g.n;i++) pts.push({ g: gi, x: g.cx, y: g.cy, tx: g.cx, ty: g.cy });
});

function sample(g){ return { x: Math.max(0.03,Math.min(0.97, g.cx+R(-1,1)*g.spread)), y: Math.max(0.03,Math.min(0.97, g.cy+R(-1,1)*g.spread)) }; }
pts.forEach(function(p){ var s = sample(GROUPS[p.g]); p.x=s.x; p.y=s.y; p.tx=s.x; p.ty=s.y; });

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=22, padR=12, padT=14, padB=20;
  var pw=W-padL-padR, ph=H-padT-padB;
  var ax = se('line');
  sa(ax,{x1:padL,y1:H-padB,x2:W-padR,y2:H-padB,class:'axis'});
  svg.appendChild(ax);
  var ay = se('line');
  sa(ay,{x1:padL,y1:padT,x2:padL,y2:H-padB,class:'axis'});
  svg.appendChild(ay);
  pts.forEach(function(p){
    var c = se('circle');
    sa(c,{cx:padL+p.x*pw, cy:padT+(1-p.y)*ph, r:3.4, fill:GROUPS[p.g].color, 'fill-opacity':0.78});
    svg.appendChild(c);
  });
}

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

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
setInterval(function(){
  pts.forEach(function(p){ var s = sample(GROUPS[p.g]); p.tx=s.x; p.ty=s.y; });
  animate();
}, 3200);

This chart answers "are these two things related." Points trending up-and-right mean positive correlation, up-and-left mean negative, and a shapeless cloud means weak or no correlation — though correlation shown here is never proof of causation.

More points bring overplotting — density matters more than any single dot, so lower the opacity, or switch to a heatmap / 2D histogram once the cloud gets dense. Coloring by group caps out around 2-3 — beyond that, colors blur together wherever clusters overlap.

Hover targets should be bigger than the dot itself; requiring a precise hit on an 8px point is a target nobody can actually land.

When to use

Use it to explore relationship or clustering between two continuous variables. Past a few hundred points, switch to a density encoding like a heatmap.