Published
Edited
Sep 29, 2022
15 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawLines(pts, indices) {
const lines = indices.map(({ from, to }) => {
const p1 = pts[from[1]][from[0]];
const x1 = bounds.x + math.lerp(0, bounds.width, p1.uv[0]);
const y1 = bounds.y + math.lerp(0, bounds.height, p1.uv[1]);

const p2 = pts[to[1]][to[0]];
const x2 = bounds.x + math.lerp(0, bounds.width, p2.uv[0]);
const y2 = bounds.y + math.lerp(0, bounds.height, p2.uv[1]);

return svg`<line
x1=${x1}
y1=${y1}
x2=${x2}
y2=${y2}
></line>`;
});

return svg`<g stroke=${palette.line}>${lines}</g>`;
}
Insert cell
function drawCurves(pts, indices) {
const lines = indices.map(({ from, to }) => {
const hasVerticalBias =
Math.abs(from[0] - to[0]) >= Math.abs(from[1] - to[1]);
const points = hasVerticalBias
? [from, to].sort((a, b) => a[0] - b[0])
: [from, to].sort((a, b) => a[1] - b[1]);
const p1 = pts[points[0][1]][points[0][0]];
const x1 = bounds.x + math.lerp(0, bounds.width, p1.uv[0]);
const y1 = bounds.y + math.lerp(0, bounds.height, p1.uv[1]);

const p2 = pts[points[1][1]][points[1][0]];
const x2 = bounds.x + math.lerp(0, bounds.width, p2.uv[0]);
const y2 = bounds.y + math.lerp(0, bounds.height, p2.uv[1]);

const kx = hasVerticalBias ? Math.abs(x1 - x2) / 2 : 0;
const ky = hasVerticalBias ? 0 : Math.abs(y1 - y2) / 2;

const path = d3.path();
path.moveTo(x1, y1);
path.bezierCurveTo(x1 + kx, y1 + ky, x2 - kx, y2 - ky, x2, y2);

const r = 8;

return svg`<path
d=${path.toString()}
fill=none
stroke=${palette.curve}
></path>
<g fill=${palette.fgFill} stroke=${palette.curve} stroke-width="1">
<circle cx=${x1} cy=${y1} r=${r}></circle>
<circle cx=${x2} cy=${y2} r=${r}></circle>
</g>`;
});

return svg`<g>${lines}</g>`;
}
Insert cell
function drawPts(pts) {
const arr = pts.flat();

const dots = arr.map((pt) => {
const [u, v] = pt.uv;
const cx = bounds.x + math.lerp(0, bounds.width, u);
const cy = bounds.y + math.lerp(0, bounds.height, v);

return svg`<circle cx=${cx} cy=${cy} r=${1.5} fill=${
palette.dot
}></circle>`;
});

return svg`<g>${dots}</g>`;
}
Insert cell
pts = {
randomize;
return math
.linspace(squares.rows + 1, true)
.map((v, j) =>
math
.linspace(squares.cols + 1, true)
.map((u, i) => ({ uv: [u, v], indices: [i, j] }))
);
}
Insert cell
ptsWithPatterns = {
let ptsCopy = pts.slice();

for (let j = 0; j < ptsCopy.length; j++) {
for (let i = 0; i < ptsCopy[j].length; i++) {
if (ptsCopy[j][i].from || ptsCopy[j][i].to) continue;

const pattern = random.weightedSet(patterns);
const to = computeDest(ptsCopy, j, i, pattern);
if (to) {
const indices = to.indices.slice();
ptsCopy[j][i].pattern = pattern;
ptsCopy[j][i].to = indices;
ptsCopy[indices[1]][indices[0]].from = ptsCopy[j][i].indices.slice();
}
}
}

return ptsCopy.flat();
}
Insert cell
ptsWithPatterns.filter(
(el) => el.pattern === "horizontal" || el.pattern === "vertical"
)
Insert cell
lineIndices = ptsWithPatterns
.filter((el) => el.pattern === "horizontal" || el.pattern === "vertical")
.map((el) => ({
from: el.indices.slice(),
to: el.to.slice()
}))
Insert cell
curveIndices = ptsWithPatterns
.filter((el) => el.pattern === "curve")
.map((el) => ({
from: el.indices.slice(),
to: el.to.slice()
}))
Insert cell
function computeDest(pts, row, col, pattern) {
if (pattern === "curve") return computeCurveDest(pts, row, col);
if (pattern === "horizontal") return computeHorizontalDest(pts, row, col);
if (pattern === "vertical") return computeVerticalDest(pts, row, col);
}
Insert cell
function computeCurveDest(pts, row, col) {
const candidates = pts
.flat()
.filter(
(pt) =>
(pt.indices[0] < col - 1 || pt.indices[0] > col + 1) &&
(pt.indices[1] < row - 1 || pt.indices[1] > row + 1) &&
!pt.from &&
!pt.to
);

return random.pick(candidates);
}
Insert cell
function computeVerticalDest(pts, row, col) {
const column = pts.flat().filter((pt) => pt.indices[0] === col);
const hasVerticalLines = column.find((p) => p.pattern === "vertical");

// Already has horizontal line
if (hasVerticalLines) return;

return pickMatching(
column,
(el) => el.indices[1] !== row && !el.pattern && !el.from
);
}
Insert cell
function computeHorizontalDest(pts, row, col) {
const hasHorizontalLines = pts[row].find((p) => p.pattern === "horizontal");

// Already has horizontal line
if (hasHorizontalLines) return;

return pickMatching(
pts[row],
(el, index, array) => index !== col && !el.pattern && !el.from
);
}
Insert cell
function pickMatching(arr, cond) {
const options = arr.filter(cond);
return random.pick(options);
}
Insert cell
aspectRatio = 16 / 9
Insert cell
bounds = {
const { side, rows, cols } = squares;

const w = side * cols;
const h = side * rows;
const marginX = (width - w) / 2;
const marginY = (height - h) / 2;

return {
width: w,
height: h,
x: marginX,
y: marginY
};
}
Insert cell
baseMargins = {
const marginX = (marginFactor * width) / 5;
const marginY = marginX / aspectRatio;

return {
x: marginX,
y: marginY
};
}
Insert cell
squares = squaresInRectangle(
width - 2 * baseMargins.x,
height - 2 * baseMargins.y,
numOfSquares
)
Insert cell
palette = {
const bg = `hsla(0, 22%, 93%, 1.00)`;
const fill = `hsla(12, 75%, 54%, 0.80)`;
return {
bg,
fgFill: culori.formatRgb(culori.blend([bg, fill])),
curve: `hsla(212, 70%, 20%, 1.00)`,
dot: `hsla(12, 38%, 53%, 1.00)`,
line: `hsla(212, 70%, 20%, 0.250)`,
debug: `hsl(300, 100%, 50%)`
};
}
Insert cell
height = Math.floor(width / aspectRatio)
Insert cell
patterns = [
{ value: "horizontal", weight: 2 },
{ value: "vertical", weight: 2 },
{ value: "curve", weight: 4 },
{ value: "none", weight: 92 }
]
Insert cell
Insert cell
random = {
rnd.setSeed(randomize);
return rnd;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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