관계망(노드-엣지) 분석을 위한 8가지 패턴 · 코드 그대로 복사 → 데이터만 교체
<div id="..."> (높이 필수!)new vis.Network(div, {nodes, edges}, options)| id (필수·고유) | label (화면 표시) | group (색·범주) | value (크기, 선택) |
|---|---|---|---|
| 서울 | 서울특별시 | 시도 | 34215 |
| 차대차 | 차대차 사고 | 유형 | 142350 |
| from (시작 id) | to (끝 id) | value (선 굵기) |
|---|---|---|
| 서울 | 차대차 | 25800 |
| 경기 | 차대차 | 44100 |
from·to는 노드의 id와 정확히 같아야 함.// CDN 추가: // <script src="https://cdn.jsdelivr.net/npm/papaparse@5"></script> Promise.all([ fetch('nodes.csv').then(r => r.text()), fetch('edges.csv').then(r => r.text()) ]).then(([nodeCsv, edgeCsv]) => { const nodes = Papa.parse(nodeCsv, { header:true, dynamicTyping:true }).data; const edges = Papa.parse(edgeCsv, { header:true, dynamicTyping:true }).data; new vis.Network(div, { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) }, options); });
| 데이터 형태 | 예시 | 시각화 패턴 |
|---|---|---|
| ① 동질 네트워크 모든 노드가 같은 종류 |
사람-사람 친구, 키워드-키워드 동시출현 | Force-Directed (④), 의미연결망 (🗣️) |
| ② 양분 네트워크 두 종류의 노드 |
시도 ↔ 사고유형, 학생 ↔ 수강과목 | 양분 (②), Sankey |
| ③ 계층 네트워크 부모-자식, 위→아래 |
전국→권역→시도, 조직도 | 계층 Tree (③), Cytoscape Dagre (⑤) |
// ① CDN: vis-network // <script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script> // ② <div id="net1" style="height:480px"></div> ← 높이 필수! // ③ 데이터 준비 ─── 노드 배열 + 엣지 배열 const nodes = new vis.DataSet([ { id:1, label:'A', color:'#3b82f6' }, { id:2, label:'B', color:'#10b981' }, { id:3, label:'C', color:'#ef4444' }, { id:4, label:'D', color:'#f59e0b' }, { id:5, label:'E', color:'#8b5cf6' } ]); const edges = new vis.DataSet([ { from:1, to:2 }, { from:1, to:3 }, { from:2, to:4 }, { from:3, to:4 }, { from:4, to:5 }, { from:5, to:1 } ]); // ④ 네트워크 생성 const network = new vis.Network( document.getElementById('net1'), { nodes, edges }, { nodes: { shape: 'dot', size: 24, font: { size: 14, color: 'white', strokeWidth: 2, strokeColor: 'rgba(0,0,0,0.4)' } }, edges: { width: 2, color: '#94a3b8' }, physics: { stabilization: { iterations: 120 } } } );
physics 활성화 → 노드들이 자동으로 자리잡음. 드래그로 직접 옮길 수도 있음.group 속성으로 노드를 그룹별로 분리, groups 옵션에서 그룹별 색 정의, scaling으로 value→크기/굵기 자동 매핑.// ② <div id="net2" style="height:480px"></div> // ③ 데이터 준비 ─── 두 그룹의 노드 + 그 사이 엣지 const nodes = new vis.DataSet([ { id:'서울', label:'서울', group:'시도', value:34215 }, { id:'경기', label:'경기', group:'시도', value:58432 }, { id:'부산', label:'부산', group:'시도', value:15621 }, { id:'차대차', label:'차대차', group:'유형', value:142350 }, { id:'차대사람', label:'차대사람', group:'유형', value:42580 }, { id:'차량단독', label:'차량단독', group:'유형', value:6380 } ]); const edges = new vis.DataSet([ { from:'서울', to:'차대차', value:25800 }, { from:'서울', to:'차대사람', value:6800 }, { from:'경기', to:'차대차', value:44100 }, { from:'경기', to:'차대사람', value:10800 }, { from:'부산', to:'차대차', value:11800 }, { from:'부산', to:'차대사람', value:2900 } ]); // ④ 네트워크 생성 new vis.Network(document.getElementById('net2'), { nodes, edges }, { nodes: { shape: 'dot', scaling: { min: 22, max: 60, label: { min: 12, max: 18 } } }, groups: { // 그룹별 색 정의 시도: { color: { background: '#3b82f6' }, font: { color: 'white' } }, 유형: { color: { background: '#f59e0b' }, font: { color: 'white' } } }, edges: { scaling: { min: 1, max: 10 }, // value → 굵기 smooth: { type: 'curvedCW', roundness: 0.2 } }, physics: { barnesHut: { gravitationalConstant: -8000, springLength: 160 } } });
layout.hierarchical 옵션 추가. physics: false 필수.// ② <div id="net3" style="height:480px"></div> // ③ 데이터 준비 ─── 엣지 방향이 부모→자식이어야 함 const nodes = new vis.DataSet([ { id:'전국', label:'전국', color:'#1e40af', font:{color:'white'} }, { id:'수도권', label:'수도권', color:'#3b82f6', font:{color:'white'} }, { id:'영남', label:'영남', color:'#3b82f6', font:{color:'white'} }, { id:'서울', label:'서울 34,215' }, { id:'경기', label:'경기 58,432' }, { id:'부산', label:'부산 15,621' } ]); const edges = new vis.DataSet([ { from:'전국', to:'수도권' }, { from:'전국', to:'영남' }, { from:'수도권', to:'서울' }, { from:'수도권', to:'경기' }, { from:'영남', to:'부산' } ]); // ④ 네트워크 생성 ─── 핵심: layout.hierarchical + physics:false new vis.Network(document.getElementById('net3'), { nodes, edges }, { layout: { hierarchical: { direction: 'UD', // UD/DU/LR/RL 4방향 sortMethod: 'directed', levelSeparation: 120, nodeSpacing: 130 } }, nodes: { shape: 'box', font: { size: 12 }, margin: 8 }, edges: { arrows: 'to', smooth: { type: 'cubicBezier' } }, physics: false // 계층은 끄는 게 깔끔 });
{id, group}·{source, target} 형태.// ① CDN: D3 v7 // <script src="https://d3js.org/d3.v7.min.js"></script> // ② <div id="net4" style="height:480px"></div> // ③ 데이터 준비 const nodes = [ { id:'서울', group:'시도', size:14 }, { id:'경기', group:'시도', size:14 }, { id:'차대차', group:'유형', size:12 }, { id:'주중주간', group:'시간대', size:11 } ]; const links = [ { source:'서울', target:'차대차', value:6 }, { source:'경기', target:'차대차', value:8 }, { source:'서울', target:'주중주간', value:5 } ]; const color = { 시도:'#3b82f6', 유형:'#f59e0b', 시간대:'#10b981' }; // ④ 시뮬레이션 + SVG 그리기 const W = 800, H = 480; const svg = d3.select('#net4').append('svg') .attr('width', '100%').attr('height', H) .attr('viewBox', `0 0 ${W} ${H}`); const sim = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id).distance(80)) .force('charge', d3.forceManyBody().strength(-260)) .force('center', d3.forceCenter(W/2, H/2)); const link = svg.append('g').selectAll('line').data(links).enter() .append('line').attr('stroke', '#94a3b8').attr('stroke-width', d => Math.sqrt(d.value)); const node = svg.append('g').selectAll('g').data(nodes).enter().append('g'); node.append('circle').attr('r', d => d.size).attr('fill', d => color[d.group]); node.append('text').text(d => d.id).attr('x', d => d.size+4).attr('y', 4); // 매 프레임마다 좌표 업데이트 sim.on('tick', () => { link.attr('x1', d => d.source.x).attr('y1', d => d.source.y) .attr('x2', d => d.target.x).attr('y2', d => d.target.y); node.attr('transform', d => `translate(${d.x},${d.y})`); });
elements 배열에 노드와 엣지를 함께 넣음. 각각 { data: { id } }·{ data: { source, target } } 형태.// ① CDN: Cytoscape + dagre + cytoscape-dagre 어댑터 // <script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script> // <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script> // <script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script> // ② <div id="net5" style="height:480px"></div> cytoscape.use(cytoscapeDagre); // 어댑터 등록 // ③ 데이터 준비 ─── 노드와 엣지를 한 elements 배열에 const elements = [ { data: { id:'전국' } }, { data: { id:'수도권' } }, { data: { id:'영남' } }, { data: { id:'서울' } }, { data: { id:'경기' } }, { data: { id:'부산' } }, { data: { id:'대구' } }, { data: { source:'전국', target:'수도권' } }, { data: { source:'전국', target:'영남' } }, { data: { source:'수도권', target:'서울' } }, { data: { source:'수도권', target:'경기' } }, { data: { source:'영남', target:'부산' } }, { data: { source:'영남', target:'대구' } } ]; // ④ Cytoscape 인스턴스 생성 const cy = cytoscape({ container: document.getElementById('net5'), elements, style: [ { selector: 'node', style: { 'background-color': '#4338ca', 'label': 'data(id)', 'color': 'white', 'text-valign': 'center', 'text-halign': 'center', 'font-size': 11, 'width': 52, 'height': 52 }}, { selector: 'edge', style: { 'width': 2, 'line-color': '#94a3b8', 'curve-style': 'bezier', 'target-arrow-shape': 'triangle' }} ], layout: { name: 'dagre', rankDir: 'TB' } // 다른 옵션: cose/circle/grid/... }); // 레이아웃 즉시 변경 // cy.layout({ name: 'cose', animate: true }).run();
add()/remove()/update()만 호출하면 화면 자동 갱신.localStorage 필요.add()·remove()·update()·getConnectedNodes().// ② <div id="net6" style="height:480px"></div> // ③ DataSet은 변경 가능한 컬렉션 ─── add/remove/update가 화면 즉시 갱신 const nodes = new vis.DataSet([ { id:1, label:'A', color:'#3b82f6' }, { id:2, label:'B', color:'#10b981' }, { id:3, label:'C', color:'#ef4444' } ]); const edges = new vis.DataSet([ { id:'e1', from:1, to:2 }, { id:'e2', from:2, to:3 } ]); // ④ 네트워크 생성 const net = new vis.Network(document.getElementById('net6'), { nodes, edges }, { nodes: { shape: 'dot', size: 24 }, interaction: { multiselect: true, selectConnectedEdges: true } }); // ─── 핵심 API 4가지 ─── // 1) 노드 추가 ─── add 호출 즉시 화면에 반영 nodes.add({ id: 99, label: 'NEW', color: '#f59e0b' }); // 2) 엣지 추가 edges.add({ id: 'e99', from: 1, to: 99 }); // 3) 선택된 항목 삭제 const sel = net.getSelection(); // { nodes:[...], edges:[...] } sel.nodes.forEach(id => nodes.remove(id)); sel.edges.forEach(id => edges.remove(id)); // 4) 클릭 → 이웃만 강조 net.on('click', params => { if (params.nodes.length === 0) return; const id = params.nodes[0]; const neighbors = net.getConnectedNodes(id); nodes.update(nodes.get().map(n => ({ id: n.id, opacity: (n.id === id || neighbors.includes(n.id)) ? 1 : 0.25 }))); }); // 5) 화면 맞춤 net.fit({ animation: true });
// ② <textarea id="sem-text">...</textarea> + <div id="net7" style="height:480px"></div> // ③ 데이터 준비 ─── 텍스트만 자기 데이터로 교체! const text = document.getElementById('sem-text').value; // 1) 문장 단위로 쪼개기 const sentences = text.split(/[.!?\n]+/).map(s => s.trim()).filter(Boolean); // 2) 토큰화 + 불용어 제거 const stop = new Set(['은','는','이','가','을','를','의','에','와','과']); function tokens(s) { return s.split(/[\s,·\-—()【】\[\]"'?!:;]+/) .filter(w => w.length >= 2 && !stop.has(w)); } // 3) 단어 빈도 + 단어 쌍 동시출현 카운트 const freq = {}, pair = {}; sentences.forEach(s => { const ws = [...new Set(tokens(s))]; // 같은 문장 내 중복 제거 ws.forEach(w => { freq[w] = (freq[w]||0) + 1; }); for (let i=0; i<ws.length; i++) for (let j=i+1; j<ws.length; j++) { const k = [ws[i], ws[j]].sort().join('|'); pair[k] = (pair[k]||0) + 1; } }); // 4) 상위 N개 단어 → 노드, 동시출현 ≥ 2 → 엣지 const topN = 25, minPair = 2; const top = Object.entries(freq).sort((a,b)=>b[1]-a[1]).slice(0, topN); const wordSet = new Set(top.map(w => w[0])); const nodes = top.map(([w,c]) => ({ id: w, label: w, value: c })); const edges = Object.entries(pair) .filter(([k,v]) => v >= minPair && k.split('|').every(w => wordSet.has(w))) .map(([k,v]) => { const [a,b] = k.split('|'); return { from:a, to:b, value:v }; }); // ④ 네트워크 생성 ─── force-directed 자동 배치 new vis.Network(document.getElementById('net7'), { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) }, { nodes: { shape: 'dot', scaling: { min: 12, max: 50 } }, edges: { scaling: { min: 1, max: 8 }, color: { color: '#cbd5e1' } }, physics: { barnesHut: { gravitationalConstant: -3500, springLength: 140 } } } );