[JS] D3.js 알아보기

Suyeon·2020년 12월 23일
6

D3.js

목록 보기
1/6
post-thumbnail

D3.js는 브라우저에서 html, css, svg, canvas등을 사용하여 데이터 시각화를 도와주는 라이브러리이다. 데이터에 어떠한 그래프가 적합할지 잘 생각해야 한다.

예를 들면,

  • 0 base line을 사용해야 하는 경우: Bar chart, Area chart
  • 그렇지 않은 경우: Scatter plot, Line chart

💫 그래프 스타일링은, Data Visualization Style Guide 참고

CSV Format

데이터를 불러올 때, .csv파일 포맷을 사용하거나, csv파일이 담긴 url을 사용한다. 또는 json파일을 사용한다.

cvs format은 comma-separated values로 이루어진 형식임

// data.csv
country,population
China,1415046
India,1354052
United States,326767
...

Indentation

D3.js에서 indentation chaining이 자주 사용된다.

const rightEyebrow = eyesG
  .append('rect')
     .attr(...)
     .attr(...)
  .transition().duration(2000)
     .attr(...)

Data Join

  • 데이터와 데이터를 그릴 DOM 요소를 서로 연결한다.
  • Object Consistancy를 위해서 각각의 데이터에 유니크한 key를 저장하는 것이 좋다.

enter()

  • 데이터 > DOM elements
svg.selectAll('circle')
  .data(fruits)
  .enter()
  .append('circle')  

exit()

  • 데이터 < DOM elements
svg.selectAll('circle')
  .data(fruits)
  .exit()
  .remove() 

update()

  • 변화한 데이터에 따라서 DOM elements를 업데이트함
  • selection.data() 자체가 update()이다.
var rects = d3.select('svg')
    .selectAll('rect')
    .data(data)

var newrects = rects.enter()
    .append('rect')
    .style('fill', 'red')

rects.merge(newrects)
    .attr('width', d => d.value)
    .attr('height', 20)
    .attr('y', (d, i) => i*20)

rects.exit()
    .remove()

💫 selection.join()

// The same above example
d3.select('svg')
    .selectAll('rect')
    .data(data)
    .join('rect') // (*)
    .style('fill', 'red')
    .attr('width', d => d.value)
    .attr('height', 20)
    .attr('y', (d, i) => i*20)

General Update Pattern

다이나믹한 그래프 혹은 재사용가능한 컴포넌트를 만들 때 사용되는 패턴이다. (변화하는 데이터에 따라서 DOM element도 업데이트 됨)

  • Listen event ➡️ change state/data ➡️ update DOM
  • 💫 General Update Pattern 대신 selection.join() 사용하기

merge()

  • 두개의 요소를 합쳐서 반복되는 로직을 줄인다. (enterupdate를 합침)
  • 데이터의 변화에 따라서, 업데이트 되어야하는 attributemerge아래에 위치 시킨다.
  • 위의 예시를 merge를 사용해서 다시 refactoring 한다면 아래와 같다.
// 💫 General Update Pattern
const render = (selection, props) => {
  const { fruits } = props;
  
  // (*) circles itself is an update selection
  const circles = selection.selectAll('circle').data(fruits);

  circles
    .enter()
    .append('circle')
      .attr('cx', (d, i) => i * 120 + 60)
      .attr('cy', height / 2)
    .merge(circles) selection. // (*) merge (enter and update)
      .attr('r', d => radiusScale(d.type))
      .attr('fill', d => colorScale(d.type));

  // exit
  circles.exit().remove();
};

Nested Element(Group)

  • Nested Element를 그룹으로 묶지 않은 경우 (반복되는 로직)
const circles = selection.selectAll('circle').data(fruits); 
const text = selection.selectAll('text').data(fruits); 

  circles // (*)
    .enter().append('circle')
      .attr('cx', (d, i) => i * 120 + 60)
      .attr('cy', height / 2)
    .merge(circles)
      .attr('r', d => radiusScale(d.type))
      .attr('fill', d => colorScale(d.type));
  circles.exit().remove();

  text // (*)
    .enter().append('text')
      .attr('x', (d, i) => i * 120 + 60)
      .attr('y', height / 2)
    .merge(text)
    .text(d => d.type)
  text.exit().remove();
  • 💫 위의 예시를 그룹으로 묶어서 리팩토링 한 경우
  const groups = selection.selectAll('g').data(fruits);
  const groupsEnter = groups.enter().append('g');

  groupsEnter
    .merge(groups)
      .attr('transform', (d, i) =>
        `translate(${i * 180 + 100},${height / 2})`
      );
  groups.exit().remove();
  
  groupsEnter.append('circle')
    .merge(groups.select('circle'))
      .attr('r', d => radiusScale(d.type))
      .attr('fill', d => colorScale(d.type));
  
  groupsEnter.append('text')
    .merge(groups.select('text'))
      .text(d => d.type)
      .attr('y', 120);
  • DOM Structure
