Published
Edited
Jul 21, 2022
3 forks
Importers
65 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const sz = 600;
const margin = 40;
const circles = mesh.circles;
const svg = d3
.create("svg")
.attr("width", sz + margin * 2)
.attr("height", sz + margin * 2)
.attr("viewBox", [-margin, -margin, sz + margin * 2, sz + margin * 2]),
link = showTriangulation
? svg
.selectAll(".link")
.data(graph.links)
.join("line")
.classed("link", true)
.classed("border", (d) => d.etype[0] === "border")
.attr("stroke", (d) =>
d.etype[0] === "border"
? "black"
: d.etype[0] === "rigid"
? "blue"
: "red"
)
: null,
node = showTriangulation
? svg
.selectAll(".node")
.data(graph.nodes)
.join("circle")
.attr("r", 2)
.classed("node", true)
.classed("fixed", (d) => d.fx !== undefined)
: null,
polygons = showTriangulation
? null
: svg
.selectAll(".poly")
.data(circles.slice(1))
.join("polygon")
.classed("poly", true);

yield svg.node();

function tick() {
if (showTriangulation) {
link
.attr("x1", (d) => d.source.x * sz)
.attr("y1", (d) => d.source.y * sz)
.attr("x2", (d) => d.target.x * sz)
.attr("y2", (d) => d.target.y * sz);
node.attr("cx", (d) => d.x * sz).attr("cy", (d) => d.y * sz);
} else {
polygons.attr("points", (d) => {
let { ivtx, nvtx } = d;
let pts = [];
for (let i = ivtx; i < ivtx + nvtx; i++) {
pts.push([graph.nodes[i].x * sz, graph.nodes[i].y * sz]);
}
return `${pts.flat()}`;
});
}
}

tick();
await Promises.delay(1000);

