function BeeswarmChart(data, {
value = d => d,
label,
domain,
x = value,
title = null,
radius = 20,
padding = 1.5,
marginTop = 10,
marginRight = 20,
marginBottom = 30,
marginLeft = 20,
width = 640,
height,
xLabel = label,
xDomain = [-51,14],
xRange = [marginLeft, width - marginRight],
annotations
} = {}) {
const X = d3.map(data, x);
const T = title == null ? null : d3.map(data, title);
const I = d3.range(X.length).filter(i => !isNaN(X[i]));
if (xDomain === undefined) xDomain = d3.extent(X);
const xScale = d3.scaleLinear(xDomain, xRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const Y = dodge(I.map(i => xScale(X[i])), radius * 2 + padding);
if (height === undefined) height = (d3.max(Y, Math.abs) + radius + padding) * 2 + marginTop + marginBottom;
function dodge(X, radius) {
const Y = new Float64Array(X.length);
const radius2 = radius ** 2;
const epsilon = 1e-3;
let head = null, tail = null;
function intersects(x, y) {
let a = head;
while (a) {
const ai = a.index;
if (radius2 - epsilon > (X[ai] - x) ** 2 + (Y[ai] - y) ** 2) return true;
a = a.next;
}
return false;
}
for (const bi of d3.range(X.length).sort((i, j) => X[i] - X[j])) {
while (head && X[head.index] < X[bi] - radius2) head = head.next;
if (intersects(X[bi], Y[bi] = 0)) {
let a = head;
Y[bi] = Infinity;
do {
const ai = a.index;
let y1 = Y[ai] + Math.sqrt(radius2 - (X[ai] - X[bi]) ** 2);
let y2 = Y[ai] - Math.sqrt(radius2 - (X[ai] - X[bi]) ** 2);
if (Math.abs(y1) < Math.abs(Y[bi]) && !intersects(X[bi], y1)) Y[bi] = y1;
if (Math.abs(y2) < Math.abs(Y[bi]) && !intersects(X[bi], y2)) Y[bi] = y2;
a = a.next;
} while (a);
}
const b = {index: bi, next: null};
if (head === null) head = tail = b;
else tail = tail.next = b;
}
return Y;
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.append("text")
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));
var defs = svg.append('svg:defs');
data.forEach(function(d, i) {
defs.append("svg:pattern")
.attr("id", "avatar" + i)
.attr("width", radius * 2)
.attr("height", radius * 2)
.attr("patternUnits", "objectBoundingBox")
.append("svg:image")
.attr("xlink:href", d.img_url)
.attr("width", radius * 2)
.attr("height", radius * 2)
.attr("x", 0)
.attr("y", 0);
const dot = svg.append("g")
.selectAll("circle")
.data(I)
.join("circle")
.attr("cx", i => xScale(X[i]))
.attr("cy", i => (marginTop + height - marginBottom) / 2 + Y[i])
.attr("r", radius)
.attr("stroke", i => {
if (data[i].highlight == "Yes") {
return '#5f2b7b';
} else {
return NaN;
}})
.attr("stroke-width", i => {
if (data[i].highlight == "Yes") {
return 2;
} else {
return NaN;
}})
.attr("fill", i => "url(#avatar" + i + ")");
if (T) dot.append("title")
.text(i => T[i]);
});
if (annotations) {
for (let annotation of annotations) {
svg.append("text")
.attr("x", annotation.x)
.attr("y", annotation.y)
.text(annotation.text)
.style("font-size", 12)
}
}
return svg.node();
}