Published
Edited
May 12, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const chart = d3
.create('svg')
.attr('width', width)
.attr('height', height);

const color = d3
.scaleOrdinal()
.domain(["a", "b", "c"])
.range(['green', 'orange', 'red']);
// make a polygon from our path
const polygon = polygonSampledFromPath(maxspath, 1000)
// number of circles
const num = circles.length;
// get the area of the polgygon
// and divide it into the number of circles we want
// with some offset because ???
const area = (d3.polygonArea(polygon) / num) * 0.8;
// get the center of the polygon
// using this as a standin for our points just to plot something
const center = d3.polygonCentroid(polygon);

const elements = circles.map((d, i) => ({
// honestly these x,ys shouldn't matter
// because it looks like we use getMask and getXYs
// to actually generate the x and ys we want
x: center[0],
y: center[1],
r: d.count,
food: d.food
}));

// TRYING TO ADAPT SYLVIANS CODE:
const mask = getMask({ width, height, polygon });
// Rather than creating the simulate(n) function
// (since I didn't know what n was)
// I just make the number of points we need statically right here
// but this makes no sense to me...
// I _think_ this is just setup to supply points as an argument to getXYs aka the script???
/*
function simulate(n) {
if (locked === true) {
next = n;
return;
}
next = null;
locked = true;
const points = new Float64Array(n * 2);
for (let i = 0; i < n; ++i) {
points[i * 2] = elements[i].x;
points[i * 2 + 1] = elements[i].y;
}
worker.postMessage({ data: mask, width, height, points, center });
}
*/

const points = new Float64Array(num * 2);
for (let i = 0; i < num; ++i) {
points[i * 2] = elements[i].x;
points[i * 2 + 1] = elements[i].y;
}
// now instead of calling a worker or anything fancy I made a function called getXY
// that should be used instead of the fancy script thats a string with workers
// but this is not working at all....
// My mental model would be that we use the mask, w,h, and center to update the points
// that must be wrong??
getXYs(mask, width, height, points, center);
chart
.append('path')
.data([polygon])
.attr('stroke', 'black')
.attr('fill', 'none')
.attr('d', d => d3.line()(d));
// show the elements in their initial position
chart
.selectAll('g.element')
.data(elements, (d, i) => i)
.enter()
.append('g')
.attr('class', 'element')
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.style('stroke', 'none')
.attr('fill', d => color(d.food))

return chart.node()

}
Insert cell
function getMask({ width, height, polygon }) {
// build the black and white mask, used by the voronoi stippling algorithm
const canvas = document.createElement("canvas");
const dpi = 1; // TODO: manage other cases, to reduce the computation complexity?
canvas.width = width * dpi;
canvas.height = height * dpi;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
const context = canvas.getContext("2d");
context.scale(dpi, dpi);
context.beginPath();
context.moveTo(...polygon[polygon.length - 1]);
for (const p of polygon) {
context.lineTo(...p);
}
context.fillStyle = 'black';
context.fill();

// prepare data
const { data: rgba } = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
const mask = new Float64Array(canvas.width * canvas.height);
for (let i = 0, n = rgba.length / 4; i < n; ++i)
mask[i] = Math.max(0, rgba[i * 4 + 3] / 254);
mask.width = canvas.width;
mask.height = canvas.height;

return mask;
}
Insert cell
// this is my attempt at simplifying the script cell and failing
getXYs = (data, width, height, points, center) => {

const n = points.length
const c = new Float64Array(n * 2);
const s = new Float64Array(n);
const [cx, cy] = center;
const strength = 0.1

const delaunay = new d3.Delaunay(points);
const voronoi = delaunay.voronoi([0, 0, width, height]);

for (let k = 0; k < 80; ++k) {
// Compute the weighted centroid for each Voronoi cell.
c.fill(0);
s.fill(0);
for (let y = 0, i = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const w = data[y * width + x];
i = delaunay.find(x + 0.5, y + 0.5, i);
s[i] += w;
c[i * 2] += w * (x + 0.5);
c[i * 2 + 1] += w * (y + 0.5);
}
}

// Relax the diagram by moving points to the weighted centroid.
// Wiggle the points a little bit so they don’t get stuck.
const w = Math.pow(k + 1, -0.8) * 10;
for (let i = 0; i < n; ++i) {
const x0 = points[i * 2], y0 = points[i * 2 + 1];
const wp = data[Math.floor(y0) * width + Math.floor(x0)];
let x1, y1;
if (wp && s[i]) {
x1 = c[i * 2] / s[i];
y1 = c[i * 2 + 1] / s[i];
} else {
x1 = x0 + (cx - x0) * strength
y1 = y0 + (cy - y0) * strength;
}
points[i * 2] = x0 + (x1 - x0) * 1.8 + (Math.random() - 0.5) * w;
points[i * 2 + 1] = y0 + (y1 - y0) * 1.8 + (Math.random() - 0.5) * w;
}
}
return points
voronoi.update()
}
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