[D3.js] selection부터 transition까지

Sol·2020년 10월 23일
0
post-thumbnail

D3.js는

D3.js is a JavaScript library for manipulating documents based on data.
(Data-Driven Documents 그래서 D3)
데이터 기반으로 도큐먼트를 컨트롤하는 자바스크립트 라이브러리이다.

사실 D3.js 이외에도 여러 차트나 그래프를 만드는데 사용되는 라이브러리들은 많지만,
굳이 D3.js를 선택한 이유는 그냥 많이 보여서다. 공고에 많이 보이고, 커뮤니티에 많이 보이고..
거창한 까닭이 없었다. 단순히 사람들이 많이 쓴다면 이유가 있을거라 생각했다.

하지만 실제로 공부하고, 사용해본 결과 굉장히 편한 라이브러리임을 깨달았다.
데이터 기반으로 DOM을 조작하기 때문에 데이터를 정리하기도 쉽고, 시각화 하는 것도 쉽다.

예를 들어 전 글 마지막에 언급했던 d3-array 같은 경우에는
객체들의 배열로 이루어진 복잡한 데이터들도, 간단히 key, value 값으로 재배열 할 수 있다.

d3-scale에서는 scaleBand, scaleLinear 등 데이터의 지정한 값 기준으로
domain과 range를 설정해 원하는 형태와 크기의 함수그래프를 그릴 수 있다.

scaleBand 예시

scaleLinear 예시

d3-axis는 그래프의 x축, y축 기준으로 중심선을 그려 단위 값을 보여줄 수 있다.

axis 예시

  • JSON, csv 데이터 파일등을 패치해주는 d3-fetch
  • 애니메이션을 구축해주는 d3-transition, 그 모션들을 쉽게 사용하게 해주는 d3-easings
  • 데이터의 값에 따라 색상을 차등지정할 수 있게 해주는 d3-interpolators.
  • D3 API Reference 이외에도 여러가지가 있다.
    (한국 번역본도 도움이 되지만 최신 상태의 API 문서를 보는게 더 도움이 될 것 같습니다.)

D3의 시작은 Selection

출처: https://www.slideshare.net/aliceinwoon/d3js-2

D3는 데이터를 불러오고, DOM요소를 선택하고, 각각 데이터에 맞춰 연결하고 시각화한다.
React에서는 useEffectuseRef를 사용해서 데이터를 연결할 수 있다.

const ref = useRef(null);
const [selection, setSelection] = useState(null)
// 컴포넌트가 렌더링을 시작할 때, useEffect가 실행되면서 if문을 지나간다.
useEffect(() => {
if(!selection){
  setSelection(select(ref.current))
  // 초기 화면에서는 svg를 select한다.
}
else {
  ...
}
}, [selection])
// depth에 [selection]을 넣었기 때문에 이후 다시 리렌더링 되고, 이번에는 else문을 지나간다.

// else문 안에서는 "rect", "text", axis를 만들기 위한 "g" 등을
// selection에 삽입하고 데이터와 연결한다.

...

return (
  <svg ref={ref}></svg>
)

else문에서는 .selectAll() 그리고 .data(), .enter(), .append() 등을 사용해서
각각 데이터를 불러오고, 연결하고, 시각화 하는 과정을 거친다.

d3.select()
  .selectAll()
  .data()
  .enter()
  .append()

How Selections Work
이 글을 읽어보면 Selection이 어떤 원리인지 더 상세히 알 수 있다.

간단하게 요약하자면 selection은 DOM 요소들의 배열이 아니고, array의 subclass이며
이 subclass는 선택된 요소들을 조작하기 위한 메서드를 제공한다.

Enter, Update and Exit


출처: https://bost.ocks.org/mike/selection/

반응형 그래프 차트니까 당연히 sort, update, reset
버튼으로 그래프를 이것저것 조작하고 싶었다.
원하는 구의 수치만을 보거나, 수치별로 정렬을 하고 싶었다.
그러기 위해 필요한 메서드는 .enter()와 .exit()

윗글 Enter, Update and Exit를 보면 데이터를 연결할 때,
selection의 3가지 동작 원리를 알 수 있다.

  • Update - selection.data
  • Enter - selection.enter
  • Exit - selection.exit

Update


vowels(모음)라는 데이터(Y, E, A, O, I)에서 name은 A와 E만 남기 때문에
새 데이터와 연결에서 update는 이렇게 진행된다.

Exit


B, C, D는 consonants(자음)이기 때문에 update하지 않고, exit selection의 공간에 보관된다.
exit selection 안에 이전 selection 데이터의 인덱스와 값을 보관한다.
그래서 애니메이션이 진행되기 전 요소들을 지우는 것에 용이하다.

Enter


