네트워크 그래프

Network graph

노드(점)와 그 사이를 잇는 링크(선)로 관계를 보여주는 차트. 위치는 값이 아니라 힘의 균형(force simulation)으로 계산됩니다.

다른 이름: Node-link diagram노드-링크 다이어그램
···
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}
.edge{stroke:var(--line);stroke-width:1.2}
.ring{fill:var(--surface)}
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 GROUPS = ['var(--accent)','var(--viz-4)','var(--viz-5)'];
var N = 11;
var nodes = [];
for (var i=0;i<N;i++) nodes.push({ x:R(0.25,0.75), y:R(0.25,0.75), vx:0, vy:0, g:i%3 });
var EDGES = [[0,1],[0,2],[1,2],[1,3],[3,4],[4,5],[3,5],[5,6],[6,7],[6,8],[7,8],[8,9],[9,10],[2,4],[0,5]];
var restLen = 0.22;
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 physics(){
  for (var i=0;i<N;i++){
    for (var j=i+1;j<N;j++){
      var dx=nodes[j].x-nodes[i].x, dy=nodes[j].y-nodes[i].y;
      var d2=dx*dx+dy*dy+0.0006;
      var d=Math.sqrt(d2);
      var f=0.00028/d2;
      var fx=dx/d*f, fy=dy/d*f;
      nodes[i].vx-=fx; nodes[i].vy-=fy;
      nodes[j].vx+=fx; nodes[j].vy+=fy;
    }
  }
  EDGES.forEach(function(e){
    var a=nodes[e[0]], b=nodes[e[1]];
    var dx=b.x-a.x, dy=b.y-a.y;
    var d=Math.sqrt(dx*dx+dy*dy)+0.0006;
    var diff=(d-restLen)*0.02;
    var fx=dx/d*diff, fy=dy/d*diff;
    a.vx+=fx; a.vy+=fy;
    b.vx-=fx; b.vy-=fy;
  });
  nodes.forEach(function(n){
    n.vx += (0.5-n.x)*0.004;
    n.vy += (0.5-n.y)*0.004;
    n.vx*=0.82; n.vy*=0.82;
    n.x += n.vx; n.y += n.vy;
    n.x = Math.max(0.05,Math.min(0.95,n.x));
    n.y = Math.max(0.05,Math.min(0.95,n.y));
  });
}

function draw(){
  svg.innerHTML = '';
  var padL=12, padR=12, padT=12, padB=12;
  var pw=W-padL-padR, ph=H-padT-padB;
  function X(n){ return padL+n.x*pw; }
  function Y(n){ return padT+n.y*ph; }
  EDGES.forEach(function(e){
    var a=nodes[e[0]], b=nodes[e[1]];
    var line = se('line');
    sa(line,{x1:X(a),y1:Y(a),x2:X(b),y2:Y(b),class:'edge'});
    svg.appendChild(line);
  });
  nodes.forEach(function(n){
    var ring = se('circle');
    sa(ring,{cx:X(n),cy:Y(n),r:6.4,class:'ring'});
    svg.appendChild(ring);
    var dot = se('circle');
    sa(dot,{cx:X(n),cy:Y(n),r:4.6,fill:GROUPS[n.g]});
    svg.appendChild(dot);
  });
}

function loop(){
  physics();
  draw();
  requestAnimationFrame(loop);
}

new ResizeObserver(function(){ measure(); }).observe(svg);
measure();
loop();
setInterval(function(){ restLen = R(0.15,0.3); }, 4200);

이 데모는 아주 작은 물리 시뮬레이션을 매 프레임 직접 계산합니다 — 모든 노드가 서로를 밀어내고, 연결된 노드끼리는 링크가 정해둔 길이로 당겨서 균형점을 찾습니다. 그래서 자주 연결된 노드들이 자연스럽게 뭉치고, 거의 안 이어진 노드는 바깥으로 밀려납니다.

가장 흔히 오해하는 지점은 "노드의 위치나 거리가 값을 나타낸다"고 읽는 것입니다 — 실제로는 그렇지 않습니다. 레이아웃 알고리즘이 다르면(또는 같은 알고리즘도 초기값이 다르면) 같은 데이터가 전혀 다른 모양으로 배치됩니다. 이 차트가 정직하게 전달하는 건 "무엇이 무엇과 연결돼 있는가"와 대략적인 군집 구조뿐, 정밀한 수치 비교는 못 합니다.

노드가 많아지면(수십~수백 개) 선이 뒤엉켜 "헤어볼"이 됩니다 — 그럴 땐 핵심 군집만 필터링하거나, 링크를 굵기로 가중치화하거나, 아예 인접 행렬(매트릭스)로 바꾸는 게 낫습니다.

언제 쓰나

관계·연결 구조와 군집을 보여주고 싶을 때. 정밀한 값 비교가 목적이면 다른 차트를 쓰세요.