Published
Edited
Aug 6, 2022
16 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// A parametrization p(t) of the trefoil knot
function p(t) {
return [
Math.sin(3 * t),
Math.sin(t) + 2 * Math.sin(2 * t),
Math.cos(t) - 2 * Math.cos(2 * t)
];
}
Insert cell
// p'(t) - the tangent vector
function pp(t) {
return [
3 * Math.cos(3 * t),
Math.cos(t) + 4 * Math.cos(2 * t),
4 * Math.sin(2 * t) - Math.sin(t)
];
}
Insert cell
// The unit tangent
function T(t) {
let v = pp(t);
let norm = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / norm, v[1] / norm, v[2] / norm];
}
Insert cell
// The unit normal vector
function N(t) {
let v = pp(t);
let norm = Math.sqrt(v[1] * v[1] + v[2] * v[2]);
return [0, -v[2] / norm, v[1] / norm];
}
Insert cell
// We'll the bi-normal vector using the
// cross product of T and N
function cross(v1, v2) {
let X = v1[1] * v2[2] - v1[2] * v2[1];
let Y = v1[2] * v2[0] - v1[0] * v2[2];
let Z = v1[0] * v2[1] - v1[1] * v2[0];
return [X, Y, Z];
}
Insert cell
Insert cell
// A global parameter defining the radius of the tube
r = 0.5
Insert cell
// The common viewpoint for all the pictures is to
// look down the positive x-axis.
function viewpoint() {
let rot = (2 * Math.PI) / 3;
rot = rot.toString();
let d = 1 / Math.sqrt(3);
d = d.toString();
let ddd = d + " " + d + " " + d;
return d3
.create('viewpoint')
.attr('position', "10 0 0")
.attr('orientation', ddd + " " + rot);
}
Insert cell
// This generates the main picture at the top of the page and
// and the small picture at the bottom of the page.
// The size parameter determines the overal size of the picture
// as a fraction of the width of the window.
function* trefoil_pic(size = 0.7) {
let this_width = size * width;
let this_height = this_width;
let container = d3
.create('div')
.style('width', this_width.toString() + 'px')
.style('height', this_height.toString() + 'px');
let scene = container
.append('x3d')
.attr('width', this_width.toString() + 'px')
.attr('height', this_height.toString() + 'px')
.attr('showLog', false)
.append('scene');
scene.append(() => viewpoint().node());

let transform = scene.append('transform');
transform.append(() => surface().node());
transform.append(() => mesh().node());
transform.append(() => trefoil_path().node());
transform.append(() => tnb_circle().node());

yield container.node();
x3dom.reload();
}
Insert cell
// Generate the surface
function surface() {
let surface = d3.create('shape').attr('class', 'surface');
surface
.append('appearance')
.attr('sortKey', 2)
.append('material')
.attr('diffuseColor', '0.6 0.6 1')
.attr('specularColor', '0.2 0.2 0.4')
.attr('transparency', 0);
surface
.append('IndexedFaceSet')
.attr('solid', 'false')
.attr('creaseAngle', '3.14156')
.attr('coordIndex', surface_strings.faces)
.append('Coordinate')
.attr('id', 'the_Coordinate')
.attr('point', surface_strings.coord);
return surface;
}
Insert cell
// Generates points on the surface of a tube of
// radius r wrapped around the trefoil knot.
function trefoil_tube(s, t, r) {
let position = p(t);
let unit_tangent = T(t);
let unit_normal = N(t);
let bi_normal = cross(unit_tangent, unit_normal);
let cos = r * Math.cos(s);
let sin = r * Math.sin(s);
let x = position[0] + cos * unit_normal[0] + sin * bi_normal[0];
let y = position[1] + cos * unit_normal[1] + sin * bi_normal[1];
let z = position[2] + cos * unit_normal[2] + sin * bi_normal[2];
return [x, y, z];
}
Insert cell
surface_strings.faces
Insert cell
// Generate the coordinate and index strings for the surface
surface_strings = {
let s_steps = 32;
let t_steps = 4 * 64;
let s_step = (2 * Math.PI) / s_steps;
let t_step = (2 * Math.PI) / t_steps;
let coord = '';
for (let i = 0; i <= t_steps; i++) {
for (let j = 0; j <= s_steps; j++) {
let s = j * s_step;
let t = i * t_step;
let xyz = trefoil_tube(s, t, r);
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
coord = coord + x.toString() + ' ';
coord = coord + y.toString() + ' ';
coord = coord + z.toString() + ', ';
}
}
let faces = '';
for (let i = 6; i < t_steps - 6; i++) {
for (let j = 0; j < s_steps; j++) {
let v1 = i * s_steps + j + i;
v1 = v1.toString();
let v2 = i * s_steps + j + i + 1;
v2 = v2.toString();
let v3 = (i + 1) * s_steps + (j + 1) + i + 1;
v3 = v3.toString();
let v4 = (i + 1) * s_steps + (j + 1) + i;
v4 = v4.toString();
faces = faces + v1 + ' ' + v2 + ' ' + v3 + ' ' + v4 + ' ' + v1 + ' -1 ';
}
}

return {
coord: coord,
faces: faces
};
}
Insert cell
// Generate the mesh
function mesh() {
let mesh = d3.create('shape');
mesh
.append('appearance')
.append('material')
.attr('transparency', '0.7');
mesh
.append('IndexedLineSet')
.attr('coordIndex', mesh_strings.faces)
.append('Coordinate')
.attr('point', mesh_strings.coord);
return mesh;
}
Insert cell
// Generate the coordinate and index strings for the mesh
mesh_strings = {
let s_steps = 16;
let t_steps = 2 * 64;
let s_step = (2 * Math.PI) / s_steps;
let t_step = (2 * Math.PI) / t_steps;
let coord = '';
for (let i = 0; i <= t_steps; i++) {
for (let j = 0; j <= s_steps; j++) {
let s = j * s_step;
let t = i * t_step;
let xyz = trefoil_tube(s, t, 1.02 * r);
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
coord = coord + x.toString() + ' ';
coord = coord + y.toString() + ' ';
coord = coord + z.toString() + ', ';
}
}
let faces = '';
for (let i = 3; i < t_steps - 3; i++) {
for (let j = 0; j < s_steps; j++) {
let v1 = i * s_steps + j + i;
v1 = v1.toString();
let v2 = i * s_steps + j + i + 1;
v2 = v2.toString();
let v3 = (i + 1) * s_steps + (j + 1) + i + 1;
v3 = v3.toString();
let v4 = (i + 1) * s_steps + (j + 1) + i;
v4 = v4.toString();
faces = faces + v1 + ' ' + v2 + ' ' + v3 + ' ' + v4 + ' ' + v1 + ' -1 ';
}
}
return {
coord: coord,
faces: faces
};
}
Insert cell
// Generate the path
function trefoil_path() {
let path = d3.create('shape');
path
.append('IndexedLineSet')
.attr('coordIndex', d3.range(path_string.split(',').length))
.append('Coordinate')
.attr('point', path_string);
return path;
}
Insert cell
// Generate the coordinate string for the path
path_string = {
let t_steps = 4 * 64;
let t_step = (2 * Math.PI) / t_steps;
let coord = '';
for (let i = 0; i <= t_steps; i++) {
let t = i * t_step;
let xyz = p(t);
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
coord = coord + x.toString() + ' ';
coord = coord + y.toString() + ' ';
coord = coord + z.toString() + ', ';
}
return coord.substring(0, coord.length - 2);
}
Insert cell
// Generate the TNB frame and the circle it generates.
// Kinda long because there are 7 parts - a cylinder and
// cone for the stem and head of each arrow, plus the
// circle. Accounting for the position and orientation of
// the frame is a bit tricky as well.

