Basic D3.js forceSimulation Structure
Example program
The nodes, shown as circles, can be dragged.
D3.js forceSimulation calculates node positions while considering spring forces and other forces. Before v3, this was defined as the force layout, but the same functions can no longer be used, so code must be rewritten for later versions. Several parameters can be configured, but this page explains the minimum demo and code needed to start using it immediately.
Example code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 v5 force simulation</title>
</head>
<body>
<svg width="400" height="300"></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// 1. Prepare the data to draw
var nodesData = [
{},
{},
{},
{},
{},
{}
]
var linksData = [
{ "source": 0, "target": 1 },
{ "source": 1, "target": 4 },
{ "source": 2, "target": 3 },
{ "source": 2, "target": 5 },
{ "source": 5, "target": 1 }
]
// 2. Add SVG elements
var link = d3.select("svg")
.selectAll("line")
.data(linksData)
.enter()
.append("line")
.attr("stroke-width", 1)
.attr("stroke", "black");
var node = d3.select("svg")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("r", 7)
.attr("fill", "LightSalmon")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// 3. Configure forceSimulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink())
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(200, 150));
simulation
.nodes(nodesData)
.on("tick", ticked);
simulation.force("link")
.links(linksData);
// 4. forceSimulation drawing update function
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. 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>
Code explanation
1. Prepare the data to draw
var nodesData = [
{},
{},
{},
{},
{},
{}
]
var linksData = [
{ "source": 0, "target": 1 },
{ "source": 1, "target": 4 },
{ "source": 2, "target": 3 },
{ "source": 2, "target": 5 },
{ "source": 5, "target": 1 }
]
nodesData is the node data. If you only need to draw nodes, an array of empty objects is enough. linksData is the link data and requires the IDs of the two connected nodes, source and target.
2. Add SVG elements
var link = d3.select("svg")
.selectAll("line")
.data(linksData)
.enter()
.append("line")
.attr("stroke-width", 1)
.attr("stroke", "black");
var node = d3.select("svg")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("r", 7)
.attr("fill", "LightSalmon")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
Add SVG elements for links and nodes. The event functions for dragging are registered with call(), which makes the nodes draggable.
3. Configure forceSimulation
This is where forceSimulation is configured.
Enable spring force from links.
d3.forceSimulation().force("link", d3.forceLink())
Enable Coulomb force between nodes. By default, a repulsive force is configured. forceSimulation still works if this line is omitted.
.force("charge", d3.forceManyBody())
Set the center position for all nodes. It also works without this setting, but without it the layout behaves like it is in zero gravity, so elements that move outside the screen may not come back.
.force("center", d3.forceCenter(200, 150));
Register the node data array with the simulation, and register the function called on each calculation update with .on('tick', ...). Calculation results are written back into the node data array, so the results must be reflected in SVG element positions to move the SVG elements.
simulation
.nodes(nodesData)
.on("tick", ticked);
Register the link data array with the simulation.
simulation.force("link")
.links(linksData);
4. forceSimulation drawing update function
The following function is called on each calculation update. It reflects the calculation results in the SVG element positions. The source and target values in the link data are no longer the original numbers; they become references to the node data array.
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. Drag event functions
These are the event functions used while dragging. The value stored in d is the registered node data. If fx and fy are defined on the node data, that node’s coordinates are fixed. To make the node follow the mouse while dragging, the dragged element’s position is fixed when dragging starts, the 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 the simulation is not active when dragging starts, it is restarted. The alphaTarget set at that point is a coefficient introduced in v4 to make simulation transitions smoother. In v3, nodes appear to jump when restarted. A value from 0 to 1 can be set; lower values are smoother, but if it is set to 0, nodes do not move at all when restarted.
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;
}