Skip to content

Instantly share code, notes, and snippets.

@polochau
Last active June 7, 2021 11:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polochau/e8b0e4bacbe861163848dafadf4dca04 to your computer and use it in GitHub Desktop.
Save polochau/e8b0e4bacbe861163848dafadf4dca04 to your computer and use it in GitHub Desktop.
Collision-free Scatter Plot

Collision-free Scatter Plot

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment