Public
Edited
Dec 26, 2023
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
canvas = {
const context = DOM.context2d(width, height);
context.fillStyle = "#fff";
context.fillRect(0, 0, width, height);

let nodes = [];

setup(nodes);

let generation = 0;

while (true) {
for (let n of nodes) {
n.run(nodes);
}

// context.clearRect(0, 0, width, height);
// console.log(nodes.length);
growth(nodes);
// display(context, nodes);
draw(context, nodes, generation);
generation++;

yield context.canvas;
}
}
Insert cell
Insert cell
setup = (nodes) => {
const angInc = (Math.PI * 2) / numberOfNodes;

// radius of starting circle
const rayStart = radius;

// Add nodes along the circle
for (let i = 0; i < numberOfNodes; i++) {
// Position
const a = i * angInc;
const x = width / 2 + Math.cos(a) * rayStart;
const y = height / 2 + Math.sin(a) * rayStart;

nodes.push(
new Node(
x,
y,
maxForce,
maxSpeed,
desiredSeparation,
separationCohesionRation
)
);
}
}
Insert cell
// Insert more nodes when each edge gets longer than the maximum edge length
growth = (nodes) => {
for (let i = 0; i < nodes.length - 1; i++) {
const n1 = nodes[i];
const n2 = nodes[i + 1];
const dx = n2.position.x - n1.position.x;
const dy = n2.position.y - n2.position.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d > maxEdgeLen) {
// Can add more rules for inserting nodes
const index = nodes.indexOf(n2);
const middleNode = n1.position.add(n2.position).divide(2);
nodes.splice(
index,
0,
new Node(
middleNode.x,
middleNode.y,
maxForce,
maxSpeed,
desiredSeparation,
separationCohesionRation
)
);
}
}
}
Insert cell
display = (context, nodes) => {
context.beginPath();
for (let i = 0; i < nodes.length - 1; i++) {
let p1 = nodes[i].position;
let p2 = nodes[i + 1].position;
context.fillStyle = "black";
context.fillRect(p1.x, p1.y, 1, 1);
if (i == nodes.length - 2) {
//curveVertex(p2.x, p2.y)
context.rect(p2.x, p2.y, 1, 1);
}
}
}
Insert cell
draw = (context, nodes, generation) => {
for (let i = 0; i < nodes.length - 1; i++) {
let hue = (generation % 1000) / 500;

if (hue > 1) {
hue = 1 - (hue - 1);
}
const alpha = 0.5;

const colorScheme = d3.interpolateBlues(hue);

// Outline
let dx, dy;
context.save();
context.translate(nodes[i].position.x, nodes[i].position.y); // Move the origin to the start point
context.beginPath();
context.strokeStyle = colorScheme;
context.lineWidth = 0.02;
if (i !== nodes.length - 2) {
dx = nodes[i + 1].position.x - nodes[i].position.x;
dy = nodes[i + 1].position.y - nodes[i].position.y;
} else {
// Connect the start point and the end point
dx = nodes[0].position.x - nodes[i].position.x;
dy = nodes[0].position.y - nodes[i].position.y;
}
context.moveTo(0, 0);
context.lineTo(dx, dy);
context.stroke();
context.restore(); // Move back the origin to (0, 0)

// Historical path
context.save();
context.translate(nodes[i].prePosition.x, nodes[i].prePosition.y);
context.beginPath();
const dx2 = nodes[i].position.x - nodes[i].prePosition.x;
const dy2 = nodes[i].position.y - nodes[i].prePosition.y;
context.moveTo(0, 0);
context.lineTo(dx2, dy2);
context.strokeStyle = colorScheme;
// context.strokeStyle = `hsla(${hue}, 50%, 50%, ${alpha})`;
context.lineWidth = 0.05;
context.stroke();
context.restore(); // Move back the origin to (0, 0)
}
}
Insert cell
class Node {
constructor(x, y, mF, mS, dS, sCR) {
this.prePosition = new Vector(x, y);
this.position = new Vector(x, y);
this.velocity = new Vector(0, 0);
this.velocity.random2d();
this.acceleration = new Vector(0, 0);

this.maxForce = mF;
this.maxSpeed = mS;
this.desiredSeparation = dS;
this.separationCohesionRation = sCR;
}

run(nodes, generation) {
this.prePosition = this.position.clone();
this.differentiate(nodes);
this.update();
}

applyForce(force) {
this.acceleration.add(force);
}

differentiate(nodes) {
let separation = this.separate(nodes);
let cohesion = this.edgeCohesion(nodes);

separation.mult(this.separationCohesionRation);
this.applyForce(separation);
this.applyForce(cohesion);
}

update() {
this.velocity.add(this.acceleration);
this.velocity.limit(this.maxSpeed);
this.position.add(this.velocity);
this.acceleration.mult(0);
}

seek(target) {
let desired = target.sub(this.position);
desired = desired.setMag(this.maxSpeed);
let steer = desired.sub(this.velocity);
steer = steer.limit(this.maxForce);

return steer;
}

separate(nodes) {
let steer = new Vector(0, 0);
let count = 0;

for (let other of nodes) {
let d = this.position.dist(other.position);
if (d > 0 && d < this.desiredSeparation) {
let diff = this.position.clone().sub(other.position);
diff.normalize();
diff.divide(d); // Weight by distance
steer.add(diff);
count++;
}
}
if (count > 0) {
steer.divide(count);
}
if (steer.magnitude() > 0) {
steer.setMag(this.maxSpeed);
steer.sub(this.velocity);
steer.limit(this.maxForce);
}
return steer;
}

edgeCohesion(nodes) {
let sum = new Vector(0, 0);
let this_index = nodes.indexOf(this);

if (this_index != 0 && this_index != nodes.length - 1) {
sum = sum.add(nodes[this_index - 1].position);
sum = sum.add(nodes[this_index + 1].position);
} else if (this_index == 0) {
sum = sum.add(nodes[nodes.length - 1].position);
sum = sum.add(nodes[this_index + 1].position);
} else if (this_index == nodes.length - 1) {
sum = sum.add(nodes[this_index - 1].position);
sum = sum.add(nodes[0].position);
}

sum = sum.divide(2);

return this.seek(sum);
}
}
Insert cell
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}

