레슨 7 / 8·25분
실전: 인터랙티브 데이터 시각화
실전 프로젝트: 인터랙티브 데이터 대시보드
지금까지 배운 D3.js 기능을 종합하여 인터랙티브 데이터 시각화 대시보드를 구축합니다. 막대 차트, 선 차트, 파이 차트를 결합하고 필터링, 트랜지션, 툴팁 등을 구현합니다.
1단계: 프로젝트 구조 설정
javascript
// 대시보드 설정
var config = {
width: 900,
height: 500,
margin: { top: 40, right: 30, bottom: 50, left: 60 },
};
config.innerWidth = config.width - config.margin.left - config.margin.right;
config.innerHeight = config.height - config.margin.top - config.margin.bottom;
// 데이터
var dataset = [
{ month: '1월', sales: 150, target: 120, category: 'A' },
{ month: '2월', sales: 230, target: 200, category: 'B' },
{ month: '3월', sales: 180, target: 150, category: 'A' },
{ month: '4월', sales: 310, target: 280, category: 'C' },
{ month: '5월', sales: 270, target: 250, category: 'B' },
{ month: '6월', sales: 350, target: 300, category: 'A' },
{ month: '7월', sales: 290, target: 270, category: 'C' },
{ month: '8월', sales: 400, target: 350, category: 'B' },
{ month: '9월', sales: 330, target: 310, category: 'A' },
{ month: '10월', sales: 450, target: 400, category: 'C' },
{ month: '11월', sales: 380, target: 350, category: 'B' },
{ month: '12월', sales: 500, target: 450, category: 'A' },
];2단계: 메인 막대 차트 생성
javascript
// SVG 컨테이너
var svg = d3.select('#dashboard')
.append('svg')
.attr('width', config.width)
.attr('height', config.height);
var chartGroup = svg.append('g')
.attr('transform',
'translate(' + config.margin.left + ',' + config.margin.top + ')');
// 스케일
var xScale = d3.scaleBand()
.domain(dataset.map(function(d) { return d.month; }))
.range([0, config.innerWidth])
.padding(0.3);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.sales; }) * 1.1])
.range([config.innerHeight, 0])
.nice();
var categoryColor = d3.scaleOrdinal()
.domain(['A', 'B', 'C'])
.range(['#6366f1', '#10b981', '#f59e0b']);
// 축
chartGroup.append('g')
.attr('class', 'x-axis')
.attr('transform', 'translate(0,' + config.innerHeight + ')')
.call(d3.axisBottom(xScale));
chartGroup.append('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(yScale).ticks(8)
.tickFormat(function(d) { return d + '만원'; }));
// 제목
svg.append('text')
.attr('x', config.width / 2)
.attr('y', 25)
.attr('text-anchor', 'middle')
.attr('font-size', '18px')
.attr('font-weight', 'bold')
.text('월별 매출 분석 대시보드');3단계: 인터랙티브 막대 그리기
javascript
// 툴팁 요소
var tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('padding', '10px 14px')
.style('background', 'rgba(0, 0, 0, 0.85)')
.style('color', '#fff')
.style('border-radius', '6px')
.style('font-size', '13px')
.style('pointer-events', 'none')
.style('opacity', 0);
// 목표선 그리기
var targetLine = d3.line()
.x(function(d) { return xScale(d.month) + xScale.bandwidth() / 2; })
.y(function(d) { return yScale(d.target); })
.curve(d3.curveMonotoneX);
chartGroup.append('path')
.datum(dataset)
.attr('d', targetLine)
.attr('fill', 'none')
.attr('stroke', '#ef4444')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '6,4');
// 막대 그리기 (애니메이션 포함)
chartGroup.selectAll('.bar')
.data(dataset)
.join('rect')
.attr('class', 'bar')
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.bandwidth())
.attr('y', config.innerHeight)
.attr('height', 0)
.attr('fill', function(d) { return categoryColor(d.category); })
.attr('rx', 3)
.on('mouseover', function(event, d) {
d3.select(this)
.transition().duration(150)
.attr('opacity', 0.8);
var diff = d.sales - d.target;
var status = diff >= 0 ? '달성' : '미달';
tooltip.transition().duration(200).style('opacity', 1);
tooltip.html(
'<strong>' + d.month + '</strong><br>' +
'매출: ' + d.sales + '만원<br>' +
'목표: ' + d.target + '만원<br>' +
'차이: ' + (diff >= 0 ? '+' : '') + diff + '만원 (' + status + ')'
)
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 20) + 'px');
})
.on('mouseout', function() {
d3.select(this)
.transition().duration(150)
.attr('opacity', 1);
tooltip.transition().duration(300).style('opacity', 0);
})
// 등장 애니메이션
.transition()
.duration(800)
.delay(function(d, i) { return i * 60; })
.ease(d3.easeCubicOut)
.attr('y', function(d) { return yScale(d.sales); })
.attr('height', function(d) {
return config.innerHeight - yScale(d.sales);
});4단계: 카테고리 필터 구현
javascript
// 필터 버튼 생성
var categories = ['전체', 'A', 'B', 'C'];
var filterGroup = d3.select('#filters');
filterGroup.selectAll('.filter-btn')
.data(categories)
.join('button')
.attr('class', 'filter-btn')
.style('padding', '6px 16px')
.style('margin', '0 4px')
.style('border', '1px solid #ddd')
.style('border-radius', '4px')
.style('cursor', 'pointer')
.style('background', function(d) {
return d === '전체' ? '#6366f1' : '#fff';
})
.style('color', function(d) {
return d === '전체' ? '#fff' : '#333';
})
.text(function(d) {
return d === '전체' ? '전체 보기' : '카테고리 ' + d;
})
.on('click', function(event, category) {
// 버튼 스타일 업데이트
filterGroup.selectAll('.filter-btn')
.style('background', '#fff')
.style('color', '#333');
d3.select(this)
.style('background', '#6366f1')
.style('color', '#fff');
// 필터링된 데이터
var filtered = category === '전체'
? dataset
: dataset.filter(function(d) { return d.category === category; });
// 차트 업데이트 (트랜지션)
updateBars(filtered);
});
function updateBars(data) {
// 스케일 업데이트
xScale.domain(data.map(function(d) { return d.month; }));
yScale.domain([0, d3.max(data, function(d) { return d.sales; }) * 1.1]).nice();
// 축 애니메이션
chartGroup.select('.x-axis')
.transition().duration(500)
.call(d3.axisBottom(xScale));
chartGroup.select('.y-axis')
.transition().duration(500)
.call(d3.axisLeft(yScale).ticks(8)
.tickFormat(function(d) { return d + '만원'; }));
// 막대 업데이트
var bars = chartGroup.selectAll('.bar').data(data, function(d) { return d.month; });
bars.exit()
.transition().duration(300)
.attr('y', config.innerHeight)
.attr('height', 0)
.attr('opacity', 0)
.remove();
bars.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.bandwidth())
.attr('y', config.innerHeight)
.attr('height', 0)
.attr('fill', function(d) { return categoryColor(d.category); })
.attr('rx', 3)
.merge(bars)
.transition().duration(600)
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.bandwidth())
.attr('y', function(d) { return yScale(d.sales); })
.attr('height', function(d) {
return config.innerHeight - yScale(d.sales);
})
.attr('opacity', 1);
}💡
실전 프로젝트에서는 데이터 로딩(d3.csv/json), 반응형 디자인(ResizeObserver), 접근성(aria 속성) 등을 추가로 고려해야 합니다. 재사용 가능한 차트 컴포넌트로 만들면 유지보수가 편리합니다.