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

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