D3.js로 인터랙티브 시각화 구현하기
D3.js로 인터랙티브 시각화 구현하기
이벤트 핸들링 기초
D3.js는 DOM 이벤트를 쉽게 처리할 수 있는 방법을 제공한다.
1. 기본 이벤트 리스너
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 기본 이벤트 바인딩
d3.select('circle')
.on('mouseover', function(event, d) {
// event: 이벤트 객체
// d: 바인딩된 데이터
d3.select(this)
.style('fill', 'red');
})
.on('mouseout', function(event, d) {
d3.select(this)
.style('fill', 'steelblue');
});
// 여러 요소에 이벤트 바인딩
svg.selectAll('circle')
.data(data)
.join('circle')
.on('click', function(event, d) {
console.log('클릭된 데이터:', d);
});
2. 이벤트 전파와 기본 동작 제어
1
2
3
4
5
6
7
8
9
10
11
// 이벤트 전파 중단
.on('click', function(event, d) {
event.stopPropagation();
// 처리 로직
});
// 기본 동작 방지
.on('click', function(event, d) {
event.preventDefault();
// 처리 로직
});
동적 요소 업데이트
1. 트랜지션(Transition) 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 기본 트랜지션
d3.select('circle')
.transition()
.duration(1000) // 1초
.attr('r', 30)
.style('fill', 'red');
// 트랜지션 체이닝
d3.select('circle')
.transition()
.duration(1000)
.attr('r', 30)
.transition()
.duration(500)
.attr('r', 10);
// 지연 효과
d3.selectAll('circle')
.transition()
.delay((d, i) => i * 100) // 순차적 애니메이션
.duration(1000)
.attr('r', d => d.value);
2. 데이터 기반 업데이트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function updateChart(newData) {
// 데이터 업데이트
const circles = svg.selectAll('circle')
.data(newData);
// 새로운 요소 추가
circles.enter()
.append('circle')
.attr('r', 0)
.merge(circles) // 기존 요소와 병합
.transition()
.duration(1000)
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 5);
// 제거될 요소
circles.exit()
.transition()
.duration(1000)
.attr('r', 0)
.remove();
}
툴팁 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 1. 툴팁 div 생성
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('visibility', 'hidden')
.style('background-color', 'white')
.style('border', '1px solid #ddd')
.style('padding', '10px')
.style('border-radius', '3px')
.style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
.style('font-size', '12px');
// 2. 차트 요소에 툴팁 이벤트 추가
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 5)
.on('mouseover', function(event, d) {
// 요소 하이라이트
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
.style('fill', 'red');
// 툴팁 표시
tooltip
.style('visibility', 'visible')
.html(`
<strong>${d.category}</strong><br>
X: ${d.x}<br>
Y: ${d.y}<br>
값: ${d.value}
`);
})
.on('mousemove', function(event) {
// 마우스 움직임에 따라 툴팁 위치 업데이트
tooltip
.style('top', (event.pageY - 10) + 'px')
.style('left', (event.pageX + 10) + 'px');
})
.on('mouseout', function() {
// 요소 원래대로
d3.select(this)
.transition()
.duration(200)
.attr('r', 5)
.style('fill', 'steelblue');
// 툴팁 숨기기
tooltip.style('visibility', 'hidden');
});
Zoom과 Pan 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 줌 행동 정의
const zoom = d3.zoom()
.scaleExtent([0.5, 5]) // 최소/최대 줌 레벨
.on('zoom', zoomed);
// SVG에 줌 적용
svg.call(zoom);
// 줌 이벤트 핸들러
function zoomed(event) {
// 현재 변환 정보
const transform = event.transform;
// 요소들 변환
svg.selectAll('circle')
.attr('transform', transform);
// 축 업데이트
svg.select('.x-axis')
.call(xAxis.scale(transform.rescaleX(xScale)));
svg.select('.y-axis')
.call(yAxis.scale(transform.rescaleY(yScale)));
}
// 특정 영역으로 줌
function zoomTo(x, y, k) {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity
.translate(x, y)
.scale(k));
}
실전 예제: 인터랙티브 산점도
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 데이터
const data = [
{x: 10, y: 20, category: 'A', value: 15},
{x: 15, y: 25, category: 'B', value: 22},
// ... 더 많은 데이터
];
// 스케일 설정
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.y))
.range([height - margin.bottom, margin.top]);
// 차트 생성
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
// 툴팁 생성
const tooltip = createTooltip(svg);
// 점 그리기
const circles = svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', 5)
.style('fill', 'steelblue')
.style('cursor', 'pointer')
// 마우스 이벤트
.on('mouseover', function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
.style('fill', 'red');
updateTooltip(tooltip, d, [
xScale(d.x) + 10,
yScale(d.y) - 10
]);
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.attr('r', 5)
.style('fill', 'steelblue');
tooltip.style('display', 'none');
})
.on('click', function(event, d) {
// 클릭 시 줌
const k = 2; // 줌 레벨
const x = xScale(d.x);
const y = yScale(d.y);
zoomTo(width/2 - x*k, height/2 - y*k, k);
});
// 줌 기능 추가
svg.call(zoom);
This post is licensed under CC BY 4.0 by the author.