D3 라이브러리란?
D3 라이브러리는
데이터를 기반으로 DOM을 조작하는 시각화 라이브러리
입니다. 그렇기에 일반적으로 차트 라이브러리라고 오해를 많이 하지만 실제로 D3는 데이터, 좌표, 도형, 애니메이션 전부 직접 제어할 수 있습니다.
D3 기본 사용법
크게 두 가지 패턴이 있습니다.
D3가 DOM을 직접 조작하게 하는 방법 (전통적인 방식)
TYPESCRIPT1import { useEffect, useRef } from "react"; 2import * as d3 from "d3"; 3 4export default function Chart() { 5 const ref = useRef<SVGSVGElement | null>(null); 6 7 useEffect(() => { 8 const svg = d3.select(ref.current); 9 10 svg 11 .append("circle") 12 .attr("cx", 50) 13 .attr("cy", 50) 14 .attr("r", 40) 15 .attr("fill", "blue"); 16 }, []); 17 18 return <svg ref={ref} width={200} height={200}></svg>; 19}
React가 svg를 생성하고 useEffect에서 D3가 돔을 직접 조작하는 것을 볼 수 있습니다. 하지만 이 방식은 React와 D3가 돔을 동시에 관리해서 충돌 위험이 있기에 일반적으로 두 번째 방식을 선호합니다.
React가 DOM을 관리하고 D3는 계산만 하는 방법 (추천)
TYPESCRIPT1import * as d3 from "d3"; 2 3export default function Chart() { 4 const data = [10, 20, 30, 40]; 5 6 const scale = d3.scaleLinear() 7 .domain([0, 40]) 8 .range([0, 200]); 9 10 return ( 11 <svg width={300} height={200}> 12 {data.map((d, i) => ( 13 <rect 14 key={i} 15 x={i * 60} 16 y={200 - scale(d)} 17 width={50} 18 height={scale(d)} 19 fill="teal" 20 /> 21 ))} 22 </svg> 23 ); 24}
D3가 scale, axis, layout을 계산하고 React는 rect에 그 계산 값을 넣어서 관리하는 것을 볼 수 있습니다.
D3 scaleLinear
스케일은 데이터를 화면 좌표(px)로 전환하는 역할을 수행합니다. 예를 들어
const data = [10, 50, 100];
의 데이터가 있을 경우 10 -> 20px, 50-> 120px, 100 -> 200px로 바꾸어 그래프화 시켜줍니다.
이를 D3에서는 이런 코드를 통해 변환할 수 있습니다.
TYPESCRIPT1const yScale = d3 2 .scaleLinear() 3 .domain([0, 100]) 4 .range([200, 0]);
- scaleLinear : 선형 변환 함수 생성기
- domain : 데이터의 범위([최소값, 최대값])
- range : 화면 좌표 범위([최소값px, 최대값px])
실전에서는 domain을 하드코딩하지 않고
d3.min(),d3max()로 자동계산합니다. 또한 SVG는 위쪽이 0이고 아래로 갈수록 숫자가 커지는 구조라서 y축은 range를 반대로 써야 차트가 위로 올라갑니다.
TYPESCRIPT1const width = 500 2const height = 300 3 4const data = [10, 40, 25, 80, 60] 5const xScale = d3.scaleLinear().domain([0, d3.max(data)]).range([0, width]) // 그대로 6const yScale = d3.scaleLinear().domain([0, d3.max(data)]).range([height, 0]) // 뒤집기
공통세팅
D3의 마진은 객체로 정의하는 것이 관례입니다.
TYPESCRIPT1const margin = { top: 20, right: 20, bottom: 40, left: 40 } 2const width = 500 - margin.left - margin.right // 실제 차트 너비 3const height = 300 - margin.top - margin.bottom // 실제 차트 높이
이런식으로 margin을 빼는 이유는 축 레이블이 잘리지 않게 여백을 확보해야 하기 때문입니다.
TSX1<svg width={width + margin.left + margin.right} 2 height={height + margin.top + margin.bottom}> 3 <g transform={`translate(${margin.left}, ${margin.top})`}> 4 {/* 여기에 차트 요소들 */} 5 </g> 6</svg>
바깥 <svg>로 전체 크기를 확보한 후 <g>를 마진만큼 이동시켜서 실제 차트 영역을 확보합니다.
차트 사용
Bar Chart
우선, Bar Chart의 x축은 숫자가 아니라 카테고리가 들어가기에 scaleBand를 활용합니다.
TYPESCRIPT1const data = [ 2 { label: 'A', value: 30 }, 3 { label: 'B', value: 80 }, 4 { label: 'C', value: 45 }, 5 { label: 'D', value: 60 }, 6] 7 8const xScale = d3.scaleBand() 9 .domain(data.map(d => d.label)) // ['A', 'B', 'C', 'D'] 10 .range([0, width]) 11 .padding(0.2) // 막대 사이 간격 12 13const yScale = d3.scaleLinear() 14 .domain([0, d3.max(data, d => d.value)]) 15 .range([height, 0])
막대는 <rect> 태그를 사용합니다.
TSX1{data.map(d => ( 2 <rect 3 key={d.label} 4 x={xScale(d.label)} 5 y={yScale(d.value)} 6 width={xScale.bandwidth()} // scaleBand가 자동 계산 7 height={height - yScale(d.value)} 8 /> 9))}
각 속성을 뜯어보면
- x : 막대 시작 x 위치
- y : 막대 시작 y 위치
- width : 막대 너비, bandWidth()가 자동 계산
- height : 막대 높이, height - yScale(d.value) 로 계산
이 됩니다.
Line Chart
Bar Chart는 <rect>를 여러 개 그렸지만, Line Chart는 점들을 이어주는 하나의 <path>를 그리게 됩니다. 이것을 이어주는 것이 d3.line()입니다.
TYPESCRIPT1const lineGenerator = d3.line() 2 .x(d => xScale(d.x)) 3 .y(d => yScale(d.y))
전체 코드는 이런 형태가 됩니다.
TSX1const LineChart = ({ data }) => { 2 const margin = { top: 20, right: 20, bottom: 40, left: 40 } 3 const width = 500 - margin.left - margin.right 4 const height = 300 - margin.top - margin.bottom 5 6 const xScale = d3.scaleLinear() 7 .domain([0, d3.max(data, d => d.x)]) 8 .range([0, width]) 9 10 const yScale = d3.scaleLinear() 11 .domain([0, d3.max(data, d => d.y)]) 12 .range([height, 0]) 13 14 const lineGenerator = d3.line() 15 .x(d => xScale(d.x)) 16 .y(d => yScale(d.y)) 17 18 return ( 19 <svg width={width + margin.left + margin.right} 20 height={height + margin.top + margin.bottom}> 21 <g transform={`translate(${margin.left}, ${margin.top})`}> 22 <path 23 d={lineGenerator(data)} 24 fill="none" 25 stroke="steelblue" 26 strokeWidth={2} 27 /> 28 </g> 29 </svg> 30 ) 31}
Scatter Plot
x, y 둘 다 scaleLinear에 <circle>태그를 달면 끝입니다.
주의! rect는 x,y가 왼쪽 위 꼭짓점이지만 circle은 cx, cy가 중심점입니다
TSX1{data.map((d, i) => ( 2 <circle 3 key={i} 4 cx={xScale(d.x)} // 중심 x 5 cy={yScale(d.y)} // 중심 y 6 r={5} // 반지름 7 fill="steelblue" 8 /> 9))}
전체 코드는 이러한 모습이 됩니다.
TSX1const ScatterPlot = ({ data }) => { 2 const margin = { top: 20, right: 20, bottom: 40, left: 40 } 3 const width = 500 - margin.left - margin.right 4 const height = 300 - margin.top - margin.bottom 5 6 const xScale = d3.scaleLinear() 7 .domain([0, d3.max(data, d => d.x)]) 8 .range([0, width]) 9 10 const yScale = d3.scaleLinear() 11 .domain([0, d3.max(data, d => d.y)]) 12 .range([height, 0]) 13 14 return ( 15 <svg width={width + margin.left + margin.right} 16 height={height + margin.top + margin.bottom}> 17 <g transform={`translate(${margin.left}, ${margin.top})`}> 18 {data.map((d, i) => ( 19 <circle 20 key={i} 21 cx={xScale(d.x)} 22 cy={yScale(d.y)} 23 r={5} 24 fill="steelblue" 25 /> 26 ))} 27 </g> 28 </svg> 29 ) 30}
Pie Chart
파이차트는 x y 좌표 개념이 없는 대신 d3.pie()와 d3.arc()를 사용합니다.
- d3.pie() : 데이터를 각도로 변환
- d3.arc() : 각도를 svg path로 변환
TYPESCRIPT1const pie = d3.pie() 2 .value(d => d.value) 3 4const arc = d3.arc() 5 .innerRadius(0) // 0이면 파이차트, 값이 있으면 도넛차트 6 .outerRadius(150) // 차트 반지름 7 8const arcs = pie(data)
전체 코드는 이러한 모습이 됩니다.
TSX1const PieChart = ({ data }) => { 2 const width = 400 3 const height = 400 4 const radius = Math.min(width, height) / 2 5 6 const pie = d3.pie().value(d => d.value) 7 const arc = d3.arc().innerRadius(0).outerRadius(radius) 8 const arcs = pie(data) 9 10 const colors = d3.schemeTableau10 // D3 기본 색상 팔레트 11 12 return ( 13 <svg width={width} height={height}> 14 <g transform={`translate(${width / 2}, ${height / 2})`}> 15 {arcs.map((d, i) => ( 16 <path 17 key={i} 18 d={arc(d)} 19 fill={colors[i]} 20 /> 21 ))} 22 </g> 23 </svg> 24 ) 25}