{
function binomialRandom(n, p) {
let x = 0;
for (let i = 0; i < n; i++) {
if (Math.random() < p) {
x++;
}
}
return x;
}
function generateData(particles, places) {
const data = [];
particles.forEach((value, index) => {
for (let y = 1; y <= value; y++) {
const colour = `hsl(${Math.floor(Math.random() * 360)}, 70%, 70%)`;
data.push({ x: index - Math.floor(places / 2), y: y, colour: colour });
}
});
return data;
}
function leap(data, places, maxParticles) {
const maxYs = data.reduce((acc, p) => {
if (!acc[p.x] || p.y > acc[p.x].y) {
acc[p.x] = p;
}
return acc;
}, {});
const topmostParticles = Object.values(maxYs);
const particleToModifyIndex = Math.floor(
Math.random() * topmostParticles.length
);
const particleToModify = topmostParticles[particleToModifyIndex];
const minX = Math.ceil(-(places + 1) / 2);
const maxX = Math.floor((places + 1) / 2);
let newX;
if (particleToModify.x === minX) {
newX = particleToModify.x + 1;
} else if (particleToModify.x === maxX) {
newX = particleToModify.x - 1;
} else {
newX = particleToModify.x + (Math.random() < 0.5 ? -1 : 1);
if (newX < minX) newX = minX;
if (newX > maxX) newX = maxX;
}
const maxYAtNewX =
Math.max(0, ...data.filter((p) => p.x === newX).map((p) => p.y)) + 1;
if (maxYAtNewX > maxParticles) {
return data;
}
data.forEach((p) => {
if (p === particleToModify) {
p.x = newX;
p.y = maxYAtNewX;
}
});
return data;
}
function createSvg(width, height, margin) {
return d3.create("svg").attr("viewBox", [0, 0, width, height]);
}
function createScales(places, maxParticles, width, height, margin) {
const x = d3
.scaleLinear()
.domain([-(places + 1) / 2, (places + 1) / 2])
.range([margin.left, width - margin.right]);
const y = d3
.scaleLinear()
.domain([0, maxParticles + 1])
.range([height - margin.bottom, margin.top]);
return { x, y };
}
function createXAxis(g, x, height, margin, places) {
g.attr("transform", `translate(0,${height - margin.bottom})`).call(
d3.axisBottom(x).ticks(places).tickSizeOuter(0)
);
}
function drawParticles(svg, data, x, y, radius) {
return svg
.append("g")
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => y(d.y))
.attr("r", radius)
.attr("fill", (d) => d.colour);
}
function animate(data, places, maxParticles, circles, x, y, duration) {
setInterval(() => {
const newData = leap(data, places, maxParticles);
circles = circles.data(newData).join(
(enter) =>
enter
.append("circle")
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => y(d.y))
.attr("fill", (d) => d.colour)
.call((enter) => enter.transition().duration(duration)),
(update) =>
update.call((update) =>
update
.transition()
.duration(duration)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => y(d.y))
.attr("fill", (d) => d.colour)
),
(exit) =>
exit.call((exit) => exit.transition().duration(duration).remove())
);
}, duration);
}
function main() {
const width = window.innerWidth < 768 ? 300 : 400;
const height = 100
const places = 10;
const maxParticles = 3;
const radius = height / 25;
const margin = { top: 20, right: 30, bottom: 40, left: 40 };
const p = 0.4;
const duration = 500;
const particles = Array.from({ length: places + 1 }, () =>
binomialRandom(maxParticles, p)
);
const data = generateData(particles, places);
const svg = createSvg(width, height, margin);
const { x, y } = createScales(places, maxParticles, width, height, margin);
svg.append("g").call((g) => createXAxis(g, x, height, margin, places));
let circles = drawParticles(svg, data, x, y, radius);
animate(data, places, maxParticles, circles, x, y, duration);
return svg.node();
}
return main();
}
Hello!
Iโm Ross. Iโm Head of Analytics at Companies House ๐๐ก๐.
I like Bayesian statistics, multilevel models and R. I think confidence intervals are overrated and misunderstood.
Hereโs some of my recent stuff:
Date | Title |
---|---|
2025-05-11 | Shut the box! |
2025-04-22 | QUALIFY in SQL |
2024-06-30 | My ideal open data API |
2024-05-28 | Exploring expert systems |
2023-10-31 | How long will my machine last? |
2023-09-06 | Confidence intervals in the wild |
No matching items