clone() {
return new Vector(this.x, this.y);
}

magnitude() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}

setMag(n) {
return this.normalize().mult(n);
}

normalize() {
const m = this.magnitude();
if (m > 0) {
this.x = this.x / m;
this.y = this.y / m;
}
return this;
}

normal() {
const new_x = this.y;
const new_y = -this.x;

this.x = new_x;
this.y = new_y;

return this;
}

limit(max) {
const mSq = this.x * this.x + this.y * this.y;
if (mSq > max * max) {
this.divide(Math.sqrt(mSq)) //normalize it
.mult(max);
}
return this;
}

rotate(angle) {
const new_x = this.x * Math.cos(angle) - this.y * Math.sin(angle);
const new_y = this.x * Math.sin(angle) + this.y * Math.cos(angle);

this.x = new_x;
this.y = new_y;

return this;
}

angle() {
if (this.x == 0) {
return this.y > 0 ? Math.PI / 2 : (Math.PI * 3) / 2;
} else if (this.y == 0) {
return this.x > 0 ? 0 : Math.PI;
} else if (this.x > 0 && this.y > 0) {
return Math.atan(this.y / this.x);
} else if (this.x < 0 && this.y > 0) {
return Math.PI + Math.atan(this.y / this.x);
} else if (this.x < 0 && this.y < 0) {
return Math.PI + Math.atan(this.y / this.x);
} else if (this.x > 0 && this.y < 0) {
return Math.PI * 2 + Math.atan(this.y / this.x);
}
}

rotateTowards(v, a) {
return this.rotate((v.angle() - this.angle()) * a);
}

add(v) {
this.x += v.x;
this.y += v.y;
return this;
}

sub(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}

divide(scalar) {
this.x /= scalar;
this.y /= scalar;
return this;
}

mult(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}

dist(v) {
const dx = v.x - this.x;
const dy = v.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
}

dot(v) {
return this.x * v.x + this.y * v.y;
}

static average(vs) {
const average = new Vector(0, 0);
vs.forEach((v) => average.add(v));
average.divide(vs.length);
return average;
}

random2d() {
// Velocity
const randomAngle = Math.random() * Math.PI;
const x = Math.abs(1 * Math.cos(randomAngle));
const y = -Math.abs(1 * Math.sin(randomAngle));
this.x = x;
this.y = y;
}
}
Insert cell
Insert cell
mockmemorydata1 = FileAttachment("mockMemoryData (1).csv").csv()
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