D3.js forceSimulation 강조 표시(highlight)

D3.js forceSimulation에서 클릭한 요소와 관련된 요소와 링크를 강조 표시하는 방법을 설명한다.

예제 프로그램 (노드를 클릭 해본다)

이 예제는는 여기 예제에 클릭시 하이라이트 기능을 추가한 것이다. 코드의 하이라이트 부분만 설명한다. 다른 부분은 여기를 참조한다.

코드 확인

예제 코드

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v5 force simulation node click response</title>
  <!-- 0. 스타일 설정 -->
  <style type="text/css">
    .selected {
      fill: tomato;
    }

    .linkSelected {
      stroke: tomato;
    }

    .conected {
      fill: orange;
    }
  </style>
</head>

<body>
  <svg width="800" height="600"></svg>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    // 1. 그리려는 데이터 준비
    var width = document.querySelector("svg").clientWidth;
    var height = document.querySelector("svg").clientHeight;
    var nodeNumber = 30;
    var nodesData = [];
    for (var i = 0; i < nodeNumber; i++) {
      nodesData.push({
        "index": i,
        "x": width * Math.random(),
        "y": height * Math.random(),
        "r": 10
      });
    }
    var linksData = [];
    for (var i = 0; i < nodeNumber; i++) {
      for (var j = i + 1; j < nodeNumber; j++) {
        if (Math.random() > 0.9) {
          linksData.push({
            "source": i,
            "target": j,
            "l": Math.random() * 150 + 5 + nodesData[i].r + nodesData[j].r
          });
        }
      }
    }

    // 2. svg 요소 추가
    var link = d3.select("svg")
      .selectAll("line")
      .data(linksData)
      .enter()
      .append("line")
      .attr("stroke-width", 2)
      .attr("stroke", "black");

    var d3_drag = d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);

    var node = d3.select("svg")
      .selectAll("circle")
      .data(nodesData)
      .enter()
      .append("circle")
      .attr("r", function (d) { return d.r })
      .attr("fill", "Gold")
      .attr("stroke", "black")
      .call(d3_drag)
      .on("click", clicked);

    function clicked(event, d) {

      d3.selectAll(".selected").classed("selected", false);
      d3.selectAll(".conected").classed("conected", false);
      d3.selectAll("line").classed("linkSelected", false);

      d3.select(this).classed("selected", true);

      d3.selectAll("line")
        .filter(function (v, i) {
          if (d == v.source) {
            node.each(function (vj, j) {
              if (v.target == vj) d3.select(this).classed("conected", true);
            });
            return true;
          } else if (d == v.target) {
            node.each(function (vj, j) {
              if (v.source == vj) d3.select(this).classed("conected", true);
            });
            return true;
          }
        }).classed("linkSelected", true);

    }

    // 3. forceSimulation 설정
    var simulation = d3.forceSimulation()
      .force("link",
        d3.forceLink()
          .distance(function (d) { return d.l; })
          .iterations(2))
      .force("collide",
        d3.forceCollide()
          .radius(function (d) { return d.r; })
          .strength(0.7)
          .iterations(2))
      .force("charge", d3.forceManyBody().strength(-100))
      .force("x", d3.forceX().strength(0.01).x(width / 2))
      .force("y", d3.forceY().strength(0.01).y(height / 2))
      .force("center", d3.forceCenter(width / 2, height / 2));

    simulation
      .nodes(nodesData)
      .on("tick", ticked);
    simulation.force("link")

      .links(linksData)
      .id(function (d) { return d.index; });

    // 4. forceSimulation 그림 업데이트 함수
    function ticked() {
      link
        .attr("x1", function (d) { return d.source.x; })
        .attr("y1", function (d) { return d.source.y; })
        .attr("x2", function (d) { return d.target.x; })
        .attr("y2", function (d) { return d.target.y; });
      node
        .attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; });
    }


    // 5. 드래그 이벤트 함수
    function dragstarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      event.subject.fx = event.subject.x;
      event.subject.fy = event.subject.y;
    }

    function dragged(event, d) {
      event.subject.fx = event.x;
      event.subject.fy = event.y;
    }

    function dragended(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      event.subject.fx = null;
      event.subject.fy = null;
    }
  </script>
</body>

</html>

설명

0. 스타일 설정

<style type="text/css">
.selected {
  fill: tomato;
}

.linkSelected {
  stroke: tomato;
}

.conected {
  fill: orange;
}
</style>

클릭할 때 추가할 스타일을 설정한다.

2. svg 요소 배치 – 클릭 이벤트 등록

selection.on("click", clicked);

function clicked(event, d) {

  d3.selectAll(".selected").classed("selected", false);
  d3.selectAll(".conected").classed("conected", false);
  d3.selectAll("line").classed("linkSelected", false);

  d3.select(this).classed("selected", true);

  d3.selectAll("line")
    .filter(function (v, i) {
      if (d == v.source) {
        node.each(function (vj, j) {
          if (v.target == vj) d3.select(this).classed("conected", true);
        });
        return true;
      } else if (d == v.target) {
        node.each(function (vj, j) {
          if (v.source == vj) d3.select(this).classed("conected", true);
        });
        return true;
      }
    }).classed("linkSelected", true);

}

클릭시 이벤트를 on 메소드로 노드 요소에 등록한다. "click" 이벤트를 등록하고 있지만, 태블릿이나 스마트폰의 "touch" 이벤트에도 자동으로 대응해 준다.

클릭시 이벤트 함수는 먼저 classed 메소드를 사용하여 모든 하이라이트를 해제한다. classed 메소드는 인수에 클래스명(앞에 .를 붙이지 않게 주의)와, boolean의 2개의 인수를 받아, false이면 클래스를 해제, true이면 등록한다. true때 이미 같은 이름의 클래스가 등록되어 있는 경우는 아무것도 하지 않는다.

클릭한 노드 요소에 selected 클래스를 추가한 후 filtereach 메서드를 사용하여 클릭한 노드 요소가 연결된 링크와 노드를 탐색하여 클래스를 추가한다.




최종 수정 : 2024-01-18