const simulation = d3
.forceSimulation()
.nodes(graph.nodes)
.force(
"link",
d3
.forceLink(graph.links)
.distance((d) => d.dist)
.strength((d) => (d.dist == 0 ? 1 : 0.03))
)
.force(
"triangle",
forceTriangle(graph.triplets).area((t, a0, a) => a0 * t.areaRatio)
)
.on("tick", tick);
}
Insert cell
Insert cell
data = {
let data = [];
const n = nCircles;
for (let i = 0; i < n; i++) {
data.push([`name ${i}`, Math.random() * 100 + 1]);
}
return data;
}
Insert cell
Insert cell
hier = d3.hierarchy({name:"root", children: data.map(([name,value]) =>({name,value}))}).sum(d=>d.value)
Insert cell
Insert cell
packed = d3.pack().padding(0.02)(hier)
Insert cell
{
const sz = 500;
const chart = d3.select(svg`<svg width=${sz} height=${sz} viewbox="-1,-1,${sz+2},${sz+2}">`);
chart.selectAll("circle")
.data([packed,...packed.children])
.enter()
.append("circle")
.attr("cx", d=>d.x*sz)
.attr("cy", d=>d.y*sz)
.attr("r", d=>d.r*sz)
.attr("stroke", "black")
.attr("fill", d=>d.parent?"gray":"none")
return chart.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
{
const sz = 500;
const chart = d3.select(svg`<svg width=${sz} height=${sz} viewbox="-1,-1,${sz+2},${sz+2}">`);
const {vertices,constraints} = mesh;
chart.selectAll("line")
.data(constraints)
.enter()
.append("line")
.attr("x1", d=>vertices[d[0]][0]*sz)
.attr("y1", d=>vertices[d[0]][1]*sz)
.attr("x2", d=>vertices[d[1]][0]*sz)
.attr("y2", d=>vertices[d[1]][1]*sz)
.attr("stroke", "black")
chart.selectAll("circle")
.data(vertices)
.enter()
.append("circle")
.attr("cx", d=>d[0]*sz)
.attr("cy", d=>d[1]*sz)
.attr("r", 2)
.attr("fill","black")
return chart.node()
}
Insert cell
Insert cell
Insert cell
Constrainautor = require("@kninnug/constrainautor")
Insert cell
constrained = {
// Creates a constrained triangulation, and populates the mesh with fields 'edges' and 'etype'
const { vertices, vtype, constraints, circles } = mesh;

const del = d3.Delaunay.from(vertices);
const con = new Constrainautor(del._delaunator);
const conEdge = [];
for (const [v1, v2] of constraints) {
con.constrainOne(v1, v2);
conEdge[v1] = v2;
}
con.delaunify();

const { points, halfedges, triangles } = del;

let edges = [];
let etype = [];
for (let i = 0, n = halfedges.length; i < n; ++i) {
let j = halfedges[i];
if (j < i) continue;
const vi = triangles[i];
const vj = triangles[j];
edges.push([vi, vj]);
if (vtype[vi][0] === "border" && vtype[vj][0] === "border") {
if (vtype[vi][1] == vtype[vj][1]) {
etype.push([
conEdge[vi] == vj || conEdge[vj] == vi ? "border" : "rigid",
vtype[vi][1]
]);
} else etype.push(["shrink", 0]);
} else {
let ivertex = vtype[vi][0] == "internal" ? vtype[vi][1] : vtype[vj][1];
if (ivertex == 0) etype.push(["shrink", 0]);
else etype.push(["rigid", ivertex]);
}
}
Object.assign(mesh, { edges, etype, triangles, circles });
return del;
}
Insert cell
{
// Draws the constrained delaunay triangulation
constrained;
const sz = 600;
const context = DOM.context2d(sz,sz)
const {edges, etype, vertices} = mesh;
for (let iedge = 0; iedge<edges.length; iedge++) {
const [i,j] = edges[iedge];
const [x1,y1] = vertices[i];
const [x2,y2] = vertices[j];
context.beginPath();
if (etype[iedge][0] == "shrink") context.strokeStyle = "red";
else if (etype[iedge][0] == "border") context.strokeStyle = "black";
else context.strokeStyle = "blue";
context.moveTo(x1*sz, y1*sz);
context.lineTo(x2*sz, y2*sz);
context.stroke()
}
return context.canvas
}

Insert cell
Insert cell
Insert cell
Insert cell
// The triangle force
function forceTriangle(triangles = []) {
let area = (t, a0, aCurrent) => a0; // Default function that maps triplet to triangle area
let strength = (t) => 1; // Default function that maps triplet to force strength
let iterations = 1; // Default number of iterations
let nodes; // Array of nodes
let areas; // Array of initial triangle areas
const makeTriangle = (
triplet // Creates a Triangle object from a node triplet
) =>
new Triangle(
...triplet.map((node) => [node.x + node.vx, node.y + node.vy])
);

function force(alpha) {
for (let k = 0; k < iterations; ++k) {
for (let itriangle = 0; itriangle < triangles.length; itriangle++) {
let triplet = triangles[itriangle];
let a0 = areas[itriangle];
let t = makeTriangle(triplet);
let q = t.p.map(([x, y]) => [x, y]); // Clone vertices
let newArea = area(triplet, a0, t.area());
t.setArea(newArea);
let s = strength(triplet);
for (let i = 0; i < 3; i++) {
let node = triplet[i];
node.vx += (t.p[i][0] - q[i][0]) * alpha * s;
node.vy += (t.p[i][1] - q[i][1]) * alpha * s;
}
}
}
}

function initialize(_nodes, _random) {
nodes = _nodes;
areas = [];
for (let triplet of triangles) {
areas.push(makeTriangle(triplet).area());
}
}

force.initialize = initialize;

force.triangles = function (_) {
return arguments.length ? ((triangles = +_), force) : triangles;
};

force.iterations = function (_) {
return arguments.length ? ((iterations = +_), force) : iterations;
};

force.strength = function (_) {
return arguments.length
? ((strength = typeof _ === "function" ? _ : () => _), force)
: strength;
};

force.area = function (_) {
return arguments.length
? ((area = typeof _ === "function" ? _ : () => _), force)
: area;
};

return force;
}
Insert cell
Insert cell
Insert cell
Insert cell
mutable debug = 0
Insert cell
class Triangle {
constructor(a, b, c) {
this.p = [a, b, c];
}
area() {
let total = 0;
let [px, py] = this.p[2];
for (let [x, y] of this.p) {
total += (px - x) * (y + py);
[px, py] = [x, y];
}
return total / 2;
}
sideLength(i) {
const j = (i + 1) % 3;
return Math.hypot(this.p[i][0] - this.p[j][0], this.p[i][1] - this.p[j][1]);
}
sideHeight(i) {
const j = (i + 1) % 3,
k = (i + 2) % 3;
const u = [this.p[j][0] - this.p[i][0], this.p[j][1] - this.p[i][1]];
const v = [this.p[k][0] - this.p[j][0], this.p[k][1] - this.p[j][1]];
const ulen = Math.hypot(...u) || 0.0001;
const uhat = [u[0] / ulen, u[1] / ulen];
const uhatDotV = uhat[0] * v[0] + uhat[1] * v[1];
return [v[0] - uhatDotV * uhat[0], v[1] - uhatDotV * uhat[1]];
}
barycenter() {
const sum = this.p.reduce((a, b) => [a[0] + b[0], a[1] + b[1]]);
return [sum[0] / 3, sum[1] / 3];
}
setArea(a) {
let factor = a / this.area();
if (Math.abs(factor) < 1) return this.setArea1(factor);
return this.setArea2(factor);
}
setArea1(factor) {
// Use scaling towards barycenter
//let formerArea = this.area();
let f = Math.sqrt(Math.abs(factor));
let bary = this.barycenter();
let v = [];
this.p.forEach((q) => {
let u = [(q[0] - bary[0]) * f, (q[1] - bary[1]) * f];
v.push(u);
q[0] = bary[0] + u[0];
q[1] = bary[1] + u[1];
});
if (factor < 0) {
// Reflect wrt barycenter using the smallest height as an axis
let { i, h, hgt } = [0, 1, 2]
.map((i) => {
let hgt = this.sideHeight(i);
let h = Math.hypot(...hgt);
return { i, h, hgt };
})
.reduce((a, b) => (a.h < b.h ? a : b));
let j = (i + 1) % 3;
let k = (i + 2) % 3;
let n = [hgt[0] / h, hgt[1] / h];
let a = v[k][0] * n[0] + v[k][1] * n[1];
let b = h - a;
this.p[i] = [this.p[i][0] + n[0] * b * 2, this.p[i][1] + n[1] * b * 2];
this.p[j] = [this.p[j][0] + n[0] * b * 2, this.p[j][1] + n[1] * b * 2];
this.p[k] = [this.p[k][0] - n[0] * a * 2, this.p[k][1] - n[1] * a * 2];
// mutable debug = { hgt, ratio: this.area() / formerArea };
}
}
setArea2(factor) {
const h = [0, 1, 2].map((i) => this.sideHeight(i));
const hlen = h.map(([x, y]) => Math.hypot(x, y));
let i =
Math.abs(factor) < 1
? // Shrink
hlen[0] > hlen[1]
? hlen[0] > hlen[2]
? 0
: 2
: hlen[1] > hlen[2]
? 1
: 2
: // Expand
hlen[0] < hlen[1]
? hlen[0] < hlen[2]
? 0
: 2
: hlen[1] < hlen[2]
? 1
: 2;
let u = h[i],
v = [0, 0];
if (factor < 0) {
factor = -factor;
v = [...u];
u = [-u[0], -u[1]];
}
u = [v[0] + (-u[0] * (factor - 1)) / 3, v[1] + (-u[1] * (factor - 1)) / 3];
const j = (i + 1) % 3,
k = (i + 2) % 3;
this.p[i][0] += u[0];
this.p[i][1] += u[1];
this.p[j][0] += u[0];
this.p[j][1] += u[1];
this.p[k][0] -= u[0] * 2;
this.p[k][1] -= u[1] * 2;
}
}
Insert cell
testTriangle = {
let t = new Triangle([0, 0], [1, 1], [1, 0]);
let a = t.area();
let result = {
original: t.p.map((d) => [...d]),
originalArea: a,
originalBary: t.barycenter()
};
t.setArea(a * 2);
result.after = t.p;
result.afterArea = t.area();
result.afterBary = t.barycenter();
return result;
}
Insert cell
html`<style>

.link {
stroke-width: 1px;
}
.link.border {
stroke-width: 3px;
}
.poly {
stroke-width:2px;
stroke: black;
fill:rgba(0,0,0,0.1);
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1px;
}

.node.fixed {
fill: #f00;
}

</style>`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more