Public
Edited
Feb 21
Fork of Beeswarm
Insert cell
Insert cell
chart = {
const width = 928;
const height = 160;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 20;
const marginLeft = 20;
const radius = 3;
const padding = 1.5;

const x = d3.scaleLinear()
.domain(d3.extent(cars, d => d["weight (lb)"]))
.range([marginLeft, width - marginRight]);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0));
svg.append("g")
.selectAll()
.data(dodge(cars, {radius: d => d["weight (lb)"]/1000, x: d => x(d["weight (lb)"])}))
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => height - marginBottom - radius - padding - d.y)
.attr("r", d => d.r)
.append("title")
.text(d => d.data.name);

return svg.node();
}
Insert cell
function dodge(data, {radius = 1, x = d => d} = {}) {
// Adapted using ChatGPT reason
// Allow radius to be a function (or a constant).
const getRadius = typeof radius === "function" ? radius : () => radius;
// Precompute each circle’s x and r.
const circles = data.map((d, i, data) => ({
x: +x(d, i, data),
r: +getRadius(d, i, data),
data: d
})).sort((a, b) => a.x - b.x);
const epsilon = 1e-3;
let head = null, tail = null;
// Returns true if a circle at (x,y) with radius r intersects any circle in the queue.
function intersects(x, y, r) {
let a = head;
while (a) {
const sumR = a.r + r;
if ((a.x - x) ** 2 + (a.y - y) ** 2 < sumR * sumR - epsilon) {
return true;
}
a = a.next;
}
return false;
}
// Place each circle sequentially.
for (const b of circles) {
// Remove circles from the queue that can no longer intersect the new circle.
// (For a given circle a in the queue, if its x is less than b.x minus the sum of the two radii,
// it is too far to the left to intersect.)
while (head && head.x < b.x - (head.r + b.r)) {
head = head.next;
}
// Set initial y position.
b.y = 0;
// If b placed at y=0 overlaps an existing circle, find the smallest y that avoids intersection.
if (intersects(b.x, b.y, b.r)) {
let a = head;
b.y = Infinity;
do {
const dx = a.x - b.x;
const sumR = a.r + b.r;
// Only compute the tangent if the circles can possibly overlap horizontally.
if (Math.abs(dx) <= sumR) {
const y = a.y + Math.sqrt(sumR * sumR - dx * dx);
if (y < b.y && !intersects(b.x, y, b.r)) {
b.y = y;
}
}
a = a.next;
} while (a);
}
// Add b to the queue.
b.next = null;
if (head === null) head = tail = b;
else tail = tail.next = b;
}
return circles;
}
Insert cell
Insert cell
Plot.plot({
height: 165,
x: {line: true},
marks: [
Plot.dotX(cars, Plot.dodgeY({
x: "weight (lb)",
sort: "weight (lb)",
title: "name",
fill: "currentColor"
}))
]
})
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more