Published
Edited
Jun 7, 2022
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
{
replay2;

// for simplicity, ensure *n* is a multiple of the # of classes
const n = 1000 + classes - (1000 % classes);
const N = d3.randomNormal(0, 40);
const points = Float32Array.from(
{ length: 2 * n },
(d, i) => N(i) + (1 - (i % 2)) * 80 * ((i % (classes * 2)) - classes)
);
const context = DOM.context2d(width, height);
context.translate(width / 2, height / 2);

const r = 4;

const maxIter = 300;

for (let t = 0; t <= maxIter; ++t) {
context.fillStyle = "rgba(255,255,255,.1)";
context.fillRect(-width / 2, -height / 2, width, height);
if (t === maxIter)
context.clearRect(-width / 2, -height / 2, width, height);

for (let i = 0; i < n; i++) {
context.beginPath();
context.moveTo(points[2 * i], points[2 * i + 1]);
context.arc(points[2 * i], points[2 * i + 1], r, 0, tau);
context.fillStyle = color(i % classes);
context.fill();
}

yield context.canvas;
if (t === 0) await visibility().then(await Promises.delay(500));

discMultipleTransport(points, height / 2 - 10, {
classes: t < 100 ? classes : 1, // first do class SOT, then switch to common SOT
strength: 0.7
});

// discMultipleTransport(points, height / 2 - 10, {
// classes: t % 2 ? classes : 1, // alternate SOT and class SOT
// dirs: t % 2 ? 3 : 5,
// strength: t % 2 ? 0.1 : 1
// });
}
}
Insert cell
discMultipleTransport = {
let indices = [],
temp = [],
projection,
deltas;

// batched
return (
points,
radius,
{ dirs = 3, strength = 1, profile = chordAreaInverse, classes = 1 } = {}
) => {
const n = points.length / 2;
const r = new Array(classes);

if (n !== indices.length) {
indices = Uint32Array.from(d3.range(n));
temp = Uint32Array.from(d3.range(n));
projection = new Float32Array(n);
deltas = new Float32Array(2 * n);
}

const a = 2 * Math.PI * Math.random();
deltas.fill(0);
for (let d = 0; d < dirs; d++) {
const ap = a + ((Math.PI / 2) * d) / dirs,
sa = Math.sin(ap),
ca = Math.cos(ap);
for (const i of indices)
projection[i] = ca * points[2 * i] + sa * points[2 * i + 1];

temp.sort((i, j) => projection[i] - projection[j]);

// interleave by class
// Fil> here I'll be trying a new idea: organise the classes around a circle
// > and interleave according to this circle; this should be more stable
// > than using a fixed interleave order?
const order = d3.sort(
d3.range(classes),
(i) =>
Math.cos((2 * i * Math.PI) / classes) * ca +
Math.sin((2 * i * Math.PI) / classes) * sa
);
r.fill(0);

for (let j = 0; j < n + classes - 1; ++j) {
const c = order[temp[j] % classes];
const k = c + classes * r[c]++;
indices[k] = temp[j];
}

for (let k = 0; k < n; k++) {
const i = indices[k],
ideal = radius * profile(k / (n + 1)),
delta = ideal - projection[i];
deltas[2 * i] += ca * delta;
deltas[2 * i + 1] += sa * delta;
}
}
for (let i = 0; i < points.length; i++)
points[i] += (deltas[i] / dirs) * strength;
};
}
Insert cell
color = d3.scaleOrdinal(d3.schemeCategory10)
Insert cell
// https://observablehq.com/@fil/chord-area-inverse
chordAreaInverse = (x) => Math.pow(x, 0.6) - Math.pow(1 - x, 0.6)
Insert cell
height = 500
Insert cell
tau = 2 * Math.PI
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