function beeswarmCanvas({
data,
canvasWidth = 800,
canvasHeight = 600,
x = (d) => d.x,
y = () => canvasHeight / 2 + Math.random() * 10 - 5,
r = (d) => d.r || 3,
fill = () => "steelblue",
gap = 1,
ticks = 50,
xStrength = 0.8,
yStrength = 0.05,
dynamic = false,
highres = false,
}) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const ratio = window.devicePixelRatio || 1;
if (highres) {
canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
context.scale(ratio, ratio);
} else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
const nodes = data.map((d) => ({
x: x(d),
y: y(d),
r: r(d),
fill: fill(d),
}));
const simulation = d3
.forceSimulation(nodes)
.force("x", d3.forceX((d) => d.x).strength(xStrength))
.force("y", d3.forceY((d) => d.y).strength(yStrength))
.force("collide", d3.forceCollide().radius((d) => d.r + gap).iterations(3))
.tick(ticks)
.stop();
function render() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
for (const node of nodes) {
context.beginPath();
context.arc(node.x, node.y, node.r, 0, 2 * Math.PI);
context.fillStyle = node.fill;
context.fill();
}
}
render();
if (dynamic) {
simulation.on("tick", render)
setTimeout(() => simulation.restart(), 100);
}
return canvas;
}