Code sample to answer this stackoverflow question, on making a beeswarm plot in D3 v5 with variable radius.
Render circles along an axis based on the node's value in a tight formation.
This is a force layout rendered statically. Collisions are detected between circles (see Clustered Force Layout I). Custom gravity drives the circle towards their desired placement along the x-axis. A light preference is employed to center circles on the x-axis respective of their value. This creates a less tightly-packed layout, but the data is represented accurately.
forked from ericandrewlewis's block: Circles on an Axis in a Static Force Layout
xxxxxxxxxx
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.x.axis .domain {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var margin = {top: 50, right: 50, bottom: 50, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
// padding between nodes
padding = 2,
maxRadius = 1000,
numberOfNodes = 50;
// Create random node data.
var data = d3.range(numberOfNodes).map(function() {
var value = Math.floor(Math.random() * 50) / 10,
size = Math.floor( Math.sqrt((value + 1) / numberOfNodes * -Math.log(Math.random())) * maxRadius * 10 ),
datum = {value: value, size: size};
return datum;
});
var x = d3.scaleLinear()
.domain( [0, 5] )
.range( [margin.left, width + margin.right]);
// Map the basic node data to d3-friendly format.
var nodes = data.map(function(node, index) {
return {
radius: node.size / 100,
color: '#ff7f0e',
x: x(node.value),
y: height / 2 + Math.random()
};
});
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.force('forceX', d3.forceX(d => d.x))
.force('forceY', d3.forceY(d => d.y))
.force('collide', d3.forceCollide(d => d.radius))
.on("tick", tick)
.stop();
var xAxis = d3.axisBottom(x);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var loading = svg.append("text")
.attr("x", ( width + margin.left + margin.right ) / 2)
.attr("y", ( height + margin.top + margin.bottom ) / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
/**
* On a tick, apply custom gravity, collision detection, and node placement.
*/
function tick() {
for ( i = 0; i < nodes.length; i++ ) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
/**
* Run the force layout to compute where each node should be placed,
* then replace the loading text with the graph.
*/
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 100;
force.tick(NUM_ITERATIONS);
force.stop();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + ( margin.top + ( height * 3/4 ) ) + ")")
.call(xAxis);
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.style("fill", function(d) { return d.color; })
.attr("cx", function(d) { return d.x} )
.attr("cy", function(d) { return d.y} )
.attr("r", function(d) { return d.radius } );
loading.remove();
}
// Use a timeout to allow the rest of the page to load first.
setTimeout(renderGraph, 10);
</script>
https://d3js.org/d3.v5.min.js