Using D3 v5's force simulation, points repel each other with animation so they do not overlap.
Simulation may be turned on or off. When off, points return back to their original overlapping locations.
Using D3 v5's force simulation, points repel each other with animation so they do not overlap.
Simulation may be turned on or off. When off, points return back to their original overlapping locations.
<html> | |
<head> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
</head> | |
<body> | |
<input type="checkbox" id="repulsion-checkbbox" onclick="changeRepulsion(this.checked)"> Points repel | |
<br/><br/> | |
<svg id="scatterplot" width="400" height="300" style="background-color: #f0f0f0;"></svg> | |
<script> | |
// Toy data with 5 points | |
let data = [ | |
{"x":50, "y":50, "r":10, "fill":"#990000", "fill_opacity": 0.4}, | |
{"x":55, "y":55, "r":20, "fill":"#009900", "fill_opacity": 0.4}, | |
{"x":60, "y":65, "r":15, "fill":"#000099", "fill_opacity": 0.4}, | |
{"x":130, "y":165, "r":10, "fill":"#009999", "fill_opacity": 0.4}, | |
{"x":130, "y":165, "r":20, "fill":"#900099", "fill_opacity": 0.4} | |
]; | |
// Creates force simulation that, when enabled, allows points to repel each other | |
let sim = d3.forceSimulation(data); | |
sim.force("collision", d3.forceCollide(d => d.r)); // Repulsion force | |
sim.force("x_force", d3.forceX(d => d.x)); // Each point attacted to its center x and y | |
sim.force("y_force", d3.forceY(d => d.y)); | |
sim.on('tick', drawPlot); // Redraws scatterplot at every simulation "tick" | |
// Uncomment both lines below lets simulation run forever with obvious movements | |
// sim.alphaDecay(0); // Allows simulation to run forever, numerically | |
// sim.velocityDecay(0); // Movements become obvious | |
sim.stop(); // Simulation is off initially | |
drawPlot(); // Draws scatterplot (points overlap as simulation is off) | |
// Checks checkbox | |
document.getElementById("repulsion-checkbbox").click(); | |
/** | |
* Draws points and labels in scatterplot as described in data. | |
* For points and labbels that are already drawn, update their locations, | |
* using (updated) values in data. | |
*/ | |
function drawPlot(){ | |
let points = d3.select("#scatterplot").selectAll("circle").data(data); | |
let labels = d3.select("#scatterplot").selectAll("text").data(data); | |
// For new points and labels (not drawn yet, e.g., when page loads), draw them | |
points.enter().append("circle") | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y) | |
.attr("r", d => d.r) | |
.attr("fill", d => d.fill) | |
.attr("fill-opacity", d => d.fill_opacity); | |
labels.enter().append("text") | |
.text(d => d.x + ", " + d.y) | |
.attr("x", d => d.x + d.r) | |
.attr("y", d => d.y) | |
.attr("alignment-baseline", "middle"); // Vertically align text with point | |
// For existing points already drawn, update their locations, | |
// using (updated) values in data | |
points | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y); | |
labels | |
// .text(d => d.x + ", " + d.y) | |
.attr("x", d => d.x + d.r) | |
.attr("y", d => d.y); | |
} | |
/** | |
* Points repel each other, when checkbbox is checked. | |
* When unchecked, repulsion force is removed. | |
*/ | |
function changeRepulsion(isCheckboxChecked){ | |
if (isCheckboxChecked){ | |
sim.force("collision", d3.forceCollide(d => d.r)); // Add repulsion force back | |
} else { | |
sim.force("collision", null); // Removes repulsion force | |
} | |
sim.alpha(1); // Alpha value of 1 resets simulation back to beginning | |
sim.restart(); | |
} | |
</script> | |
</body> | |
</html> |