<g>
  <circlr />
  <text />
</g>
<g>
  <circlr />
  <text />
</g>
...

Singular Element

  • Single Element를 다뤄야 하는 경우, 아래와 같이 작성한다.
  const renderBowl = selection.selectAll('rect')
    .data([null]) // (*)
    .enter().append('rect')
      .attr('y', 110)
      .attr('width', 920)
      .attr('height', 300)
      .attr('rx', 300 / 2);

/* 이렇게 작성한다면, 렌더링마다 "rect"가 생성된다.
  const renderBowl = selection.append('rect')
      .attr('y', 110)
      .attr('width', 920)
      .attr('height', 300)
      .attr('rx', 300 / 2);
*/

Component

D3.js에서도 재사용 가능한 컴포넌트를 만들 수 있다. 구조는 리액트와 비슷하다.

// (*) index.js
  render(svg, {
    fruits, 
    height: +svg.attr('height')
  });

// (*) fruitBowl.js
const render = (selection, props) => {
  const { fruits, height } = props;
  const circles = selection.selectAll('circle').data(fruits); 

  // General Update Pattern
  circles
    .enter()
    .append('circle')
    .attr('cx', (d, i) => i * 120 + 60)
    .attr('cy', height / 2)
    .merge(circles)
    .attr('r', d => radiusScale(d.type))
    .attr('fill', d => colorScale(d.type));

  circles.exit().remove();
};

Animated Transition

  • selection.transition()
d3.select("body")
  .transition()
    .style("background-color", "red");

// Transition function
const t = d3.transition()
    .duration(750)
    .ease(d3.easeLinear);

d3.selectAll(".apple").transition(t)
    .style("fill", "red");

d3.selectAll(".orange").transition(t)
    .style("fill", "orange");

Methods

자주 사용하는 method 정리

select() / selectAll

DOM에 있는 element를 선택함

  • const svg = select('svg')
  • const svg = selectAll('rect')

attr()

Attribute를 추가하거나 업데이트 함
d3.selectAll('rect').attr('width', 10)

  • attr('x', 10)/(attr('y', 10)) xPosition, yPosition
  • attr('width', 10)/(attr('height', 10)) 높이, 너비
  • attr('cx', 100)/(attr('cy', 200)) position the x-centre, position the Y-centre of circle
  • attr('r', 50) radius
  • attr('transform', `translate(0, 100)`) Transform(translate, scale, rotate)

아래처럼 function과 함께 사용할 수 있음

d3.selectAll('circle')
  .attr('cx', function(d, i) {
    return i * 100;
  });

Scale()

domain() 범위의 입력값이 들어오면 range() 범위의 결과값으로 바꿔주는 함수를 만든다. scale 종류는 총 12개가 있다.

domain은 데이터의 최소, 최대값으로, range는 그 데이터를 사용하여 표출할 범위의 너비, 높이 픽셀값으로 설정한다. (svg의 너비등)

  const xScale = scaleLinear()
    .domain([0, max(data, d => d.population)])
    .range([0, innerWidth]);

  const yScale = scaleBand()
    .domain(data.map(d.country))
    .range([0, innerHeight])
    .padding(0.1);

Classed

  • 조건적으로 클래스를 추가한다.

selection.call()

  • Call에 정의된 함수를 단 한번만 실행한다.
  mapG.call(choroplethMap, {
    features,
    colorScale,
    colorValue
  });

D3.js와 React 함께 사용하기

D3.js와 리액트를 함께 사용할 경우, 두 라이브러이 모두 DOM을 핸들링하려고 하기 때문에 적절하게 믹싱하는 것이 중요하다.

select

예를 들어, DOM element를 선택해야 할 때, D3.js에서는 select()를 사용하지만 리액트에서는 useRef()을 사용할 수 있다. 따라서 아래와 같이 함께 사용할 수 있다. D3.js에서 제공하는 transition을 사용할 때도 마찬가지이다.

function BarChart() {
  const [data, setData] = useState([...])
  const rectRef = useRef<GroupTag>(null);

  const handleDrawRect = useCallback(
    (group) => {
      group
        .selectAll('rect')
        .data(data)
        .join('rect')
  	//...
    },
    [data]
  );

  useEffect(() => {
    const rectGroup = select(rectRef.current); // (*)
    handleDrawRect(rectGroup);
  }, [handleDrawRect]);

  return (
      <svg width={WIDTH} height={HEIGHT}>
          <g ref={rectRef} />
        </Group>
      </svg>
  );
}
profile
Hello World.

0개의 댓글