Public
Edited
Jan 20
Insert cell
Insert cell
data = {
const random = d3.randomNormal();
return Array.from({ length: width }, random);
}
Insert cell
Plot.plot({
y: { axis: null },
marks: [
beeswarmX(data, { x: (d) => d, y: () => 0, fill: "white", stroke: "black" })
],
height: 200
})
Insert cell
Insert cell
Insert cell
(replay,
Plot.plot({
height: 320,
marks: [
beeswarmX(data, {
x: (d) => d,
r: (d, i) => 1 + 2 * (i % 2),
fill: (d) => d,
gap: 1,
ticks: 10,
dynamic: true,
title: (d) => d
})
]
}))
Insert cell
Plot.plot({
x: { axis: null },
y: { axis: null },
marks: [
beeswarmY(data.slice(0, 100), {
y: (d) => d,
x: () => 0,
r: 1,
fill: (d) => d
}),
Plot.frame()
],
width: 50,
inset: 4,
marginRight: 1,
marginBottom: 1
})
Insert cell
function beeswarmX(data, options = {}) {
return beeswarm(data, { ...options, direction: "x" });
}
Insert cell
function beeswarmY(data, options = {}) {
return beeswarm(data, { ...options, direction: "y" });
}
Insert cell
function beeswarm(
data,
{ gap = 1, ticks = 50, dynamic, direction, xStrength = 0.8, yStrength = 0.05, ...options }
) {
const dots = Plot.dot(data, options);
const { render } = dots;

dots.render = function () {
const g = render.apply(this, arguments);
const circles = d3.select(g).selectAll("circle");

const nodes = [];
const [cx, cy, x, y, forceX, forceY] =
direction === "x"
? ["cx", "cy", "x", "y", d3.forceX, d3.forceY]
: ["cy", "cx", "y", "x", d3.forceY, d3.forceX];
for (const c of circles) {
const node = {
x: +c.getAttribute(cx),
y: +c.getAttribute(cy),
r: +c.getAttribute("r")
};
nodes.push(node);
}
const force = d3
.forceSimulation(nodes)
.force("x", forceX((d) => d[x]).strength(xStrength))
.force("y", forceY((d) => d[y]).strength(yStrength))
.force(
"collide",
d3
.forceCollide()
.radius((d) => d.r + gap)
.iterations(3)
)
.tick(ticks)
.stop();
update();
if (dynamic) force.on("tick", update).restart();
return g;

function update() {
circles.attr(cx, (_, i) => nodes[i].x).attr(cy, (_, i) => nodes[i].y);
}
};

return dots;
}
Insert cell
function beeswarmCanvas({
data,
canvasWidth = 800,
canvasHeight = 600,
x = (d) => d.x, // Accessor for x-position
y = () => canvasHeight / 2 + Math.random() * 10 - 5, // Start near the center with slight randomness
r = (d) => d.r || 3, // Radius accessor (default to 3)
fill = () => "steelblue",
gap = 1,
ticks = 50,
xStrength = 0.8,
yStrength = 0.05,
dynamic = false,
highres = false,
}) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const ratio = window.devicePixelRatio || 1;

if (highres) {
canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
context.scale(ratio, ratio);
} else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}

const nodes = data.map((d) => ({
x: x(d),
y: y(d),
r: r(d),
fill: fill(d),
}));

const simulation = d3
.forceSimulation(nodes)
.force("x", d3.forceX((d) => d.x).strength(xStrength))
.force("y", d3.forceY((d) => d.y).strength(yStrength))
.force("collide", d3.forceCollide().radius((d) => d.r + gap).iterations(3))
.tick(ticks)
.stop();

function render() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
for (const node of nodes) {
context.beginPath();
context.arc(node.x, node.y, node.r, 0, 2 * Math.PI);
context.fillStyle = node.fill;
context.fill();
}
}

render();

if (dynamic) {
simulation.on("tick", render)
setTimeout(() => simulation.restart(), 100);
}

return canvas;
}

Insert cell
function computeForce({
data,
canvasHeight = 600,
x = (d) => d.x, // Accessor for x-position
y = () => canvasHeight / 2 + Math.random() * 10 - 5, // Start near the center with slight randomness
r = (d) => d.r || 3, // Radius accessor (default to 3)
fill = () => "steelblue",
gap = 1,
ticks = 50,
xStrength = 0.8,
yStrength = 0.05,
}){
const nodes = data.map((d) => ({
x: x(d),
y: y(d),
r: r(d),
fill: fill(d),
}));

const promise = new Promise((resolve, reject) => {
const simulation = d3
.forceSimulation(nodes)
.force("x", d3.forceX((d) => d.x).strength(xStrength))
.force("y", d3.forceY((d) => d.y).strength(yStrength))
.force("collide", d3.forceCollide().radius((d) => d.r + gap).iterations(3));

simulation.on("end", resolve(nodes))
});



return promise;
}
Insert cell
computedChartData = {const canvasWidth = 800;
const marginLeft = 20;
const marginRight = 20;
const xScale = d3.scaleLinear(d3.extent(penguins, (d) => d.body_mass_g), [marginLeft, canvasWidth - marginRight]);
const cScale = d3.scaleOrdinal().domain([...new Set(penguins.map((d) => d.species))]).range(["red", "green", "blue"]);
return computeForce({
data: penguins,
x: (d) => xScale(d.body_mass_g),
r: () => 5,
fill: (d) => cScale(d.species),
xStrength: 0.5,
yStrength: 0.01,
canvasHeight: 600,
ticks: 50,
dynamic: true,
highres: true,
})
}
Insert cell
function beeswarmCanvasDirect({
data,
canvasWidth = 800,
canvasHeight = 600,
x = (d) => d.x, // Accessor for x-position
y = () => canvasHeight / 2 + Math.random() * 10 - 5, // Start near the center with slight randomness
r = (d) => d.r || 3, // Radius accessor (default to 3)
fill = () => "steelblue",
gap = 1,
ticks = 50,
xStrength = 0.8,
yStrength = 0.05,
dynamic = false,
highres = false,
}) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const ratio = window.devicePixelRatio || 1;

if (highres) {
canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
context.scale(ratio, ratio);
} else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}

function render() {
context.clearRect(0, 0, canvasWidth, canvasHeight);
for (const node of data) {
context.beginPath();
context.arc(node.x, node.y, node.highlighted ? 10 : node.r, 0, 2 * Math.PI);
context.fillStyle = node.fill;
context.globalAlpha = node.highlighted ? 1 : 0.7; // Change opacity if highlighted
context.fill();
context.globalAlpha = 1; // Reset opacity
}
}

render();

return canvas;
}

Insert cell
{
const canvasWidth = 800;
const marginLeft = 20;
const marginRight = 20;
const xScale = d3.scaleLinear(d3.extent(penguins, (d) => d.body_mass_g), [marginLeft, canvasWidth - marginRight]);
const cScale = d3.scaleOrdinal().domain([...new Set(penguins.map((d) => d.species))]).range(["red", "green", "blue"]);

let canvas;
yield (canvas = beeswarmCanvas({
data: penguins,
x: (d) => xScale(d.body_mass_g),
r: () => 5,
fill: (d) => cScale(d.species),
xStrength: 0.5,
yStrength: 0.01,
canvasWidth,
canvasHeight: 600,
ticks: 10,
dynamic: true,
highres: true,
}));
}

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