Published
Edited
Sep 28, 2021
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg.append("g").call(xAxis);

const h = height - margin.bottom - radius - padding;

svg
.append("g")
.selectAll("circle")
.data(
beeswarm(data, {
radius: radius * 2 + padding,
x: (d) => x(d.value),
sort: sort,
side: side
})
)
.join("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => (h * side) / 2 + h / 2 - d.y)
.attr("r", radius)
.append("title")
.text((d) => d.data.name);

return svg.node();
}
Insert cell
function is_y_feasible(poty, nearby_circles, start_idx, end_idx) {
for (let i = start_idx; i < end_idx; i++) {
let { forbiddenYBottom, forbiddenYTop } = nearby_circles[i];
if (poty > forbiddenYBottom && poty < forbiddenYTop) return false;
}
return true;
}
Insert cell
function can_use_poty(j, poty, nearby_circles) {
// For speed, only check circles that are within a y distance of 1 from the potential y value `poty`
let start = j;
while (start > 0 && nearby_circles[start - 1].y > poty - 1) --start;
let end = j;
while (end < nearby_circles.length && nearby_circles[end].y < poty + 1) ++end;
return is_y_feasible(poty, nearby_circles, start, end);
}
Insert cell
function beeswarm(
data,
{ radius = 1, x = (d) => d, side = 1, sort = true } = {}
) {
const circles = data
.map((d, i, data) => ({
x: +x(d, i, data) / radius, // divide by radius to simplify calculations
data: d,
y: 0,
priority: i
}))
.sort((a, b) => a.x - b.x);

let n = data.length;
let order = [];
for (let i = 0; i < n; i++) {
if (sort) circles[i].priority = i;
order[circles[i].priority] = i;
}

for (let iter = 0; iter < n; iter++) {
let i = order[iter];

let nearby_circles = [];

let start = i;
while (start > 0 && circles[start - 1].x > circles[i].x - 1) --start;
for (let j = start; j < n; j++) {
if (circles[j].priority >= iter) continue; // skip unplaced points
if (circles[j].x >= circles[i].x + 1) break;
let x_diff = circles[i].x - circles[j].x;
let dx_sq = x_diff * x_diff;
nearby_circles.push({
y: circles[j].y,
priority: circles[j].priority,
forbiddenYBottom: circles[j].y - Math.sqrt(1 - dx_sq),
forbiddenYTop: circles[j].y + Math.sqrt(1 - dx_sq)
});
}

nearby_circles.sort((a, b) => a.y - b.y);

if (is_y_feasible(0, nearby_circles, 0, nearby_circles.length)) {
circles[i].y = 0;
} else {
let yAbs = Infinity;
let sides = side === 0 ? [1, -1] : side === 1 ? [1] : [-1];
for (let s of sides) {
let p = s === 1 ? n : -1; // priority of the existing point from which circles[i].y was computed
for (let j = 0; j < nearby_circles.length; j++) {
let poty =
s == -1
? nearby_circles[j].forbiddenYBottom
: nearby_circles[j].forbiddenYTop;
let potyAbs = Math.abs(poty);
let is_improvement =
potyAbs <= yAbs &&
(potyAbs < yAbs || nearby_circles[j].priority < p);
if (is_improvement && can_use_poty(j, poty, nearby_circles)) {
circles[i].y = poty;
yAbs = Math.abs(poty);
p = nearby_circles[j].priority;
}
}
}
}
}

var result = circles.map(({ x, y, data }) => ({
x: x * radius,
y: y * radius,
data
}));
return result;
}
Insert cell
data = (await FileAttachment("cars-2.csv").csv({typed: true})).map(({Name: name, Weight_in_lbs: value}) => ({name, value: +value}))
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(data, d => d.value))
.range([margin.left, width - margin.right])
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
Insert cell
height = 240
Insert cell
radius = 3
Insert cell
padding = 1.5
Insert cell
margin = ({top: 20, right: 20, bottom: 30, left: 20})
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