D3.js forceSimulationのノードに複数のSVG要素を含める方法

D3.jsでノードに複数のSVG要素を含め、テキストなどの要素を同時にドラッグする方法を説明する。

サンプルプログラム

D3.js forceSimulationで、複数のSVG要素を1つのドラッグ可能なノードとしてまとめるデモである。

コードを確認

サンプルコード

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v7 force simulation group element</title>
</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 nodesData = [];
    for (var i = 0; i < 50; i++) {
      nodesData.push({
        "x": width * Math.random(),
        "y": height * Math.random(),
        "r": 40 * Math.random() + 5
      });
    }

    // 2. SVG要素を追加
    var nodeGroup = d3.select("svg")
      .selectAll("g")
      .data(nodesData)
      .enter()
      .append("g")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    nodeGroup.append("circle")
      .attr("cx", function (d) { return d.x; })
      .attr("cy", function (d) { return d.y; })
      .attr("r", function (d) { return d.r })
      .attr("fill", "Gold")
      .attr("stroke", "black")
      .append("title")
      .text("This is title.");

    nodeGroup.append("text")
      .attr("x", function (d) { return d.x; })
      .attr("y", function (d) { return d.y; })
      .attr("text-anchor", "middle")
      .attr("dominant-baseline", "middle")
      .style("fill", "steelblue")
      .text("Ball")
      .append("title")
      .text("This is title.");

    // 3. forceSimulationを設定
    var simulation = d3.forceSimulation()
      .force("collide",
        d3.forceCollide()
          .radius(function (d) { return d.r + 1 }))
      .force("charge", d3.forceManyBody())
      .force("x", d3.forceX().strength(0.05).x(width / 2))
      .force("y", d3.forceY().strength(0.05).y(height / 2));

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

    // 4. forceSimulationの描画更新関数
    function ticked() {
      nodeGroup.select("circle")
        .attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; });
      nodeGroup.select("text")
        .attr("x", function (d) { return d.x; })
        .attr("y", function (d) { return d.y; });
    }

    // 5. ドラッグイベント関数
    function dragstarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

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

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

</html>

説明

このサンプルプログラムはforceSimulationを使用する。forceSimulationの詳細はこちらを参照する。

プログラムの一部だけを説明する。

  var nodeGroup = d3.select("svg")
    .selectAll("g")
    .data(nodesData)
    .enter()
    .append("g")
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));

まずグループ要素を表す<g>要素を作る。<g>要素にノードのデータ配列を割り当て、ドラッグイベントを登録する。

次に、<g>タグの子要素として<circle><text>を設定する。<g>要素にはノードのデータ配列が割り当てられているため、それを参照して使用できる。また、<circle><text>の子要素としてtitleも設定できる。サンプルプログラムではノードにカーソルを置くとタイトル文字列が表示される。ただし、タブレットやスマートフォンでは表示されない。

  nodeGroup.append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", function(d) { return d.r })
    .attr("fill", "Gold")
    .attr("stroke", "black")
    .append("title")
    .text("This is title.");

  nodeGroup.append("text")
    .attr("x", function(d) { return d.x; })
    .attr("y", function(d) { return d.y; })
    .attr("text-anchor", "middle")
    .attr("dominant-baseline", "middle")
    .style("fill", "steelblue")
    .text("Ball")
    .append("title")
    .text("This is title.");

forceSimulationを使用せず、ドラッグだけで動かす場合は、イベント関数を次のように変更する。

function dragged(event, d) {
  d3.select(this).select("circle")
    .attr("cx", d.x = event.x)
    .attr("cy", d.y = event.y);
  d3.select(this).select("text")
    .attr("x", d.x = event.x)
    .attr("y", d.y = event.y);
}