(3D)Dev Deep Dive/Frontend origin

[시각화] D3를 이용하여 간단한 노드관계 만들어보기

TMInstaller 2023. 7. 10. 19:29
728x90

프론트엔드를 함에 따라 기본적인 개발과정을 따를 수 있으면 한 번 씩은 바라보는 주제가 있습니다.

바로 시각화 입니다. 어떤 주제로 개발을 할 때, 개발에 사용했던 데이터를 이용하여 시각화를 하는 것은 꽤나 값진 경험이 될 것입니다.

 

그렇기에 오늘은 D3를 이용하여 간단한 노드를 연결하는 작업을 해 볼 것입니다.

우선, D3가 무엇인 지 알아보아야겠죠?

D3를 이전에 정리했던 글에서 살펴보실 수 있습니다.

 

 

[D3.js] 강력한 성능의 시각화 라이브러리, D3.js에 대해 알아보자

D3.js 라는 이름을 들어 보셨나요? "Data-Driven Documents"의 줄임말인 D3.js는 웹 표준을 이용해 데이터를 시각화하는 데 사용되는 강력한 자바스크립트 라이브러리입니다. 주로 프론트엔드 고도화의

time-map-installer.tistory.com


간단하게 D3만 써보기 위해 CRA(Create React App)을 이용하여 JS 리액트 프로젝트만 생성한 후

노드를 연결해보는 작업을 하겠습니다

 

리액트 프로젝트를 생성하고 기본적으로 형성되어있는 보일러플레이트 코드를 제거하는 과정은 생략하겠습니다.

 

1단계 : 필요한 라이브러리 설치

React 초기 세팅을 모두 마쳤으면 D3를 설치합니다.

npm install d3

 

2단계 : Mock Data 준비

노드, 그리고 노드 사이의 링크를 나타내기 위해 Mock data를 이용하여 구성 해보겠습니다.

뭔가 그럴싸해 보이게 하기 위해 10개 정도씩 준비해 보았습니다.

const nodes = [
    { id: "node1", name: "Node 1" },
    { id: "node2", name: "Node 2" },
    { id: "node3", name: "Node 3" },
    { id: "node4", name: "Node 4" },
    { id: "node5", name: "Node 5" },
    { id: "node6", name: "Node 6" },
    { id: "node7", name: "Node 7" },
    { id: "node8", name: "Node 8" },
    { id: "node9", name: "Node 9" },
    { id: "node10", name: "Node 10" },
  ];

  const links = [
    { source: "node1", target: "node2", name: "Link 1-2" },
    { source: "node1", target: "node3", name: "Link 1-3" },
    { source: "node1", target: "node4", name: "Link 1-4" },
    { source: "node2", target: "node3", name: "Link 2-3" },
    { source: "node2", target: "node4", name: "Link 2-4" },
    { source: "node2", target: "node5", name: "Link 2-5" },
    { source: "node3", target: "node4", name: "Link 3-4" },
    { source: "node3", target: "node5", name: "Link 3-5" },
    { source: "node3", target: "node6", name: "Link 3-6" },
    { source: "node4", target: "node5", name: "Link 4-5" },
    { source: "node4", target: "node6", name: "Link 4-6" },
    { source: "node4", target: "node7", name: "Link 4-7" },
    { source: "node5", target: "node6", name: "Link 5-6" },
  ];

 

3단계 : 그래프 구성요소 만들기

새로운 구성요소를 만들 것입니다. 구성요소가 마운트 될 때 useEffect 후크를 사용하여 그래프를 초기화합니다.

D3.js는 force-directed 그래프를 생성하는 데 사용할 수 있는 'forceSimulation'기능을 제공합니다.

여기에서는 useEffect Hooks에서 그래프를 생성하는 데 사용할 것입니다.
forceSimulation 기능은 force에 따라 노드의 위치를 자동으로 업데이트하는 시뮬레이션을 실행합니다.

이 시뮬레이션의 tick 이벤트를 수신하여 SVG에서 링크, 그리고 노드의 위치를 업데이트 할 수 있습니다.
여기서 노드의 x, y 속성과 링크의 x1, y1, x2, y2 속성을 노드의 현재 위치를 기준으로 설정합니다.

import React, { useRef, useEffect } from "react";
import * as d3 from "d3";

function Graph({ data }) {
	//...

  const svgRef = useRef();
  const width = 1600;
  const height = 1000;

  useEffect(() => {
    const simulation = d3
      .forceSimulation(nodes)
      .force(
        "link",
        d3
          .forceLink(links)
          .id((d) => d.id)
          .distance(100)
      )
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2));

    const svg = d3.select(svgRef.current);

    const link = svg
      .append("g")
      .attr("stroke", "#999")
      .attr("stroke-opacity", 0.6)
      .selectAll("line")
      .data(links)
      .join("line")
      .attr("stroke-width", (d) => Math.sqrt(d.value));

    const node = svg
      .append("g")
      .attr("stroke", "#fff")
      .attr("stroke-width", 1.5)
      .selectAll("circle")
      .data(nodes)
      .join("circle")
      .attr("r", 5)
      .attr("fill", "#69b3a2");

    const labels = svg
      .append("g")
      .selectAll("text")
      .data(nodes)
      .join("text")
      .attr("text-anchor", "middle")
      .attr("dominant-baseline", "central")
      .text((d) => d.name);

    function getConnectedNodes(node, depth = 0, nodeSet = new Set()) {
      if (depth > 2) return nodeSet;
      nodeSet.add(node.id);
      links.forEach((link) => {
        if (link.source === node.id && !nodeSet.has(link.target)) {
          getConnectedNodes(
            nodes.find((n) => n.id === link.target),
            depth + 1,
            nodeSet
          );
        }
        if (link.target === node.id && !nodeSet.has(link.source)) {
          getConnectedNodes(
            nodes.find((n) => n.id === link.source),
            depth + 1,
            nodeSet
          );
        }
      });
      return nodeSet;
    }

    node.on("click", (event, d) => {
      const connectedNodes = Array.from(getConnectedNodes(d));
      node.attr("fill", (n) =>
        connectedNodes.includes(n.id) ? "#69b3a2" : "#ddd"
      );
      link.style("stroke", (l) =>
        connectedNodes.includes(l.source.id) &&
        connectedNodes.includes(l.target.id)
          ? "#999"
          : "#ddd"
      );
    });

    simulation.on("tick", () => {
      link
        .attr("x1", (d) => d.source.x)
        .attr("y1", (d) => d.source.y)
        .attr("x2", (d) => d.target.x)
        .attr("y2", (d) => d.target.y);

      node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);

      labels.attr("x", (d) => d.x).attr("y", (d) => d.y);
    });
  }, []);

  return (
  	<svg ref={svgRef} width={width} height={height} />
      <g className="nodes" />
      <g className="links" />
    </svg>
  );
}

export default Graph;

SVG에서 링크와 노드를 그리면 아래와 같은 결과가 출력됩니다

 

 

이제, D3를 이용하여 여러분들의 상상의 나래를 펼쳐보아요!

728x90