D3.js forceSimulationのノード相互作用

D3.js forceSimulationでリンク、つまりedgeがないノード間の相互作用を説明する。

サンプルプログラム

ノード、つまり円をドラッグできる。

コードを確認

サンプルコード

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v7 force simulation node detail</title>
</head>

<body>
  <svg width="800" height="550"></svg>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    // 1. 描画するデータを用意
    var nodesData = [];
    for (var i = 0; i < 50; i++) {
      nodesData.push({
        "x": 800 * Math.random(),
        "y": 600 * Math.random(),
        "r": 30 * Math.random() + 5
      });
    }

    // 2. SVG要素を追加
    var node = d3.select("svg")
      .selectAll("circle")
      .data(nodesData)
      .enter()
      .append("circle")
      .attr("r", function (d) { return d.r })
      .attr("fill", "LightSalmon")
      .attr("stroke", "black")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    // 3. forceSimulationを設定
    var simulation = d3.forceSimulation()
      // .force("link", d3.forceLink()) // ここでは不要
      .force("collide",
        d3.forceCollide()
          .radius(function (d) { return d.r })
          .strength(1.0)
          .iterations(16))
      .force("charge", d3.forceManyBody().strength(5))
      .force("x", d3.forceX().strength(0.1).x(400))
      .force("y", d3.forceY().strength(0.1).y(300));
    // .force("center", d3.forceCenter(300, 200)); // ここでは不要

    simulation
      .nodes(nodesData)    // simulationにノード用データを登録
      .on("tick", ticked); // 計算更新ごとに呼び出す関数を登録

    // 4. forceSimulationの描画更新関数
    function ticked() {
      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();
      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>

説明

1. 描画するデータを用意

  var nodesData = [];
  for(var i = 0; i < 50; i++) {
    nodesData.push({
      "x": 800 * Math.random(),
      "y": 600 * Math.random(),
      "r": 30 * Math.random() + 5
    });
  }

まずノードのデータであるnodesDataを用意する。forceSimulationnodesDataxy座標を更新するが、先にxyを定義しておくと初期位置として設定できる。半径をノードごとに変えるため、rを変数として定義している。

2. SVG要素を追加

var node = d3.select("svg")
  .selectAll("circle")
  .data(nodesData)
  .enter()
  .append("circle")
  .attr("r", function(d) { return d.r })
  .attr("fill", "LightSalmon")
  .attr("stroke", "black")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

ノードのSVG要素を追加する。call(...)でドラッグ時のイベント関数を登録している。

また、次のようにノードごとに異なる半径、つまりランダム値を設定している。

.attr("r", function(d) { return d.r })

3. forceSimulationを設定

var simulation = d3.forceSimulation()
  // .force("link", d3.forceLink()) // ここでは不要
  .force("collide",
    d3.forceCollide()
    .radius(function(d) { return d.r })
    .strength(1.0)
    .iterations(16))
  .force("charge", d3.forceManyBody().strength(5))
  .force("x", d3.forceX(400).strength(0.1))
  .force("y", d3.forceY(300).strength(0.1));
// .force("center", d3.forceCenter(400, 300)); // ここでは不要

simulation
  .nodes(nodesData)    // simulationにノード用データを登録
  .on("tick", ticked); // 計算更新ごとに呼び出す関数を登録

forceSimulationでは次のような相互作用を設定できる。

設定 説明
"link" ノード間を接続するリンクの作用力。ここでは省略する。
"collide" ノード間の接触反発力。
"charge" ノード間のクーロン力、つまり非接触作用力。
"x", "y" 位置ベースの場の力。
"center" すべてのノードの質量中心。
"r" ラジアルフォース。

リンク以外の部分を詳しく見ていく。

“collide”: ノード間の接触反発力

.force("collide",
  d3.forceCollide()
  .radius(function(d) { return d.r })
  .strength(1.0)
  .iterations(16))
関数 説明
radius simulationするノードの半径を設定する。
デフォルトは1である。
今回はfunction(d) { return d.r; }を設定し、nodesDataで定義した半径rを割り当てている。
ここでは半径だけを関数にしているが、他のパラメータもすべて関数で設定できる。
strength 重なったノード間の反発力である。
0.0から1.0の小数で設定する。
デフォルトは0.7である。
iterations simulationの反復回数。
反復回数を増やすと計算が大きく安定し、ノードの重なりを避けやすくなるが、計算時間は増える。
デフォルトは1である。

“charge”: ノード間のクーロン力、非接触作用力

.force("charge", d3.forceManyBody().strength(5))
関数 説明
strength 正の値を指定すると重力のようにノード同士が引き合い、負の値を指定すると静電気のようにノード同士が反発する。
値の大きさで力の強さを設定する。
デフォルトは-30である。
theta 計算近似の精度を決める整数。
すべての粒子間のクーロン力を計算すると時間がかかるため、遠くにあるノードをひとまとまりとして計算するBarnes-Hut近似で高速化している。
デフォルトは0.9である。今回は設定していない。
distanceMin クーロン力を計算する最小距離。
2つのノードが重なると距離が0になり、力が無限大になることを避ける。
デフォルトは1である。今回は設定していない。
distanceMax ノード間の最大距離を設定する。
指定していない場合は現在の最大距離を返す。
デフォルトは無限大である。
最大距離を指定すると性能が向上する。今回は設定していない。

“x”, “y”: 位置ベースの場の力

.force("x", d3.forceX().strength(0.1).x(400))
.force("y", d3.forceY().strength(0.1).y(300))
関数 説明
strength 強度を表す指標で、計算1ステップで指定位置へどの程度戻るかを決める係数である。
0.1なら指定位置へ計算1ステップで10%移動する。
0.0から1.0が推奨値で、デフォルトは0.1である。
x 場の力の中心x座標。デフォルトは0である。
y 場の力の中心y座標。デフォルトは0である。

“center”: すべてのノードの質量中心

// .force("center", d3.forceCenter(300, 200)); // ここでは不要
関数 説明
d3.forceCenter(x, y) すべてのノードの質量中心座標である。
描画をビューポート中央に維持するのに役立つ。
他の作用力とは異なり、ノード間の相対位置は変更しない。

4. forceSimulationの描画更新関数

function ticked() {
  node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
}

シミュレーションのステップごとに呼び出される関数である。SVG要素を移動するため、計算結果をSVG要素の位置へ反映する。

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;
}

ドラッグ時のイベント関数である。ノードのデータにfxfyが定義されている場合、そのノードの座標は固定される。ドラッグ中にマウスの動きと連動させるため、ドラッグ開始時にドラッグ要素の位置を固定し、ドラッグ中はマウス座標(event.xevent.y)を反映し、ドラッグ終了時に固定を解除するためnullを代入する。

また、simulationは時間が経つと停止する仕様なので、ドラッグ開始時にsimulationactiveでない場合は再起動する。このとき設定しているalphaTargetはシミュレーションをなめらかにつなげるための係数で、0から1の値を設定でき、低い値ほどなめらかになる。