A moving bubbles plot shows state changes over time. Each focus represents a state. Each node represents an object being tracked. As the state changes, the color changes and the node moves to its new focus.
I wrote this example, because so many have been written using earlier versions of D3. This example is written with version 5.7.0. Let’s start by creating the foci and the nodes.
let foci = {};
for (let i = 0; i < fociCount; i++) {
let angle = 2 * Math.PI / fociCount * i;
let focus = {
index: i,
color: d3.interpolateRainbow(i / fociCount),
x: center.x + centerRadius * Math.cos(angle),
y: center.y + centerRadius * Math.sin(angle)
};
foci[i] = focus;
}
let nodes = [];
for (let i = 0; i < nodeCount; i++) {
let focus = pickRandom(foci);
let node = {
index: i,
focus: focus.index,
radius: 5,
x: randBetween(0, WIDTH),
y: randBetween(0, HEIGHT)
};
nodes.push(node);
}
Define the SVG element.
svg = d3.select("#canvas")
.attr("width", WIDTH + 2 * MARGIN)
.attr("height", HEIGHT + 2 * MARGIN)
.style("border", "1px solid #c0c0c0")
.append("g")
.attr("class", "margin")
.attr("transform", `translate(${MARGIN}, ${MARGIN})`);
Define the simulation.
simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-10).distanceMin(5))
.force("collide", d3.forceCollide(5))
.force("position-x", d3.forceX(d => foci[d.focus].x).strength(0.1))
.force("position-y", d3.forceY(d => foci[d.focus].y).strength(0.1))
.on("tick", onSimulationTick);
The rest of the code is pretty routine stuff, so it isn’t necessary to list them here. The complete code for this page is available in Github.
The form elements below let you change the counts and forces applied to the nodes.