이제 본격적으로 d3를 이용하여 어떻게 라인 차트를 그릴 수 있는지 알아보도록 하겠습니다.
물론, HTML, CSS, JS만으로도 충분히 그리는게 가능하지만,
저는 Next.js를 사용하여 라인차트를 그려 보겠습니다.
만약, svg에 대해 전혀 모르신다면 이전에 작성한 글을 참고하고 오시는 걸 추천드립니다.
포스트의 구성은 다음과 같습니다.
- 사전준비(설치, import)
- d3 사용
2-1. D3 기본 Flow
3-1. 라인차트 그리기- React에서 d3 사용하기
- 라인차트 그리기
- 최종 코드
1. 사전 준비 - prerequisite
[ installation ]
가장 먼저 npm을 이용하여, 설치 해줍니다. (참고링크1 - d3: npm)
npm install d3
물론, 아래처럼 바로 script에서 링크를 걸어 사용할 수도 있지만, 저는 위의 방식처럼 npm을 이용하여 설치하도록 하겠습니다.
<script src="http://d3js.org/d3.v3.min.js"></script>
[ import ]
앞서 설치한 d3는 아래처럼 import하여 사용할 수 있습니다.
import * as d3 from "d3";
const div = d3.selectAll("div");
2. D3 사용
이제 우리가 d3를 사용하기 위한 사전 준비과정은 모두 끝났습니다.
본격적으로 d3사용하여 라인차트를 함께 만들어 보도록 하겠습니다.
2-1. D3 기본 flow
d3는 크게 다음의 일련의 과정을 통해 우리가 데이터 Visualization하는 것을 도와줍니다.
(DOM에 대한 개념이 전혀 없거나, 자세히 알고싶다면 다음 링크(mdn - DOM)를 참고해주시기 바랍니다.)
- DOM Selection
d3를 사용할 때 가장 선행되어야 할 작업입니다. 내가 data를 보여주고 싶은 element를 선택합니다.
(methods - select, selectAll, selection 등 (참고링크 - select Api)) - DOM Manipulation
이제 선택한 DOM element를 d3를 이용하여 내가 원하는대로 변화 시킬 수 있다.
(methods - text(), append(), insert(), remove(), attr(), property(), style(), classed())
각 메소드에 대해서는 밑에서 자세히 알아보도록 하겠습니다. - Data Loading
우리가 d3를 쓰는 대부분의 이유는 가지고 있는 data를 효과적으로 보여주기 위함입니다.
이때,d3는 우리가 외부 data를 load하는 것을 도와주는 method를 제공합니다.
(methods - d3.csv(), d3.json(), d3.tsv(), d3.xml()) - Data Binding
사실상 d3 flow에서 가장 핵심이 되는 부분입니다.
앞서 load한 데이터를 DOM element에 binding하는 과정입니다.
(methods - data(), enter(), exit(), datum() (참고링크 - dataJoin Api))
물론, 위의 프로세스 이외에도 Scaling, Axis control, Animation 등과 같은 정말 많은 다루지 않은 부분이 있겠지만,
우선 가장 필수적인 process인 위의 4가지를 먼저 기억해주시고, 부차적인 과정은 등장 할 때마다 그때 그때 다뤄보겠습니다.
위의 4가지 과정은 정말 중요하니 꼭 기억해 주세요!
2-2. 라인차트 그리기
사실, 위의 과정들마다 하나씩 어떤 메소드가 있고, 어떤 역할을 하는지 설명을 하려고 했지만
바로 실제 코드를 보면서, 각 과정마다 어떤 메소드가 어떻게 사용되는지 정리하는 것이 더 효율적이라 생각되어 바로 코드를 보며 들어가보도록 하겠습니다.
[ React에서 d3 사용하기 ]
아! 본격적으로 들어가기 전에, 앞서 말한 것 처럼 저는 React(Nextjs)를 사용 할 것입니다.
이때 조금 생각해 볼 점이 있습니다. React, D3 모두 DOM을 조작을 할 수 있는데, 누구에게 지휘권(?)을 줄 것인가에 대한 부분입니다.
이에 대해 꽤나 자세히 얘기하고 있는 영상이 있어 링크를 남깁니다.(Who Needs Control of the DOM? - Shirley Wu)
영상의 결론은, D3는 아래 총 3가지 경우에만 DOM에 접근할 필요가 있다는 것입니다.
- transition (for animate)
- axes
- brushes
그리고 위 3가지를 제외한 나머지 경우는 React가 전담 하는 것이 React DOM을 벗어나지 않고 최대한 활용할 수 있는 방법입니다.
따라서 우리는 React에서 제공하는 useRef 훅을 이용할 것입니다.(참고링크: react - useRef)
예를 들어 볼까요?
import { useEffect, useRef } from "react";
import * as d3 from "d3";
function LineChart({ datas, width = 1000, height = 600, margin = 100 }) {
const ref = useRef();
useEffect(() => {
const currentElement = ref.current;
const svg = d3
.select(currentElement)
.attr("width", width)
.attr("height", height);
return <svg ref={ref}></svg>;
}
}
위의 코드를 보면, d3.select('body').append('svg')를 사용하는 대신,
useRef()를 이용하여 ref를 생성해주고, 이걸 svg ref속성에 넣어 줍니다.
이런식으로 우린 d3를 통해 DOM에 접근하는 것이 아닌, React를 이용하여 DOM에 접근할 것입니다.
자, 이제 본격적으로 시작해봅시다.
[ HTML 구조 ]
말한 것 처럼, d3를 이용해서 DOM element를 생성하는 과정을 거치지 않을 것이기 때문에,
아래처럼 내가 만들 element들을 return문안에 미리 생성해줍시다.
그럼, 우리가 만들 차트가 어떻게 구성되는지 먼저 알아야겠습니다. 아래 이미지를 살펴볼까요?
위 이미지에서 볼 수 있듯이, 크게 3가지로 나눌 수 있습니다.
- 그래프 라인
- x축
- y축
자, 직접 코드로 HTML 구조를 짜 봅시다.
import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
function LineChart({ datas, width = 1000, height = 600, margin = 100 }) {
return (
// LineChart가 그려질 SVG영역
<svg width={width} height={height}>
//차트의 라인 부분
<path
></path>
// x축, y축을 묶는 g 태그
<g>
// x축 g태그
<g></g>
// y축 g태그
<g></g>
</g>
</svg>
);
}
export default LineChart;
크게 어려운 부분은 없지만, 알아야 하는 부분은 우린 path태그를 이용하여 차트의 라인을 구현할 것이라는 점입니다.
path태그는 말그대로 svg안에서 좌표를 이용해 원하는 모양을 만들 수 있는 태그입니다. 이때 d속성을 이용하게 됩니다.
(참고링크: mdn-path element)
[ ref 설정 ]
자, 이제 기본 구조는 만들었습니다.
앞서 설명드린 d3의 기본 flow는 아직 기억하시죠? 가장 먼저해야 할 부분은 DOM element를 선택하는 것입니다.
그리고, 이 DOM Selection을 저는 useRef 훅을 이용하여 할 것입니다.
바로 코드로 보시죠.
import { useEffect, useRef } from "react";
import * as d3 from "d3";
function LineChart({
datas,
width = 1200,
height = 600,
margin = { top: 20, right: 100, bottom: 20, left: 100 },
}) {
// 1. ref 생성
const xAixsRef = useRef();
const yAixsRef = useRef();
return (
<svg width={width} height={height}>
<path></path>
<g>
{/* 2. x축, y축 element에 ref 설정해주기 */}
<g
ref={xAixsRef}
></g>
<g
ref={yAixsRef}
></g>
</g>
</svg>
);
}
export default LineChart;
먼저, useRef를 import 해주고 1. ref를 생성해주고, 2. ref속성에 넣어줍시다.
이때 주의할 점은, ref를 선언할 때, 꼭 최상위에서 생성해줘야 한다는 것입니다.
여기서는 이에 대해 자세히 다루진 않겠습니다. 자세한 사항은 참고링크를 참고 부탁드립니다.(참고링크: React: hooks규칙)
[ y축 그리기 ]
여기까지는 쉽게 따라오셨을 거라 생각합니다. 이제 가장 먼저 y축을 그려보겠습니다.
보통 d3에서 축을 그리는 것은 총 3가지 단계로 이뤄 집니다.
- scale 설정 - range
- scale 설정 - domain
- y축 그리기
참고로 d3로 컴포넌트를 그리는 것은, 아래처럼 useEffect안에서 모두 작동하게 될 것입니다.
이제 코드를 보면서 하나씩 설명드리도록 하겠습니다.
useEffect(() => {
// ####################### Y축 그리기 #######################
const yScale = d3
//1. scale함수 호출
.scaleLinear()
//2. range설정
.range([height - margin.bottom, margin.top]);
//3. domain설정
yScale.domain([0, d3.max(datas, (d) => d.precipitation)]);
//4. y축 생성
const yAxis = d3.axisLeft().scale(yScale);
//5. y축 그리기
d3.select(yAixsRef.current).call(yAxis);
, []}
먼저, 코드를 이해하기 전에 range와 domain에 대해 이해할 필요가 있습니다.
우선 우리가 차트로 보여주고 싶어 하는 data값이 [100, 400, 300, 1000] 이라고 가정하고 설명을 이어가봅시다.
[ domain ]
도메인은 간단합니다. 우리가 입력하고자 하는 input값의 최소값과 최대값입니다.
따라서, 위의 data에서 우리의 domain은 [100, 1000]이 될 것입니다.
[ range ]
range는 output의 최소값과 최대값을 의미합니다.
이 range는 왜 필요한 것일까요? 바로 우리가 화면에 보여줄 수 있는 pixel은 한정되어 있기 때문입니다.
예를 들어, 저는 500pixel width를 가지고 있는 SVG에 차트를 출력하고 싶습니다.
이때 위의 data값을 pixel과 1대1로 mapping하게 되면 당연히 모든 데이터를 출력하지 못할 것입니다.
이 pixel과 input의 data들을 mapping하는 것이 바로 range 메소드입니다.
따라서, 제가 50px, 500px 안에서 보여주고 싶다고 한다면, range는 [50, 500]이 될 것입니다.
이는 input값 100은 50px에, 1000은 500px에 맵핑 된다는 것을 의미합니다.
크게 어렵지 않죠?
[ 1. scale함수 호출 ]
d3에서는 다양한 scale 메소드를 제공하고 있습니다. (참고링크: d3-Scales)
그리고 그 중 우리는 scaleLinear 함수(선형눈금)를 사용할 것입니다.
** 주의할 점은 이 scaleLinear함수를 호출하고, range와 domain을 설정한 return값 (yScale)이 convert 함수라는 것입니다.**
즉, 우리가 range를 [100, 1000], domain을 [50, 500]으로 설정하면, scaleFactor가 0.5가 되므로,
yScale(1000)을 하게 되면 500이 리턴되게 될 것입니다. 즉, yScale(300) === 150 은 true가 되겠죠.
[ 2. range 설정 ]
지금 그리고 있는 것은 y축입니다. 따라서 최소값은 SVG 바닥의 좌표, 최대값은 SVG 천장의 좌표가 될 것입니다.
참고로 SVG의 좌표는 좌상단을 (0,0)으로 잡고 있으므로, range를 ([height - margin.bottom, margin.top])으로 설정하였습니다.
[ 3. domain 설정 ]
앞서 설명한 것처럼 인풋값의 최소 최대값을 설정해줘야 합니다.
현재 제가 라인차트로 표현하고자 하는 data는 아래와 같습니다.
[
{ "date": "2021-01", "precipitation": 18.9, "rainDay": 9 },
{ "date": "2021-02", "precipitation": 7.1, "rainDay": 5 },
{ "date": "2021-03", "precipitation": 110.9, "rainDay": 9 },
{ "date": "2021-04", "precipitation": 124.1, "rainDay": 9 },
{ "date": "2021-05", "precipitation": 183.1, "rainDay": 17 },
{ "date": "2021-06", "precipitation": 104.6, "rainDay": 13 },
{ "date": "2021-07", "precipitation": 168.3, "rainDay": 8 },
{ "date": "2021-08", "precipitation": 211.2, "rainDay": 13 },
{ "date": "2021-09", "precipitation": 131, "rainDay": 8 },
{ "date": "2021-10", "precipitation": 57, "rainDay": 11 },
{ "date": "2021-11", "precipitation": 62.4, "rainDay": 6 },
{ "date": "2021-12", "precipitation": 7.9, "rainDay": "" }
]
이 중에서 저는 precipitation, 즉 강수량을 표시할 예정입니다.
이때 최대 최소값을 넣는 방법이 조금 생소해 보일 수 있지만, 천천히 살펴봅시다.
yScale.domain([0, d3.max(datas, (d) => d.precipitation)]);
최소값 0은 이해하기 쉬울 것입니다. 강수량이 음수값을 가질 수는 없으니까요.
d3에서는 내가 입력한 배열안에서 최대값을 return해주는 max라는 메소드를 제공해주고 있습니다. (참고링크: d3-max)
이 max메소드는 첫 번째 인자로 input data를 받고, 두 번째 인자로 accessor(함수)를 받습니다.
물론, 아래처럼 인자를 한개만 전달할 수도 있습니다.
const data = [1, 2, 3, 4, 5]
const max = d3.max(data);
max === 5 //true
하지만, 제가 가지고 있는 data는 단순한 배열 형태가 아닌, 배열안에 객체를 가지고 있습니다. 바로 이때 2번째 인자가 필요합니다.
두번째 인자로 우리가 접근하고자 하는 값을 정확히 명시해줍니다. 우리의 경우에는 d.precipitation이 될 것입니다.
뭐... 화살표 함수가 싫다면 아래처럼 표현 할 수도 있겠지만, 저는 편의상 위의 방식인 화살표 함수로 이용하겠습니다.
yScale.domain([0, d3.max(datas, function(d){ return d.precipitation })]);
[ 4. y축 Generator ]
이제 앞의 1~3의 과정을 통해 우리는 y축에 입력될 data들을 화면에 적절한 사이즈로 표현해 줄 convert함수를 만들었습니다.
이제 d3에서 제공하는 axisLeft메소드를 이용해 y축 generator를 생성해줄 것입니다. (참고링크: d3-axisLeft)
scale함수의 인자로 우리가 만들어 놓은 convert함수, yScale을 전달해주면 됩니다.
const yAxis = d3.axisLeft().scale(yScale);
[ 5. y축 그리기 ]
이제 y축 generator를 생성하는 과정은 모두 끝났습니다.
실제로 화면에 그려볼까요?
가장 중요한 먼저 우리가 y축을 그릴 element를 선택해주고, - d3.select(yAxisRef.current)
call메소드 인자로 우리가 만들어 놓은 yAxis를 전달해줍니다.
d3.select(yAixsRef.current).call(yAxis);
이제 화면에 우리가 원하는 y축이 나타난 것을 볼 수 있을 것입니다.
[ x축 그리기 ]
이제 x축을 그려봅시다. 앞서 y축을 그리면서 대부분 설명했기에, 빨리빨리 넘어가 보겠습니다.
이 부분도 당연히 useEffect내부에 위치할 것입니다.
const timeParser = d3.timeParse("%Y-%m");
// ####################### X축 그리기 #######################
const xScale = d3
//1. scaleTime 함수 호출
.scaleTime()
//2. range 설정
.range([margin.left, width - margin.right]);
//3. timeDomain 생성
const timeDomain = d3.extent(datas, (d) => {
const parsedDate = timeParser(d.date);
return parsedDate;
});
//4. xAxis Generator 생성
const xAxis = d3
.axisBottom()
.scale(xScale)
.tickFormat(d3.timeFormat("%y-%m"))
.ticks(12);
//5. domain 설정
xScale.domain(timeDomain);
//6. x축 그리기
d3.select(xAixsRef.current).call(xAxis);
[ 1. scaleTime 호출 ]
x축은 시간을 보여주는 축이 될 것이므로, scaleLinear이 아닌, scaleTime함수를 호출했습니다.
여기에서 조금 주의해야 할 점은 scaleTime의 domain에는 반드시! date type의 Object를 넣어줘야 한다는 것입니다.
(즉, string타입의 "2020-01"을 넣으면 정상적으로 출력되지 않습니다.)
[ 2. range 설정 ]
위에서 설명한 y축 range와 동일합니다.
[ 3. timeDomain 생성 ]
d3에서는 한번에 최소 최댓값을 return해주는 extent 메소드를 제공합니다.(참고링크: d3-extent)
사용법은 위에서 설명한 max와 동일합니다.
d3.timeParse()는 string타입의 날짜를 입력받아 Date Object형태로 리턴해주는 역할을 합니다.(참고링크: d3-timeParse)
[ 4. x축 generator 생성 ]
axisBottom함수를 이용하여 generator를 생성해줍니다.
tickFormat은 축에서 보여줄 label의 format을 정할 수 있는 메소드입니다.(참고링크: d3-tickFormat)
[ 5. Domain 설정 ]
y축과 같은 방식으로 Domain 설정을 해주고,
[ 6. x축 그리기 ]
y축과 같은 방식으로 x축을 그려줍니다.
[ 라인 그리기 ]
여기까지 오시느라 고생하셨습니다. 이제, 우리에게 남은 것은 실제 라인을 그리는 것만 남았습니다.
여기에서 우리는 useState hook을 이용하여 dataBinding한 lineGenerator를 저장하고, 이를 path의 d 속성에 전달해줄 것입니다.
말이 조금 어렵지만, 바로 코드로 알아보도록 하겠습니다.
const [lines, setLines] = useState("");
useEffect(() => {
// ####################### 라인 그래프 그리기 #######################
// 1. lineGenerator 생성
const lineGenerator = d3.line();
// 2. lineGenerator x축 accessor 등록
lineGenerator.x((d) => xScale(timeParser(d.date)));
// 3. lineGenerator y축 accessor 등록
lineGenerator.y((d) => yScale(d.precipitation));
// 4. lineGenerator data binding
setLines(lineGenerator(datas));
}, []);
return (
<svg width={width} height={height}>
{/* 5. d속성에 lineGenerator 넣어주기 */}
<path
d={lines}
fill="none"
stroke="red"
strokeWidth="2"
transform={`translate(${margin}, ${0})`}
></path>
</svg>
);
[ 1. line generator 생성 ]
가장 먼저, d3에서 제공하는 line메소드를 통해서 lineGenerator를 생성해줍니다. (참고링크: d3-line)
[ 2. line generator - x축 Accessor 등록]
바인딩되는 data에서 어떤 값이 x축인지 알려주는 accessor를 등록해줍니다.
이때 이전에 만들어줬던, x축 convert 함수인, xScale을 사용해줍니다.
[ 3. line generator - y축 Accessor 등록]
바인딩되는 data에서 어떤 값이 y축인지 알려주는 accessor를 등록해줍니다.
이때 이전에 만들어줬던, y축 convert 함수인, yScale을 사용해줍니다.
[ 4. line generator - data binding]
이제 우리가 만든 linegenerator에 보여주고 싶은 data를 binding해줍시다. 이는 data를 lineGenerator의 인자로 전달해줌으로써 간단히 binding 해줄 수 있습니다.
그리고 이 값은 setState를 통해 저장해줍니다.(여기에서는 setLines가 될 것입니다.)
[ 5. line 생성 ]
이제 state(lines)를 이용하여 진짜 line을 생성해 봅시다. 앞서 말한 것처럼, path element의 d속성에 넣어주면 됩니다.
짜잔! 이제 모든 과정이 끝났습니다!
열심히 정리한 만큼, 많은 분들께 도움이 됐으면 좋겠습니다.
궁금한 점이나 잘못된 정보가 있다면 언제든지 댓글 남겨주세요! 감사합니다.
다음 포스트에서는 d3를 이용하여 지도를 출력하는 방법에 대해 정리해보겠습니다.
그리고 최종 코드는 아래와 같을 것입니다.
import { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
function LineChart({
datas,
width = 1200,
height = 600,
margin = { top: 20, right: 100, bottom: 50, left: 100 },
}) {
const xAixsRef = useRef();
const yAixsRef = useRef();
const [lines, setLines] = useState("");
useEffect(() => {
// ####################### X축 그리기 #######################
const xScale = d3
.scaleTime()
.range([margin.left, width - margin.right]);
const timeDomain = d3.extent(datas, (d) => {
const parsedDate = timeParser(d.date);
return parsedDate;
});
const xAxis = d3
.axisBottom()
.scale(xScale)
.tickFormat(d3.timeFormat("%y-%m"))
.ticks(12);
xScale.domain(timeDomain);
d3.select(xAixsRef.current).call(xAxis);
// ####################### y축 그리기 #######################
const yScale = d3
.scaleLinear()
.range([height - margin.bottom, margin.top]);
yScale.domain([0, d3.max(datas, (d) => d.precipitation)]);
const yAxis = d3.axisLeft().scale(yScale);
d3.select(yAixsRef.current).call(yAxis);
// ####################### 라인 그래프 그리기 #######################
const lineGenerator = d3.line();
lineGenerator.x((d) => xScale(timeParser(d.date)));
lineGenerator.y((d) => yScale(d.precipitation));
setLines(lineGenerator(datas));
}, []);
return (
<svg width={width} height={height}>
{/* d속성에 lineGenerator 넣어주기 */}
<path
d={lines}
fill="none"
stroke="red"
strokeWidth="2"
transform={`translate(${margin}, ${0})`}
></path>
<g transform={`translate(${margin}, ${margin})`}>
<g
ref={xAixsRef}
transform={`translate(0,${height - margin.bottom})`}
></g>
<g
ref={yAixsRef}
transform={`translate(${margin.left}, 0)`}
></g>
</g>
</svg>
);
}
//string Type의 날짜를 Date Object로 변환
const timeParser = d3.timeParse("%Y-%m");
export default LineChart;
'd3' 카테고리의 다른 글
[ D3 ]1. Learning d3 - svg (0) | 2022.12.06 |
---|