Published
Edited
May 11, 2022
21 stars
Insert cell
# Dots and lines - Saneef H. Ansari
https://observablehq.com/@saneef/049-dots-and-lines

(Recreated for my own learning)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const iter = 12;
const gap = 4;

// CSUtils.random.setSeed(seed);
const xScale = d3.scaleLinear().domain([0,1]).range([0,width])
const yScale = d3.scaleLinear().domain([0,1]).range([0,height])
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const delaunay = d3.Delaunay.from(
pts,
d => xScale(d[0]),
d => yScale(d[1])
);

const voronoi = delaunay.voronoi([0, 0, width, height])


//////////////////////////////////////////////
/////////// vornoi relaxation ////////////////
//////////////////////////////////////////////

let cellCount = 0;
for (let k = 0; k < iter; k++) { //iterate relaxation process iter times to space point out
for (let i = 0; i < delaunay.points.length; i += 2) { //double iteration due to 1D array of coords
const cell = voronoi.cellPolygon(i >> 1); // (i >> 1) converts double iteration back to single iteration
if (cell === null) continue;

const x0 = delaunay.points[i];
const y0 = delaunay.points[i+1];

const [x1,y1] = d3.polygonCentroid(cell);

delaunay.points[i] = x0 + (x1 - x0) * 1; //move point towards voronoi cell centroid horiztonally
delaunay.points[i + 1] = y0 + (y1 - y0) * 1; //move point towards voronoi cell centroid vertically
}

voronoi.update();
}



//return array of final centroids
const centeredPts = [];
for (let p of voronoi.cellPolygons()) {
const centroid = d3.polygonCentroid(p);
centeredPts.push(centroid);
}


const fittingCircles = [];
for (let p of voronoi.cellPolygons()) { //for every polygon
const [cx, cy] = d3.polygonCentroid(p);
if (isNaN(cx) || isNaN(cy)) continue;
const edgeMidPoints = [];
for (let i = 0; i < p.length - 1; i++) { //for every point in polygon
const midpoint = midpointOfALine(...p[i], ...p[i+1]); // return middle point between each edge
edgeMidPoints.push(midpoint);
}



const r = edgeMidPoints.reduce((acc, pt) => {
const dist = distance(...pt, cx, cy); // distance between edgeMidPoint and voronoi polygon centroid
return dist < acc ? dist : acc; //return the minimum distance
}, width);

const freq = lerp(1/20000, 1/400, freqUV); //contrain the noise freq between this range
const angle = Math.PI * 2 * CSUtils.random.noise2D(cx, cy, freq); //pi * 2 = 360 deg in radians

fittingCircles.push([cx, cy, r, angle]);

}


svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", colorScheme.background);

if (debug) {
svg
.append("g")
.selectAll(".fitting-circle")
.data(fittingCircles)
.join("circle")
.attr("class", "fitting-circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", d => d[2])

svg
.append("g")
.selectAll(".voronoi")
.data(pts)
.join("path")
.attr("class", "voronoi")
.attr("d", (d,i) => voronoi.renderCell(i));

svg.append("g")
.attr("class", "original")
.selectAll("circle")
.data(pts)
.join("circle")
.attr("class","dot")
.attr("cx", d => xScale(d[0]))
.attr("cy", d => yScale(d[1]))
.attr("r", 3)

svg.append("g")
.attr("class", "centered")
.selectAll("circle")
.data(centeredPts)
.join("circle")
.attr("class", "dot")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 3)

}

svg.append("g")
.selectAll(".shape")
.data(fittingCircles)
.join("g")
.attr("class", d =>
Math.round(Math.random()) % 2 == 1? "shape shape-line" : "shape shape-circle"
);

svg.selectAll(".shape-circle")
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", d => d[2] - gap / 2)
.attr("fill", d => shuffle(colorScheme.colors).slice().pop());

svg.selectAll(".shape-line")
.append("line")
.each(function (d) {
const [cx, cy, r, angle] = d;
const length = 2 * r - strokeWidth - gap;
const [x1, y1] = rotatePoint(cx + length / 2, cy, angle, cx, cy);
const [x2, y2] = rotatePoint(cx - length / 2, cy, angle, cx, cy);

d3.select(this)
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2);
})
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", "round")
.attr("stroke", d => shuffle(colorScheme.colors).slice().pop())




return svg.node()
}


Insert cell
rotatePoint = (x, y, angle, xc = 0, yc = 0) => {
const xO = x - xc;
const yO = y - yc;

const xOR = xO * Math.cos(angle) - yO * Math.sin(angle);
const yOR = xO * Math.sin(angle) + yO * Math.cos(angle);

return [xOR + xc, yOR + yc];
}
Insert cell
strokeWidth = {
const cellSize = Math.min(width,height) / Math.sqrt(pts.length)

return cellSize / 2.5
}
Insert cell
Insert cell
Insert cell
height = 648
Insert cell
seed = 0.17615661197951038
Insert cell
pts = {
return d3.range(numPoints).map(() => [
Math.random(),
Math.random()
])
}
Insert cell
lerp = function (value1, value2, amount) {
amount = amount < 0 ? 0 : amount;
amount = amount > 1 ? 1 : amount;
return value1 + (value2 - value1) * amount;
}
Insert cell
midpointOfALine = (x1, y1, x2, y2) => [(x1 + x2) / 2, (y1 + y2) / 2]
Insert cell
distance = (x1, y1, x2, y2) =>
Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
Insert cell
colorScheme = {
randomize;
return getRandomPalette();
}
Insert cell
getRandomPalette = () => {
const minContrastRatio = 2;
const {name, colors: c, background = palette.bg} = chromotome.getRandom();

let colors = shuffle(c).slice();
colors = colors.filter(
(c) => CSUtils.color.contrastRatio(background,c) > minContrastRatio
);

if (colors.length === 0) {
return {
name: "Fallback",
colors: [palette.fg],
background
};
}

return {name, colors, background}
}
Insert cell
getRandomPalette()
Insert cell
chromotome.getRandom()
Insert cell
palette = ({
red: "#F87171",
blue: "#38BDF8",
lightGray: "#E2E8F0",
bg: "#F8FAFC",
fg: "#0F172A"
})
Insert cell
chromotome = import(
"https://unpkg.com/chromotome@1.19.1/dist/index.esm.js?module"
)
Insert cell
shuffle = function(array) {
let currentIndex = array.length, randomIndex;

// While there remain elements to shuffle.
while (currentIndex != 0) {

// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;

// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}

return array;
}
Insert cell
CSUtils.color.contrastRatio
Insert cell
CSUtils = require("https://bundle.run/canvas-sketch-util@1.10.0")
Insert cell
html`<style>
.voronoi {
fill: none;
stroke: ${palette.blue}
}

.dot {
stroke: white;
fill: ${palette.red};
stroke-width: 1px;
}

.centered .dot {
fill: ${palette.blue};
}

.fitting-circle {
fill: ${palette.lightGray}
}`
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