[ Data Visualizing - D3.js ] Tree Chart 만들기

승진·2019년 10월 22일
0

Data Visualizing

목록 보기
7/7
post-thumbnail

기본구조

나의 스킬 스택을 Data로 활용해 Tree 구조의 차트로 만들어 보자
image.png
Meterialize를 사용해 기본적인 앱의 틀을 잡아주고 모달을 띄워 데이터를 추가할 수 있게 한다.

Firebase Data

Firebase에 데이터를 저장해 실시간으로 반영하려고 한다.
HTML에 input과 firebase를 연결해주고

그래프를 그리는 js 파일에서 firebase data를 불러와서 적용한다.
graph.js

// data & firebase
let data = [];

db.collection("stacks").onSnapshot(res => {
  // 실시간 데이터 업데이트
  res.docChanges().forEach(change => {
    const doc = {
      ...change.doc.data(),
      id: change.doc.id
    };
    switch (change.type) {
      case "added":
        data.push(doc);
        break;
      case "modified":
        const index = data.findIndex(item => item.id == doc.id);
        data[index] = doc;
        break;
      case "removed":
        data = data.filter(item => item.id !== doc.id);
        break;
      default:
        break;
    }
  });
  update(data);
});

image.png

기본적인 데이터 구조는 Javascript > React > Redux 로 3가지를 추가해보자

image.png

Firebase에 잘 저장된 것을 확인할 수 있다.

Tree Hierarchy

Data를 트리 계층구조로 변환시키기 위해 stratify를 사용한다.

D3 공식문서 API Reference

d3.stratify는 d3-hierarchy API의 하나로,
평면 데이터 모델을 자신의 키와 부모의 키를 바탕으로 tree 구조의 데이터로 전환해준다.

const stratify = d3
  .stratify()
  .id(d => d.name)
  .parentId(d => d.parent);
  
const rootNode = stratify(data);
console.log(rootNode);

image.png

콘솔을 확인해 보면 data가 계층구조가 된 것을 확인할 수 있다.

const tree = d3.tree().size([dims.width, dims.height]);

// update function
const update = data => {
  const rootNode = stratify(data);

  const treeData = tree(rootNode);
  console.log(treeData);
};

tree() 에 사이즈를 적용해주고 Data를 적용해서 확인해 보면
image.png

x, y 값이 잘 적용됐고, 계층적 데이터 형식을 얻게 되었다!

이제 루트 노드를 통해 tree 생성기로 데이터를 전달해줘야 한다.
update 함수 안에 아래와 같이 작성해준다.

// get nodes selection and join data
  const nodes = graph
    .selectAll(".node") // .node = g
    .data(treeData.descendants()); // 자손 연결

graph

값을 반환하려면 각 데이터 객체의 x 및 y 좌표에 있는 데이터에 액세스해야 하기 때문에
그래서 D는 그 데이터로 전달된다.
그런 다음 출력하기 때문에 템플릿 문자열이 될 문자열을 반환할 겁니다.

// create enter node groups
 const enterNodes = nodes
    .enter()
    .append("g")
    .attr("class", "node")
    .attr("transform", d => `translate(${d.x}, ${d.y})`); 

  // append rects to enter nodes
  enterNodes
    .append("rect")
    .attr("fill", "#aaa")
    .attr("stroke", "#555")
    .attr("stroke-width", 2)
    .attr("width", d => d.data.name.length * 20) // name의 길이만큼 width값 설정
    .attr("height", 50);

image.png
지금 우리는 모든 노트를 문으로 출력하고 있고 각 노트는 직사각형과 텍스트를 포함하고 있다.

이제 데이터를 배열 형식으로 만드들어서 노드에 데이터를 결합해야한다.

// get link selection and join new data
  const link = graph.selectAll('.link')
    .data(tree(rootNode).links());

  // enter new links
  link.enter()
    .append('path')
      .transition().duration(300)
      .attr('class', 'link')
      .attr('fill', 'none')
      .attr('stroke', '#aaa') 
      .attr('stroke-width', 2)
      .attr('d', d3.linkVertical()
        .x(d => d.x)
        .y(d => d.y )
      );

스타일 속성을 추가하고 이 데이터 안에서 본 x와 y의 위치를 지정해주면 연결된다.

image.png
각 데이터가 연결된 선이 그려졌다.

텍스트와 선이 왼쪽으로 치우쳐있기 때문에 위치를 바로잡아줘야한다.

enterNodes
    .append("rect")
    .attr("fill", "#aaa")
    .attr("stroke", "#555")
    .attr("stroke-width", 2)
    .attr("width", d => d.data.name.length * 20) // name의 길이만큼 width값 설정
    .attr("height", 50)
    .attr("transform", d => { // 추가된 부분
      let x = d.data.name.length * 20;
      return `translate(${-x / 2}, -25)`; // text를 사각형 가운데 위치
    });

image.png

새로운 데이터

image.png

image.png

새로운 데이터를 추가해보니 생각한 모양이 나오지 않는다.

가상 돔에 추가되는 것이기 때문에 지금 돔에는 아무것도 없을 것이고 그래서 나중에 우리가 데이터를 바꿀 때마다 돔의 모든 요소들과 모든 노드와 모든 요소들을 잡아내야한다.

// remove current nodes
  graph.selectAll(".node").remove();
  graph.selectAll(".link").remove();

update function 맨 위쪽에 이 두가지를 추가하면 해결된다.

image.png

ordinal scale

각 부모 데이터 별로 컬러를 다르게 지정하려고 한다.

// color 지정
const color = d3.scaleOrdinal(["#27AE60", "#9B51E0", "#F2C94C", "#EB5757"]);

// *** update function 안쪽 *****

// update ordinal scale
  color.domain(data.map(d => d.department));

enterNodes
    .append("rect")
    .attr("fill", d => color(d.data.department)) // **수정된 부분** color 적용
    .attr("stroke", "#555")
    .attr("stroke-width", 2)
    .attr("width", d => d.data.name.length * 20) // name의 길이만큼 width값 설정
    .attr("height", 50)
    .attr("transform", d => {
      let x = d.data.name.length * 20;
      return `translate(${-x / 2}, -25)`; // text를 사각형 가운데 위치
    });

image.png

이렇게 해서 차트를 완성했다!

profile
Front-end 개발 공부일지

2개의 댓글

comment-user-thumbnail
2021년 9월 24일

예제 감사합니다 !

답글 달기
comment-user-thumbnail
2024년 2월 15일

안녕하세요 잘 설명해주셔서 감사합니다! 혹시 이 상태에서 tree 의 하위 노드들을 숨겼다가 펼쳐보는 dynamic 한 동작을 연결해주고 싶다면 어떻게 하면 좋을까요..?

답글 달기