React 목록으로

D3 라이브러리 React에서 사용하는 방법

React

D3 라이브러리란?

D3 라이브러리는

데이터를 기반으로 DOM을 조작하는 시각화 라이브러리

입니다. 그렇기에 일반적으로 차트 라이브러리라고 오해를 많이 하지만 실제로 D3는 데이터, 좌표, 도형, 애니메이션 전부 직접 제어할 수 있습니다.



D3 기본 사용법

크게 두 가지 패턴이 있습니다.

D3가 DOM을 직접 조작하게 하는 방법 (전통적인 방식)

TYPESCRIPT
1import { 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는 계산만 하는 방법 (추천)

TYPESCRIPT
1import * 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에서는 이런 코드를 통해 변환할 수 있습니다.

TYPESCRIPT
1const 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를 반대로 써야 차트가 위로 올라갑니다.

TYPESCRIPT
1const 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의 마진은 객체로 정의하는 것이 관례입니다.

TYPESCRIPT
1const margin = { top: 20, right: 20, bottom: 40, left: 40 } 2const width = 500 - margin.left - margin.right // 실제 차트 너비 3const height = 300 - margin.top - margin.bottom // 실제 차트 높이

이런식으로 margin을 빼는 이유는 축 레이블이 잘리지 않게 여백을 확보해야 하기 때문입니다.

TSX
1<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를 활용합니다.

TYPESCRIPT
1const 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> 태그를 사용합니다.

TSX
1{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()입니다.

TYPESCRIPT
1const lineGenerator = d3.line() 2 .x(d => xScale(d.x)) 3 .y(d => yScale(d.y))

전체 코드는 이런 형태가 됩니다.

TSX
1const 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가 중심점입니다

TSX
1{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))}

전체 코드는 이러한 모습이 됩니다.

TSX
1const 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로 변환
TYPESCRIPT
1const 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)

전체 코드는 이러한 모습이 됩니다.

TSX
1const 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}