D3.js v4/v5 force simulation ノード相互作用 – サンプル
D3.jsのforceSimulationのプログラムデモです(v4/v5対応)。リンクなしバージョンです。ノード間の相互作用を説明します。
ノード(丸いやつ)をドラックしてください。
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>D3 v5 force simulation node detail</title> </head> <body> <svg width="800" height="600"></svg> <script src="https://d3js.org/d3.v5.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) .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(d) { if(!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if(!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } </script> </body> </html> |
解説
1. 描画用のデータ準備
1 2 3 4 5 6 7 8 |
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)を準備します。forceSimulationによりnodesDataのx、y座標が更新されますが、最初にx、yを定義しておけば初期位置として設定できます。半径をノードごとに変えたいのでrを変数にして定義しておきます。
2. svg要素を配置
1 2 3 4 5 6 7 8 9 10 11 12 |
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( )でドラッグ時のイベント関数を登録しています。
また、
1 |
.attr("r", function(d) { return d.r }) |
の部分で、ノードごとに異なる半径を割り当てています。
3. forceSimulation設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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" : ラジアルフォース(こちらを参照)
link以外の部分を詳細に見ていきます。
"collide" : ノード間の接触反発力
1 2 3 4 5 |
.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" : ノード間のクーロン力(非接触作用力)
1 |
.force("charge", d3.forceManyBody().strength(5)) |
strength | 正の値を指定すると重力と同様にノードが互いに引き寄せられ、負の値を指定すると静電気と同様にノードが互いに反発します。値の大きさで力の大きさを設定します。デフォルトは-30。 |
theta | 計算の近時の精度を決める定数。全ての粒子間のクーロン力を計算すると時間がかかるため、遠くにあるノード塊として計算する(Barnes-Hut近似)ことで高速化しています。デフォルトは0.9。(今回は未設定) |
distanceMin | クーロン力を計算する最小距離。二つのノードが重なると距離が0になり、力が無限大になることを回避します。デフォルトは1。(今回は未設定) |
distanceMax | ノード間の最大距離を設定します。指定されていない場合は、現在の最大距離を返す。デフォルトは無限大。最大距離を指定すると、パフォーマンスが向上します。(今回は未設定) |
"x", "y" : 位置に基づく場の力
1 2 |
.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" : 全てのノードの質量中心
1 |
// .force("center", d3.forceCenter(300, 200)); // 今回は不使用 |
d3.forceCenter(x, y) | 全てのノードの質量中心の座標です。ビューポートの中央に描画を保持するのに役立ちます。他の作用力とは異なり、ノード間の相対位置を変更しません。 |
4. forceSimulation 描画更新用関数
1 2 3 4 5 |
function ticked() { node .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); } |
シミュレーションのステップごとに呼び出される関数です。svg要素を動かすために計算結果をsvg要素の位置に反映します。
5. forceSimulation 描画更新用関数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function dragstarted(d) { if(!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if(!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } |
ドラッグ時のイベント関数です。ノード用のデータにfx, fyが定義されている場合、そのノードの座標が固定されます。ドラッグ中にマウスと動作を連動させるため、ドラッグ開始時にドラック要素の位置を固定し、ドラッグ中はマウス座標(d3.event.x, d3.event.y)を反映して、ドラッグ終了時に固定を解除(nullを代入)します。
また、simulationは時間がたつと停止する仕様になっているので、ドラッグ開始時にsimulationがactiveでない場合はリスタートさせます。このときに設定しているalphaTargetは、シミュレーションを滑らかに繋げるための係数で0~1の値を設定でき、低い値の方が滑らかになります。
まとめ
ここでは、ノード相互作用の詳細をまとめました。次にリンクによる相互作用を見ていきたいと思います。