Public
Edited
Feb 28
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
var width = 640;
var height = 300;
var svg = d3.select(DOM.svg(width, height))
.style("overflow", "visible");
var input = PoincareCircleOutput(svg, 150, 130, 120, 10);
var mixed_input = PoincareCircleOutput(svg, 450, 130, 120, 10);
var dual_input = PoincareCircleOutput(svg, 750, 130, 120, 10);

var tesselation = HyperbolicTesselation([na,nb,nc], nlevels);
var vertices = [];
var faces = [];

tesselation.faces.forEach(function(f) {
input.AddSegment(f.v[0].pos, f.v[1].pos);
input.AddSegment(f.v[1].pos, f.v[2].pos);
input.AddSegment(f.v[2].pos, f.v[0].pos);
mixed_input.AddSegment(f.v[0].pos, f.v[1].pos);
mixed_input.AddSegment(f.v[1].pos, f.v[2].pos);
mixed_input.AddSegment(f.v[2].pos, f.v[0].pos);
});
tesselation.vertices.forEach(function(v) {
var nfaces = 2 * tesselation.k[v.i];
for (var i = 0; i < nfaces; ++i) {
if (v.f[i] != null & v.f[(i+1)%nfaces] != null) {
dual_input.AddSegment(v.f[i].center, v.f[(i+1)%nfaces].center);
mixed_input.AddSegment(v.f[i].center, v.f[(i+1)%nfaces].center, "red");
}
}
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
function Draw3d(svg, x, y, scale, theta) {
// data
var segments = [];
var points = [];
// auxiliary data
var zrange = [0, 0];
// visual elements
var shape = svg.append("g");
var angle = theta;
// Maps a 3d point to its position in the svg given by the
// first two coordinates of the transformation. We keep the
// third coordinate to give us a notion of depth
function Project(p) {
return geom.Translate(
geom.Scale(
geom.Project(
geom.Translate(
geom.Rotateyz(p, angle), [0,0,-4])
), scale
), [x,y,0]
);
}
function EditSegments(edit) {
segments.forEach(edit);
}
function UpdateZRange() {
zrange = geom.DepthRange(segments);
}
function UpdateTheta(theta) {
angle = theta;
}
// Given two points in shepherical coordinates, we add a point
function AddSegment(p1, p2, color = [1,1,1], stroke = 1, update = null) {
segments.push({
p1: Project(p1),
p2: Project(p2),
p1o : p1,
p2o : p2,
color: color,
stroke: stroke,
update: update,
});
}
function AddPoint(p1, color = [1,1,1], update = null) {
points.push({
p1: Project(p1),
p1o : p1,
color: color,
update: update,
});
}
function Update() {
segments.forEach(function(s) {
if (s.update) {
[s.p1o, s.p2o] = s.update();
}
s.p1 = Project(s.p1o);
s.p2 = Project(s.p2o);
});
points.forEach(function(s) {
if (s.update) {
s.p1o = s.update();
}
s.p1 = Project(s.p1o);
});
shape.selectAll("circle").data(points).enter().append("circle");
shape.selectAll("circle")
.attr("r", 3.5)
.attr("cx", d => d.p1[0])
.attr("cy", d => d.p1[1])
.attr("fill", d => geom.DepthColor(zrange,d.color)(d.p1));
shape.selectAll("circle").exit().remove();
segments.sort(function(s1,s2) {return s1.p1[2] + 4 * s1.stroke -
s2.p1[2] - 4 * s2.stroke});
shape.selectAll("line").data(segments).enter().append("line");
shape.selectAll("line")
.attr("stroke", d => geom.DepthColor(zrange,d.color)(d.p1))
.attr("stroke-width", d => d.stroke)
.attr("x1", d => d.p1[0])
.attr("y1", d => d.p1[1])
.attr("x2", d => d.p2[0])
.attr("y2", d => d.p2[1]);
shape.selectAll("line").exit().remove();
}
return {
UpdateZRange: UpdateZRange,
UpdateTheta: UpdateTheta,
AddSegment: AddSegment,
AddPoint: AddPoint,
Update: Update,
EditSegments: EditSegments,
}
}
Insert cell
function DrawHyperboloid(svg, x, y, scale, theta, k) {
var grid = geom.Grid(k,k);

var segs = geom.ApplyToSegmentSet(p => {
return geom.HyperbolicalCoordinate(
geom.ScaleCoordinates(p, [2 * Math.PI, 1.5]));}, grid);
var draw = Draw3d(svg, x, y, scale, theta);
segs.forEach(function(s) { draw.AddSegment(s.p1, s.p2) });
draw.UpdateZRange();
draw.Update();
function AddSegment(update, color, stroke) {
var p1, p2;
[p1, p2] = update();
draw.AddSegment(p1, p2, color, stroke, update);
}
function AddFixedSegment(p1, p2, color, stroke) {
draw.AddSegment(p1, p2, color, stroke);
}
function AddPoint(update, color) {
var p1 = update();
draw.AddPoint(p1, color, update);
}
return {
Update: draw.Update,
AddSegment: AddSegment,
AddFixedSegment: AddFixedSegment,
AddPoint: AddPoint,
}
}
Insert cell
function ProjectionInput(svg, x, y, scale, locked = 200) {
var locked = locked;
var shape = svg.append("g");
shape.append("circle")
.attr("r", scale)
.attr("cx", x)
.attr("cy", y)
.attr("stroke","red")
.attr("stroke-width", 1.5)
.attr("fill", "none");
var points = [];
function AddPoint(p, callback) {
var p = p;
var dragging = false;
var point = shape.append("circle")
.attr("r", 3.5)
.attr("cx", x + scale * p[0])
.attr("cy", y + scale * p[1])
.call(d3.drag()
.on("drag", dragged)
.on("start", dragstart)
.on("end", dragend))
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);
points.push(point);

function ProjectToBox(a, [min,max]) {
return Math.max(Math.min(a, max), min);
}

function dragged() {
var pos = [ProjectToBox(d3.event.x, [x - 2 * scale, x + 2 * scale]),
ProjectToBox(d3.event.y, [y - 1.5 * scale, y + 1.5 * scale])];
var dp = [pos[0] - x, pos[1] - y];
pos = geom.Translate([x,y], geom.Scale(dp, Math.min(1, locked * scale / geom.Norm(dp))));

point.attr("cx", pos[0])
.attr("cy", pos[1]);

var px = (pos[0] - x) / scale;
var py = (pos[1] - y) / scale;
p = [px, py];
callback([px,py]);
}
function dragstart() {
dragging = true;
point.attr("r", 5);
}
function dragend() {
dragging = false;
point.attr("r", 3.5);
}
function handleMouseOver() {
if (!dragging) {
point.attr("r", 5);
}
}
function handleMouseOut() {
if (!dragging) {
point.attr("r", 3.5);
}
}
function Coordinates() {
return p;
}
function SetPosition(z) {
p = z;
point.attr("cx", x + scale * z[0])
.attr("cy", y + scale * z[1]);
}
return {
Coordinates: Coordinates,
SetPosition: SetPosition,
};
}
var segments = [];
function AddSegment(update, color, stroke = 2) {
segments.push({
p1: [0,0],
p2: [0,0],
color: color,
stroke: stroke,
update: update,
});
}
function Update() {
segments.forEach(function(s) {
[s.p1, s.p2] = s.update();
});
shape.selectAll("line").data(segments).enter().append("line");
shape.selectAll("line")
.attr("stroke", d => d.color)
.attr("stroke-width", d => d.stroke)
.attr("x1", d => x + scale * d.p1[0])
.attr("y1", d => y + scale * d.p1[1])
.attr("x2", d => x + scale * d.p2[0])
.attr("y2", d => y + scale * d.p2[1]);
shape.selectAll("line").exit().remove();
points.forEach(function(p) { p.raise(); });
}
return {
AddPoint: AddPoint,
AddSegment: AddSegment,
Update: Update,
SetLocked : function(locked_) { locked = locked_; },
}
}
Insert cell
function PoincareCircle(svg, x, y, scale, k=20) {
var circle = ProjectionInput(svg, x, y, scale, 1.0);
var lines = [];
var segments = [];
// var centers = [];
function CirclePoint(c,u,v,theta) {
return geom.Translate(c,
geom.Translate(
geom.Scale(u, Math.cos(theta)),
geom.Scale(v, Math.sin(theta))
));
}
function LinePoint(u,v,t) {
return geom.Translate(
geom.Scale(u, t),
geom.Scale(v, 1-t)
);
}
function PoincareLine(l,i,j) {
return function() {
var c = l.data.c;
if (c != Infinity) {
var u = geom.Scale(l.data.pn, l.data.r);
var v = geom.Scale(l.data.qn, l.data.r);
return [ CirclePoint(c,u,v,l.data.theta * i),
CirclePoint(c,u,v,l.data.theta * j) ];
} else {
var p = l.data.cp;
var q = l.data.cq;
return [ LinePoint(p,q,i), LinePoint(p,q,j) ];
}
}
}
function PoincareSegment(l,i,j) {
return function() {
var c = l.data.c;
if (c != Infinity) {
var u = geom.Scale(l.data.pn, l.data.r);
var v = geom.Scale(l.data.qn, l.data.r);
return [ CirclePoint(c,u,v,
l.data.thetap * i + l.data.thetaq * (1-i)),
CirclePoint(c,u,v,
l.data.thetap * j + l.data.thetaq * (1-j)) ];
} else {
var p = l.data.p;
var q = l.data.q;
return [ LinePoint(p,q,i), LinePoint(p,q,j) ];
}
}
}
function AddLine(p, q) {
var l = {p:p, q:q, data:geom.PoincareLine(p.Coordinates(), q.Coordinates())};
lines.push(l);
for (var i = 0; i < k; ++i) {
circle.AddSegment(PoincareLine(l, i/k, (i+1)/k), "blue", 2);
}
}
function AddLineDir(p, d) {
var l = {p:p, d:d, data:geom.PoincareLineDir(p.Coordinates(), d)};
lines.push(l);
for (var i = 0; i < k; ++i) {
circle.AddSegment(PoincareLine(l, i/k, (i+1)/k), "blue", 2);
}
}
function AddSegment(p, q, color = "blue") {
var l = {p:p, q:q, data:geom.PoincareLine(p.Coordinates(), q.Coordinates())};
segments.push(l);
for (var i = 0; i < k; ++i) {
circle.AddSegment(PoincareSegment(l, i/k, (i+1)/k), color, 2);
}
}
function AddSegmentDir(p, d, dist, color="blue") {
var l = {p:p, d:d, dist:dist, data:geom.PoincareSegmentDir(p.Coordinates(), d, dist)};
segments.push(l);
for (var i = 0; i < k; ++i) {
circle.AddSegment(PoincareSegment(l, i/k, (i+1)/k), color, 2);
}
}
function WrapVector(v) {
return {
Coordinates: function() { return v; },
};
}
function AddFixedSegment(p,q, color="blue") {
AddSegment(WrapVector(p), WrapVector(q), color);
}
function AddEuclidianSegment(p,q) {
circle.AddSegment(function() { return [p,q]; }, "red", 1);
}
function Update() {
lines.forEach(function(l) {
if (l.d != undefined) {
l.data = geom.PoincareLineDir(l.p.Coordinates(), l.d);
} else {
l.data = geom.PoincareLine(l.p.Coordinates(), l.q.Coordinates());
}
});
segments.forEach(function(l) {
if (l.d != undefined) {
l.data = geom.PoincareSegmentDir(l.p.Coordinates(), l.d, l.dist)
} else {
l.data = geom.PoincareLine(l.p.Coordinates(), l.q.Coordinates());
}
});
circle.Update();
}
return {
AddPoint: circle.AddPoint,
AddLine: AddLine,
AddLineDir: AddLineDir,
AddSegment: AddSegment,
AddFixedSegment: AddFixedSegment,
AddSegmentDir: AddSegmentDir,
AddEuclidianSegment: AddEuclidianSegment,
Update: Update,
SetLocked : function(locked_) { circle.SetLocked(locked_); },
};
}

Insert cell
function PoincareCircleOutput(svg, x, y, scale, k=20) {
var circle = ProjectionInput(svg, x, y, scale, 1.0);
var lines = [];
var segments = [];
var shape = svg.append("g");
shape.append("circle")
.attr("r", scale)
.attr("cx", x)
.attr("cy", y)
.attr("stroke","red")
.attr("stroke-width", 1.5)
.attr("fill", "none");
function NewSegment(p1, p2, color, stroke) {
shape.append("line")
.attr("stroke", color)
.attr("stroke-width", stroke)
.attr("x1", x + scale * p1[0])
.attr("y1", y + scale * p1[1])
.attr("x2", x + scale * p2[0])
.attr("y2", y + scale * p2[1]);
}
function PoincareSegmentPoint(l,t) {
if (l.c != Infinity) {
var a = l.thetap * (1-t) + l.thetaq * t;
return geom.Translate(l.c,
geom.Scale(geom.Translate(geom.Scale(l.pn, Math.cos(a)),
geom.Scale(l.qn, Math.sin(a))),
l.r));
} else {
return geom.Translate(geom.Scale(l.p, 1-t),
geom.Scale(l.q, t));
}
}
function PoincareLinePoint(l,t) {
if (l.c != Infinity) {
var a = -l.theta * (1-t) + l.theta * t;
return geom.Translate(l.c,
geom.Scale(geom.Translate(geom.Scale(l.pn, Math.cos(a)),
geom.Scale(l.qn, Math.sin(a))),
l.r));
} else {
return geom.Translate(geom.Scale(l.pn, 1-t),
geom.Scale(l.qn, t));
}
}

function AddLine(l, color="blue", stroke = 2) {
if (l.c != Infinity) {
var k = Math.ceil((2 * l.theta * l.r) / 0.1);
if (k == 0) console.log("k=0");
for (var i = 0; i < k; ++i) {
NewSegment(PoincareLinePoint(l, i/k),
PoincareLinePoint(l, (i+1)/k),
color, stroke);
}
} else {
NewSegment(PoincareLinePoint(l, 0),
PoincareLinePoint(l, 1),
color, stroke);
}
}
function AddSegmentLine(l, color="blue", stroke = 2 ) {
if (l.c != Infinity) {
var k = Math.ceil((Math.abs(l.thetaq - l.thetap) * l.r) / 0.1);
for (var i = 0; i < k; ++i) {
NewSegment(PoincareSegmentPoint(l, i/k),
PoincareSegmentPoint(l, (i+1)/k),
color, stroke);
}
} else {
NewSegment(PoincareSegmentPoint(l, 0),
PoincareSegmentPoint(l, 1),
color, stroke);
}
}
function AddSegment(p,q, color="blue", stroke = 2) {
var l = geom.PoincareLine(p, q);
AddSegmentLine(l, color, stroke);
}
function AddPoint(p) {
var point = shape.append("circle")
.attr("r", 3.5)
.attr("cx", x + scale * p[0])
.attr("cy", y + scale * p[1]);
}
return {
AddPoint: AddPoint,
AddLine: AddLine,
AddSegment: AddSegment,
AddSegmentLine: AddSegmentLine,
};
}
Insert cell
function HyperbolicTesselation(k, n_levels) {
// We are expecting parameters such that 1/a + 1/b + 1/c = 1;
var a = k[0], b = k[1], c = k[2];
var k = k;
var vertices = [];
var faces = [];
function GetTriangleLengths() {
var gamma = Math.PI / c;
var v = [Math.cos(gamma), Math.sin(gamma)];
var angle = -Math.PI * (1- (1/b) - (1/c));
var d = [Math.cos(angle), Math.sin(angle)];
function pred(t) {
var line = geom.PoincareLineDir(geom.Scale(v,t), d);
if (line.c[1] - line.r > 0) return 1;
return - Math.cos(Math.PI / a) + line.c[1] / line.r;
}
var t = geom.BinarySearch(pred, 0.000001, 0, 1);
var p = geom.Scale(v,t);
var l = geom.PoincareLineDir(p, d);
var q = [l.c[0] - Math.sqrt((l.r**2) - (l.c[1]**2)),0];
var z = [0,0];
return [geom.PoincareDistance(z,p),
geom.PoincareDistance(q,z),
geom.PoincareDistance(p,q)];
}
var lenghts = GetTriangleLengths();
function NewVertex(index, i, pos, base_dir) {
return {
index: index,
base_dir: base_dir,
i : i,
pos: pos,
f: new Array(2 * k[i]).fill(null),
v: new Array(2 * k[i]).fill(null),
level: 0,
};
}
function NewFace(a,b,c) {
return {
v: [a,b,c],
prev: null,
};
}
// We add the first face with its vertices to the
// tesselation and start expanding from there.
function InitializeTesselation() {
var base_dir = [1,0];
var pos = [0,0];
for (var i = 0; i < 3; ++i) {
var p = NewVertex(vertices.length, i, pos, base_dir);
var lp = geom.PoincareSegmentDir(pos, base_dir, lenghts[(6-(2*i+1))%3]);
pos = [lp.q[0], lp.q[1]];
base_dir = geom.Rotate(lp.tq, Math.PI * (1- (1/k[(i+1)%3])));
vertices.push(p);
}
var f = NewFace(vertices[0], vertices[1], vertices[2]);
for (var i = 0; i < 3; ++i) {
vertices[i].f[0] = f;
vertices[i].v[0] = vertices[(i+1)%3];
vertices[i].v[1] = vertices[(i+2)%3];
}
vertices[1].level = 1;
vertices[2].level = 1;
faces.push(f);
}
InitializeTesselation();
function FindIndex(w,v) {
var wj = 0;
for (wj = 0; wj < 2*k[w.i]; ++wj) {
if (w.v[wj] == v) break;
}
return wj;
}
function ProcessVertex(v, max_faces = Infinity ) {
var nfaces = 2 * k[v.i];
for (var j = 0; j < Math.min(nfaces, max_faces); ++j) {
if (v.f[j] == null && v.f[(j+1)%nfaces] == null) {
var w = v.v[j];
var wj = FindIndex(w,v);
wj = (2*k[w.i] + wj - 1) % (2*k[w.i]);

var u_i = 3 - v.i - w.i;
var l = geom.PoincareSegmentDir(v.pos,
geom.Rotate(v.base_dir, Math.PI * (j+1) / k[v.i] ),
lenghts[w.i]);
var u_base_dir = geom.Scale(l.tq, -1);
var u_pos = [l.q[0], l.q[1]];
var u = NewVertex(vertices.length, u_i, u_pos, u_base_dir);
u.level = v.level + 1;
u.v[0] = v;
u.v[1] = w;
v.v[(j+1)%nfaces] = u;
w.v[wj] = u;
var f = NewFace(v, w, u);
f.prev = v.f[j-1];
u.f[0] = f;
v.f[j] = f;
w.f[wj] = f;
faces.push(f);
vertices.push(u);
} else if (v.f[j] == null && v.f[(j+1)%nfaces] != null) {
var w = v.v[j];
var u = v.v[(j+1)% nfaces ];
var wv_j = FindIndex(w,v);
var f = NewFace(v, w, u);
f.prev = v.f[j-1];
v.f[j] = f;
w.v[(2*k[w.i] + wv_j-1 ) % (2*k[w.i])] = u;
w.f[(2*k[w.i] + wv_j-1 ) % (2*k[w.i])] = f;
var uv_j = FindIndex(u,v);
u.v[(uv_j+1) % (2*k[u.i])] = w;
u.f[uv_j] = f;
faces.push(f);
}
}
}
var j = 0;
for (var l = 0; l < n_levels; ++l) {
var lvertices = [];
for (; j < vertices.length && vertices[j].level == l; ++j) {
lvertices.push(vertices[j]);
}
lvertices.sort(function(u,w) { return geom.Norm(u.pos) - geom.Norm(w.pos); });
lvertices.forEach(function(v) {
ProcessVertex(v);
});
}
faces.forEach(function(f) {
f.center = geom.HyperbolicCenter(f.v[0].pos, f.v[1].pos, f.v[2].pos);
});
return {
vertices: vertices,
faces: faces,
k: k,
};
}
Insert cell
import {slider} from "@jashkenas/inputs"
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