Published
Edited
May 14, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
positionedData = new AccurateBeeswarm(
data,
d => rScale(d.pop) + .2,
d => xScale(d.pct_change)
).calculateYPositions()
Insert cell
data = mobility_data.filter(d => d.in_lockdown === "TRUE")
Insert cell
class AccurateBeeswarm {
constructor(items, radiusFun, xFun) {
this.items = items;
this.radiusFun = radiusFun;
this.xFun = xFun;
this.tieBreakFn = this._sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
this.maxR = Math.max(...items.map(d => radiusFun(d)));
this.rng = this._sfc32(1, 2, 3, seed);
}

calculateYPositions() {
let all = this.items
.map((d, i) => ({
datum: d,
originalIndex: i,
x: this.xFun(d),
y: null,
placed: false,
forbiddenY: new UnionOfIntervals(),
bestPosition: 0,
heapPos: -1
}))
.sort((a, b) => a.x - b.x);
all.forEach(function(d, i) {
d.index = i;
});
let tieBreakFn = this.tieBreakFn;
all.forEach(function(d) {
d.tieBreaker = tieBreakFn(d.x);
});
let allSortedByPriority = [...all].sort((a, b) => {
let key_a = this.radiusFun(a.datum) + a.tieBreaker * randomness1;
let key_b = this.radiusFun(b.datum) + b.tieBreaker * randomness1;
return key_b - key_a;
});
for (let item of allSortedByPriority) {
item.placed = true;
// If nothing placed overlaps this circle on the x axis (even if
// they were side by side), choose the y position at random. This
// makes the plot look a bit more natural and less like the Mandelbrot
// set!
item.y =
item.forbiddenY.head.next != item.forbiddenY.tail
? item.bestPosition
: this.radiusFun(item.datum) * (this.rng() - .5) * randomness2;
this._updateForbiddenYIntervals(item, all);
}
all.sort((a, b) => a.originalIndex - b.originalIndex);
return all.map(d => ({ datum: d.datum, x: d.x, y: d.y }));
}

// Random number generator (for reproducibility)
// https://stackoverflow.com/a/47593316
_sfc32(a, b, c, d) {
let rng = function() {
a >>>= 0;
b >>>= 0;
c >>>= 0;
d >>>= 0;
var t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
for (let i = 0; i < 10; i++) {
rng();
}
return rng;
}

_updateForbiddenYIntervals(item, all) {
for (let step of [-1, 1]) {
let xDist;
let r = this.radiusFun(item.datum);
for (
let i = item.index + step;
i >= 0 &&
i < all.length &&
(xDist = Math.abs(item.x - all[i].x)) < r + this.maxR;
i += step
) {
let other = all[i];
if (other.placed) continue;
let sumOfRadii = r + this.radiusFun(other.datum);
if (xDist >= r + this.radiusFun(other.datum)) continue;
let yDist = Math.sqrt(sumOfRadii * sumOfRadii - xDist * xDist);
let forbiddenInterval = [item.y - yDist, item.y + yDist];
other.forbiddenY.addInterval(forbiddenInterval);
other.bestPosition = other.forbiddenY.bestAvailableValue();
}
}
}
}
Insert cell
Insert cell
class UnionOfIntervals {
constructor() {
this.head = { right: -Infinity };
this.tail = { left: Infinity };
this.head.next = this.tail;
this.tail.prev = this.head;
}

del(i) {
i.prev.next = i.next;
i.next.prev = i.prev;
}

insertBefore(interval, i) {
interval.prev = i.prev;
interval.next = i;
interval.prev.next = interval;
i.prev = interval;
}

addInterval(interval) {
let l = interval[0];
let r = interval[1];
interval = { left: l, right: r };
let nxt;
for (let i = this.head.next; i != this.tail; i = nxt) {
nxt = i.next;
// if `i` contains `interval`, do nothing
if (i.left <= l && i.right >= r) {
return;
}
// if `interval` contains `i`, delete `i`
if (l <= i.left && r >= i.right) {
this.del(i);
}
}
for (let i = this.head.next; i != this.tail; i = nxt) {
nxt = i.next;
if (r <= i.left) {
this.insertBefore(interval, i);
return;
}
if (r > i.left && r < i.right) {
i.left = l;
return;
}
if (l < i.right) {
if (r > i.next.left) {
// combine i and i.next
i.right = i.next.right;
this.del(i.next);
} else {
i.right = r;
}
return;
}
}
this.insertBefore(interval, this.tail);
}

bestAvailableValue() {
let best = Infinity;
let zeroAvailable = true;
for (let i = this.head.next; i != this.tail; i = i.next) {
if (i.left < 0 && i.right > 0) zeroAvailable = false;
if (Math.abs(i.left) < Math.abs(best)) best = i.left;
if (Math.abs(i.right) < Math.abs(best)) best = i.right;
}
return zeroAvailable ? 0 : best;
}
}
Insert cell
Insert cell
mobility_data = d3.csv("https://static01.nyt.com/newsgraphics/2020/03/27/coronavirus-behavior-int/d08526cd59a705b40c03e9950174af4a5bb72835/pct_change_04_01.csv")
Insert cell
Insert cell
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