D3.js forceSimulation Node Interaction
Example program
The nodes, shown as circles, can be dragged.
Example code
<!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. Prepare the data to draw
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. Add SVG elements
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. Configure forceSimulation
var simulation = d3.forceSimulation()
// .force("link", d3.forceLink()) // Not needed here
.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)); // Not needed here
simulation
.nodes(nodesData) // Register node data with the simulation
.on("tick", ticked); // Register the function called on each calculation update
// 4. forceSimulation drawing update function
function ticked() {
node
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
// 5. Drag event functions
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>
Explanation
1. Prepare the data to draw
var nodesData = [];
for(var i = 0; i < 50; i++) {
nodesData.push({
"x": 800 * Math.random(),
"y": 600 * Math.random(),
"r": 30 * Math.random() + 5
});
}
First, prepare the node data, nodesData. forceSimulation updates the x and y coordinates in nodesData, but if x and y are defined first, they are used as the initial positions. The radius is defined as the variable r so it can vary by node.
2. Add SVG elements
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));
Add the SVG elements for nodes. The event functions called during dragging are registered with call(...).
Each node receives a different random radius as follows.
.attr("r", function(d) { return d.r })
3. Configure forceSimulation
var simulation = d3.forceSimulation()
// .force("link", d3.forceLink()) // Not needed here
.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)); // Not needed here
simulation
.nodes(nodesData) // Register node data with the simulation
.on("tick", ticked); // Register the function called on each calculation update
The following interactions can be configured in forceSimulation.
| Setting | Description |
|---|---|
"link" |
The force from links connecting nodes. Omitted here. |
"collide" |
Contact repulsion between nodes. |
"charge" |
Coulomb force, or non-contact force, between nodes. |
"x", "y" |
Position-based field forces. |
"center" |
Center of mass for all nodes. |
"r" |
Radial force. |
The non-link parts are described in more detail below.
“collide”: contact repulsion between nodes
.force("collide",
d3.forceCollide()
.radius(function(d) { return d.r })
.strength(1.0)
.iterations(16))
| Function | Description |
|---|---|
radius |
Sets the radius of the nodes to simulate. The default is 1. Here, function(d) { return d.r; } is set so the radius r defined in nodesData is assigned.Only the radius variable is shown as a function here, but all other parameters can also be set with functions. |
strength |
The repulsive force between overlapping nodes. Set it as a decimal from 0.0 to 1.0. The default is 0.7. |
iterations |
The number of simulation iterations. Increasing the number of iterations makes the calculation much more stable and makes overlaps easier to avoid, but increases calculation time. The default is 1. |
“charge”: Coulomb force between nodes, a non-contact force
.force("charge", d3.forceManyBody().strength(5))
| Function | Description |
|---|---|
strength |
Positive values attract nodes like gravity, while negative values repel nodes like static electricity. The magnitude controls the strength of the force. The default is -30. |
theta |
Integer that determines approximation precision. Calculating Coulomb force for every pair of particles is expensive, so distant groups of nodes are calculated as groups using Barnes-Hut approximation for speed. The default is 0.9. It is not set in this example. |
distanceMin |
Minimum distance used to calculate Coulomb force. This avoids infinite force when two overlapping nodes have distance 0. The default is 1. It is not set in this example. |
distanceMax |
Sets the maximum distance between nodes. If unspecified, returns the current maximum distance. The default is infinity. Specifying a maximum distance improves performance. It is not set in this example. |
“x” and “y”: position-based field forces
.force("x", d3.forceX().strength(0.1).x(400))
.force("y", d3.forceY().strength(0.1).y(300))
| Function | Description |
|---|---|
strength |
A coefficient that determines how much the node returns toward the specified position in one calculation step. With 0.1, the node moves 10% toward the specified position in each calculation step. A value from 0.0 to 1.0 is recommended, and the default is 0.1. |
x |
The center x coordinate of the field force. The default is 0. |
y |
The center y coordinate of the field force. The default is 0. |
“center”: center of mass for all nodes
// .force("center", d3.forceCenter(300, 200)); // Not needed here
| Function | Description |
|---|---|
d3.forceCenter(x, y) |
The center of mass coordinate for all nodes. It helps keep the drawing in the center of the viewport. Unlike other forces, it does not change the relative positions between nodes. |
4. forceSimulation drawing update function
function ticked() {
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
This function is called at each simulation step. It reflects the calculation result in the SVG element position so the SVG elements move.
5. Drag event functions
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;
}
These are the event functions used during dragging. If fx and fy are defined on the node data, that node’s coordinates are fixed. To link movement with the mouse while dragging, the dragged element’s position is fixed when dragging starts, mouse coordinates (event.x, event.y) are reflected while dragging, and the fixed position is released by assigning null when dragging ends.
Also, the simulation stops over time, so if simulation is not active when dragging starts, it is restarted. The alphaTarget set at that time is a coefficient that makes simulation transitions smooth. A value from 0 to 1 can be set; lower values produce smoother movement.