function tnb_circle() {
let t0 = 0;
let unit_tangent = T(t0);
let unit_normal = N(t0);
let bi_normal = cross(T(t0), N(t0));

let arrows = d3
.create('transform')
.attr('class', 'tnb_circle')
.attr('translation', '0 0 -1');
let [a, b, c] = unit_tangent;
let norm = Math.sqrt(a ** 2 + b ** 2 + c ** 2);
let theta = -Math.acos(b);
let unit_tangent_transform = arrows
.append('transform')
.attr('translation', `${(r * a) / 2} ${(r * b) / 2} ${(r * c) / 2}`)
.attr('rotation', `${-c} 0 ${a} ${theta}`);
let unit_tangent_vector = unit_tangent_transform.append('shape');
unit_tangent_vector
.append('appearance')
.attr('sortKey', 1)
.append('material')
.attr('diffuseColor', '0 0 0');
unit_tangent_vector
.append('cylinder')
.attr('radius', 0.01)
.attr('height', `${r}`)
.attr('subdivision', '32');

let head_length = 0.2;
let unit_tangent_vector_head = arrows
.append('transform')
.attr('rotation', `${-c} 0 ${a} ${theta}`)
.attr(
'translation',
`${(r - head_length / 2) * a} ${(r - head_length / 2) * b}
${(r - head_length / 2) * c}`
)
.append('shape');
unit_tangent_vector_head
.append('appearance')
.attr('sortKey', 1)
.append('material')
.attr('diffuseColor', '0 0 0');
unit_tangent_vector_head
.append('cone')
.attr('bottomRadius', '0.05')
.attr('topRadius', '0')
.attr('height', head_length);

[a, b, c] = unit_normal;
norm = Math.sqrt(a ** 2 + b ** 2 + c ** 2);
theta = -Math.acos(b);
let normal_transform = arrows
.append('transform')
.attr('translation', `0 0 ${r / 2}`)
.attr('rotation', `${-c} 0 ${a} ${theta}`);
let normal_vector = normal_transform.append('shape');
normal_vector
.append('appearance')
.append('material')
.attr('diffuseColor', '0 0 0');
normal_vector
.append('cylinder')
.attr('radius', 0.01)
.attr('height', `${r}`)
.attr('subdivision', '32');

let normal_vector_head = arrows
.append('transform')
.attr('rotation', `${-c} 0 ${a} ${theta}`)
.attr('translation', `0 0 ${r - head_length / 2}`)
.append('shape');
normal_vector_head
.append('appearance')
.append('material')
.attr('diffuseColor', '0 0 0');
normal_vector_head
.append('cone')
.attr('bottomRadius', '0.05')
.attr('topRadius', '0')
.attr('height', head_length);

[a, b, c] = bi_normal;
norm = Math.sqrt(a ** 2 + b ** 2 + c ** 2);
theta = -Math.acos(b);
let bi_normal_transform = arrows
.append('transform')
.attr('translation', `${(r * a) / 2} ${(r * b) / 2} ${(r * c) / 2}`)
.attr('rotation', `${-c} 0 ${a} ${theta}`);
let bi_normal_vector = bi_normal_transform.append('shape');
bi_normal_vector
.append('appearance')
.append('material')
.attr('diffuseColor', '0 0 0');
bi_normal_vector
.append('cylinder')
.attr('radius', 0.01)
.attr('height', `${r}`)
.attr('subdivision', '32');

let bi_normal_vector_head = arrows
.append('transform')
.attr('rotation', `${-c} 0 ${a} ${theta}`)
.attr(
'translation',
`${(r - head_length / 2) * a} ${(r - head_length / 2) * b}
${(r - head_length / 2) * c}`
)
.append('shape');
bi_normal_vector_head
.append('appearance')
.append('material')
.attr('diffuseColor', '0 0 0');
bi_normal_vector_head
.append('cone')
.attr('bottomRadius', '0.05')
.attr('topRadius', '0')
.attr('height', head_length);

let circle_string = '';
let n = 64;
for (let i = 0; i <= n; i++) {
let t = (2 * i * Math.PI) / n;
let x = r * Math.cos(t) * unit_normal[0] + r * Math.sin(t) * bi_normal[0];
let y = r * Math.cos(t) * unit_normal[1] + r * Math.sin(t) * bi_normal[1];
let z = r * Math.cos(t) * unit_normal[2] + r * Math.sin(t) * bi_normal[2];
circle_string = circle_string + x.toString() + ' ';
circle_string = circle_string + y.toString() + ' ';
circle_string = circle_string + z.toString();
if (i < n) {
circle_string = circle_string + ', ';
}
}
let circle = arrows.append('shape');
circle
.append('IndexedLineSet')
.attr('coordIndex', d3.range(circle_string.split(',').length))
.append('Coordinate')
.attr('point', circle_string);

return arrows;
}
Insert cell
Insert cell
// The t_values for both t_point and t_frame that are
// pased to mbostock's Scrubber.
t_values = d3.range(0, 2 * Math.PI, Math.PI / 100)
Insert cell
// Move the point on point_path_pic according to t_point
{
let [x, y, z] = p(t_point);
d3.select(point_path_pic)
.select('.position')
.attr('translation', [x, y, z].toString().replace(/,/g, ' '));
}
Insert cell
// Move the point on frame_path_pic according to t_frame
{
let unit_tangent0 = T(0);
let [a0, b0, c0] = unit_tangent0;

let [x, y, z] = p(t_frame);
let unit_tangent1 = T(t_frame);
let [a1, b1, c1] = unit_tangent1;
let dot = a0 * a1 + b0 * b1 + c0 * c1;
let theta = Math.acos(dot);
let [a, b, c] = cross(unit_tangent0, unit_tangent1);
d3.select(frame_path_pic)
.select('.tnb_circle')
.attr('rotation', `${a} ${b} ${c} ${theta}`)
.attr('translation', [x, y, z].toString().replace(/,/g, ' '));
}
Insert cell
Insert cell
d3 = require('d3@5')
Insert cell
x3dom = require('x3dom').catch(() => window['x3dom'])
Insert cell
import { radio, slider } from "@jashkenas/inputs"
Insert cell
import { Scrubber } from "@mbostock/scrubber"
Insert cell
// Supress the dashed box that appears around X3Dom display.
html`<style>
canvas {
outline: none;
}
output[name=o] {
display: none
}
</style>`
Insert cell
Insert cell
small_pic = trefoil_pic(0.2)
Insert cell
run_it = {
let transform = d3.select(small_pic).select(".tnb_circle");
let unit_tangent0 = T(0);
let [a0, b0, c0] = unit_tangent0;

function start(t) {
let [x, y, z] = p(t / 500);
let unit_tangent1 = T(t / 500);
let [a1, b1, c1] = unit_tangent1;
let dot = a0 * a1 + b0 * b1 + c0 * c1;
let theta = Math.acos(dot);
let [a, b, c] = cross(unit_tangent0, unit_tangent1);
transform
.attr("rotation", `${a} ${b} ${c} ${theta}`)
.attr("translation", [x, y, z].toString().replace(/,/g, " "));
}
function stop() {
timer.stop();
transform.attr("translation", "0 0 -1").attr("rotation", "1 0 0 0");
}
let timer = d3.timer(start);
stop();
d3.select(small_pic)
.on("mouseover", () => timer.restart(start))
.on("mouseout", stop);
}
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