레슨 5 / 8·20분
레이아웃 (트리, 포스, 파이)
D3 레이아웃이란?
D3 레이아웃은 원본 데이터를 특정 시각화 형태에 맞는 좌표와 크기로 변환하는 알고리즘입니다. 데이터를 넣으면 시각화에 필요한 x, y 좌표, 크기, 각도 등을 자동으로 계산해 줍니다.
트리 레이아웃 (Tree Layout)
트리 레이아웃은 계층 구조 데이터를 트리 형태로 시각화합니다. 조직도, 파일 시스템, 카테고리 분류 등에 적합합니다.
javascript
// 계층 구조 데이터
var treeData = {
name: 'CEO',
children: [
{
name: 'CTO',
children: [
{ name: '프론트엔드팀' },
{ name: '백엔드팀' },
{ name: 'DevOps팀' },
],
},
{
name: 'CFO',
children: [
{ name: '재무팀' },
{ name: '회계팀' },
],
},
{
name: 'CMO',
children: [
{ name: '마케팅팀' },
{ name: '영업팀' },
],
},
],
};
var width = 600, height = 400;
var svg = d3.select('#tree')
.append('svg')
.attr('width', width)
.attr('height', height);
var g = svg.append('g')
.attr('transform', 'translate(50, 30)');
// 계층 구조 생성
var root = d3.hierarchy(treeData);
// 트리 레이아웃 적용
var treeLayout = d3.tree()
.size([width - 100, height - 80]);
treeLayout(root);
// 연결선 그리기
g.selectAll('.link')
.data(root.links())
.join('path')
.attr('class', 'link')
.attr('d', d3.linkVertical()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; }))
.attr('fill', 'none')
.attr('stroke', '#999')
.attr('stroke-width', 1.5);
// 노드 그리기
var nodes = g.selectAll('.node')
.data(root.descendants())
.join('g')
.attr('class', 'node')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
nodes.append('circle')
.attr('r', 6)
.attr('fill', function(d) {
return d.children ? '#4f46e5' : '#10b981';
});
nodes.append('text')
.attr('dy', -12)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.text(function(d) { return d.data.name; });포스 레이아웃 (Force Layout)
포스 레이아웃은 물리 시뮬레이션을 사용하여 노드를 배치합니다. 노드 사이의 인력과 척력으로 자연스러운 네트워크 그래프를 만듭니다.
javascript
var nodes = [
{ id: 'React', group: 1 },
{ id: 'Vue', group: 1 },
{ id: 'Angular', group: 1 },
{ id: 'Node.js', group: 2 },
{ id: 'Express', group: 2 },
{ id: 'Next.js', group: 3 },
{ id: 'Nuxt', group: 3 },
{ id: 'TypeScript', group: 4 },
];
var links = [
{ source: 'React', target: 'Next.js' },
{ source: 'Vue', target: 'Nuxt' },
{ source: 'Node.js', target: 'Express' },
{ source: 'React', target: 'TypeScript' },
{ source: 'Vue', target: 'TypeScript' },
{ source: 'Angular', target: 'TypeScript' },
{ source: 'Next.js', target: 'Node.js' },
];
var width = 500, height = 400;
var svg = d3.select('#force')
.append('svg')
.attr('width', width)
.attr('height', height);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
// 포스 시뮬레이션 생성
var simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(function(d) { return d.id; }).distance(80))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(30));
// 연결선
var link = svg.selectAll('.link')
.data(links)
.join('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', 2);
// 노드
var node = svg.selectAll('.node')
.data(nodes)
.join('g');
node.append('circle')
.attr('r', 15)
.attr('fill', function(d) { return colorScale(d.group); })
.attr('stroke', '#fff')
.attr('stroke-width', 2);
node.append('text')
.text(function(d) { return d.id; })
.attr('text-anchor', 'middle')
.attr('dy', 30)
.attr('font-size', '11px');
// 시뮬레이션 갱신
simulation.on('tick', function() {
link
.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
node.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});파이 레이아웃 (Pie Layout)
javascript
var data = [
{ label: 'JavaScript', value: 40 },
{ label: 'TypeScript', value: 30 },
{ label: 'Python', value: 20 },
{ label: 'Go', value: 10 },
];
var width = 400, height = 400;
var radius = Math.min(width, height) / 2;
var svg = d3.select('#pie')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var color = d3.scaleOrdinal()
.domain(data.map(function(d) { return d.label; }))
.range(d3.schemeSet2);
// 파이 레이아웃
var pie = d3.pie()
.value(function(d) { return d.value; })
.sort(null);
// 호(arc) 생성기
var arc = d3.arc()
.innerRadius(0) // 0이면 파이, > 0이면 도넛
.outerRadius(radius - 20);
var labelArc = d3.arc()
.innerRadius(radius - 60)
.outerRadius(radius - 60);
// 파이 조각 그리기
svg.selectAll('.slice')
.data(pie(data))
.join('path')
.attr('d', arc)
.attr('fill', function(d) { return color(d.data.label); })
.attr('stroke', '#fff')
.attr('stroke-width', 2);
// 라벨 추가
svg.selectAll('.label')
.data(pie(data))
.join('text')
.attr('transform', function(d) {
return 'translate(' + labelArc.centroid(d) + ')';
})
.attr('text-anchor', 'middle')
.attr('font-size', '13px')
.text(function(d) { return d.data.label; });- •d3.hierarchy() — 원시 데이터를 계층 구조 객체로 변환
- •d3.tree() — 트리 레이아웃 (정돈된 수직/수평 트리)
- •d3.cluster() — 클러스터 레이아웃 (리프 노드가 같은 깊이)
- •d3.treemap() — 트리맵 (면적 비례 사각형)
- •d3.pack() — 원형 패킹 레이아웃
- •d3.forceSimulation() — 물리 기반 포스 레이아웃
- •d3.pie() — 파이/도넛 레이아웃
- •d3.arc() — 호(arc) 경로 생성기
💡
포스 레이아웃에 d3.drag()를 추가하면 사용자가 노드를 드래그하여 재배치할 수 있습니다. node.call(d3.drag().on("start", ...).on("drag", ...).on("end", ...)) 패턴으로 구현합니다.