vowels 데이터에서 남은 Y, O, I는 데이터와 매칭되는 요소가 없다면 보이지 않게 된다.
다만 일시적으로 placeholder가 생기고, 이 요소는 enter.append, enter.insert
사용하게 되면 일반적인 selection으로 대체 된다.
updateexit 도중 enterselectionsubclass가 된다.
아직 exit 하지 않은 요소들을 보여주기 위해 사용된다.
그래서 select 혹은 selectAll을 하기 전에 데이터를 연결해서는 안된다.
placeholder를 대체하면서 그룹의 부모 노드에 삽입되어야 하는데
아직 생성되지 않았기 때문이다.


Scale

let x = scaleBand()
	.domain(selectList.map((d) => d.name))
	.range([0, canvas.chartWidth])
	.paddingInner(0.1);

let y = scaleLinear()
	.domain([0, max(selectList, (d) => (axis.changeAxis ? d.bikeC : d.divC))!])
	.range([canvas.chartHeight, 0]);

let color = scaleLinear()
	.domain([0, max(selectList, (d) => (axis.changeAxis ? d.bikeC : d.divC))!])
	.range([0.2, 0.8]);

위 그래프에서 사용된 3가지 scale이다.

domain, range

domain과 range는 모든 scale에서 동일하게 사용된다.
domain은 입력하는 데이터 값의 범위
range는 출력되는 범위

좀 더 정확하게 정의하면, 입력되는 정의역(domain)과 출력되는 치역(range)을 맵핑한다.

위 그래프에서
X축의 domain(강남구, 강동구, 강서구, 강북구,......), range([0, 그래프의 넓이])
Y축의 domain([0, 그래프의 최대수치]), range([그래프의 높이, 0])
(Y축은 그래프의 상단부터 계산하기 때문에 range를 최대치에서 0으로 설정해야 한다.)

.attr("width", x.bandwidth) // bandwidth는 scaleBand의 메서드
// 구의 갯수만큼 그래프의 넓이를 나누어 출력한다.
.attr("height", (d) => canvas.chartHeight - y(d.bikeC))
// 최대높이에서 데이터의 수치만큼 뺀 값으로 높이를 설정한다.

color domain([0, 그래프의 최대수치]), range([0.2, 0.8])

.attr("fill", (d) => d3.interpolateGreens(color(d.bikeC)))

interpolateGreens는 Sequential(singleHue)이다.
주어지는 값으로 sequential과 매칭되는 색상 스트링을 리턴한다.

scale이 중요한 이유

그래프의 사용되는 바("rect")의 크기는 설정했으나 위치는 설정하지 못했다.

.attr("x", (d) => x(d.name)!) 
// typescript가 useStrit 모드에서 undefined를 찾아내기 때문에 !로 아니라고 알려줘야한다.
// x값은 scaleBand를 사용해 지정해준다.
.attr("y", (d) => canvas.chartHeight - y(d.bikeC)!)
// y값은 그래프의 최대높이에서 데이터와 연결한 scaleLinear 값을 뺀 값으로 지정해준다.

이렇게 scaleBand와 scaleLinear를 사용하면 이후에 axis를 설정하기에도 쉽다.

.call과 axisBottom, axisTop, axisRight, axisLeft 등을 사용하면 된다.

.call(axisBottom(x)) // x는 scaleBand
.call(axisLeft(y)) // y는 scaleLinear

Transition

이전글 Enter, Update and Exit에서 제외되는 요소들을 임시로 보관하는 별개의 placeholder가 있다고 했다. 덕분에 애니메이션을 구사하기가 편해졌다.
transition선택된 DOM요소의 이전 상태에서 현 상태로 변경하는 과정을 보간(Interpolation)한다.
CSS 트랜지션 사용하기 - MDN

즉 애니메이션 속도를 조절하게 해주는 interfaced3-transition이다.
D3는 복잡하게 계산할 필요없이 간단히 transition을 구축할 수 있게 d3-ease를 제공한다.


d3.easePolyIn과 d3.easePolyOut 이외에도 d3-ease - github 많은 메서드들이 있어 사용하기가 편하다.

    .attr("height", 0)
    .attr("y", canvas.chartHeight) // 이전 상태
    .transition()
    .duration(1000) // 지속 시간 설정
    .delay((_, i) => i * 100)
    // 바(rect) 하나당 delay 100mc를 주어 순서대로 애니메이션이 되게 한다.
    .ease(easeCircleOut)
    .attr("height", (d) => canvas.chartHeight - y(d.bikeC) - 10)
    .attr("y", (d) => y(d.bikeC); // 현 상태

d3-transition은 사용법이 다른 d3 API들에 비해 비교적 쉬운 편이다.
ease 메서드들을 활용해서 다양하기도 하고, DOM요소를 조작하는 selection이 있어
이전 상태와 현 상태를 설정함과 동시에 애니메이션도 설정할 수 있다.

profile
야호

0개의 댓글