Public
Edited
Aug 13, 2023
2 stars
Insert cell
Insert cell
chart = {
const height = width;
const color = d3.scaleOrdinal(d3.schemeTableau10);
const context = DOM.context2d(width, height);
const data_nodes = data.map(Object.create);
const floorY = height/2-100
// const wall_nodes = Array.from({length: 200}, (_, i) => ({r: width/400-1, group: 5, xpos: i*width/200-width/2, ypos:floorY})).map(Object.create);
const wall_r = 1e5
const wall_node = [{r: wall_r, group:5, ypos: wall_r+floorY, y: wall_r+floorY, x: 0, target_x:0}]
const nodes = data_nodes.concat(wall_node)


const simulation = d3.forceSimulation(nodes)
.alphaTarget(0.3) // stay hot
.velocityDecay(0.1) // low friction
.force("x", d3.forceX(d=>d.target_x).strength(d=> d.group == 5 ? 1 : 0.05))
// .force('center', d3.forceCenter())
.force("y", d3.forceY().y(d=>floorY - d.r - 1).strength(d=>d.group==5 ? 0 : 0.005))
.force("ywall", d3.forceY().y(d => d.group==5?d.ypos : 0).strength(d=>d.group==5 ? 1 : 0))
.force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
// .force("wall", wallCollideY(floorY, -1).strength(0.9))
// .force("charge", d3.forceManyBody().strength((d, i) => i ? -width/200 : -width * 2 / 3))
// .force("charge", d3.forceManyBody().strength((d, i) => i ? -width/150 : 0))
.on("tick", ticked);

d3.select(context.canvas)
.on("touchmove", event => event.preventDefault())
.on("pointermove", pointermoved);

invalidation.then(() => simulation.stop());

function pointermoved(event) {
const [x, y] = d3.pointer(event);
nodes[0].fx = x - width / 2;
nodes[0].fy = y - height / 2;
}

function ticked() {
context.clearRect(0, 0, width, height);
context.save();
context.translate(width / 2, height / 2);
for (let i = 1; i < nodes.length; ++i) {
// why are we manually drawing circles? Cause it's a canvas rather than SVG I guess?
// if (nodes[i].group ==5) {
// nodes[i].y=nodes[i].ypos
// nodes[i].x=0
// nodes[i].vy=0
// nodes[i].vx=0
// }
// nodes[i].y = Math.min(nodes[i].y, floorY-nodes[i].r)
const d = nodes[i];
context.beginPath();
context.moveTo(d.x + d.r, d.y);
context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
context.fillStyle = color(d.group);
context.fill();
context.beginPath();
context.moveTo(d.x, d.y);
context.lineTo(d.target_x, d.y);
context.strokeStyle = 'black';
context.stroke()
}
context.restore();
}

return context.canvas;
}
Insert cell
// function wallForce() {
// // function(y) { // rather than passing the force function itself, we call this factory function that returns a force with y already set. We don't care about that because this function doesn't need to be general.
// var strength = constant(0.1),
// nodes,
// strengths,
// yz; // don't need this

// // if (typeof y !== "function") y = constant(y == null ? 0 : +y);

// function force(alpha) {
// // the actual force application.
// // we want to effectively evaluate a collision with a flat surface
// for (var i = 0, n = nodes.length, node; i < n; ++i) {
// node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
// }
// }

// function initialize() {
// if (!nodes) return;
// var i, n = nodes.length;
// strengths = new Array(n);
// yz = new Array(n);
// for (i = 0; i < n; ++i) {
// strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
// }
// }

// force.initialize = function(_) {
// nodes = _;
// initialize();
// };

// force.strength = function(_) {
// return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
// };

// force.y = function(_) {
// return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y;
// };

// return force;
// }
Insert cell
Insert cell
function wallCollideY(wall_y, wall_dir) {
var nodes,
strength = 1,
random
function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i]
// node.vy += (yz[i] - node.y) * strengths[i] * alpha;
if ((node.y + node.vy - wall_y) * wall_dir < 0 && Math.sign(node.vy)==-wall_dir ) {
node.vy += (-2*node.vy) * strength
}
// node.y = node.y - wall.y
}
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
}

force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};

force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
return force
}
Insert cell
// import {quadtree} from "d3-quadtree";
// import constant from "./constant.js";
// import jiggle from "./jiggle.js";
{
function x(d) {
return d.x + d.vx;
}

function y(d) {
return d.y + d.vy;
}

function wallCollide(radius) {
var nodes,
radii,
random,
strength = 1,
iterations = 1;

if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);

function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;

for (var k = 0; k < iterations; ++k) {
// go through the tree assigning r values starting with the leaf nodes and working upwards (post-order traversal):
tree = quadtree(nodes, x, y).visitAfter(prepare);

// go through the nodelist
for (i = 0; i < n; ++i) {
node = nodes[i];
// radius and radius-squared for this node
ri = radii[node.index], ri2 = ri * ri;
xi = node.x + node.vx; // projected position of this node after next tick
yi = node.y + node.vy;
// pre-order traverse the tree calling apply for every node
tree.visit(apply);
}
}

function apply(quad, x0, y0, x1, y1) {
// quad is the node of the quadtree
// x0 etc are the bounds of the node
var data = quad.data, rj = quad.r, r = ri + rj;
// if the quad is a leaf node i.e. contains only one (force-graph) node, data is the data for that node
// otherwise data is undefined
// rj is the radius of the largest node in this quad
if (data) {
// this node is a leaf node

// we only want to evaluate each node pair once, so compare indices and evaluate one way round
if (data.index > node.index) { // i.e. 'data' node has not yet been evaluated at the force() level. Nor has 'node' because that's the one we're doing now.
// Do they both have their original positions and velocities? not necessarily, if either had been involved in multiple collisions they may already have been adjusted. How does this work?

var x = xi - data.x - data.vx, // difference between projected position of this node after next tick and back-projected position of data node on the last tick? But why?
y = yi - data.y - data.vy,
l = x * x + y * y; // square euclidean distance
if (l < r * r) { // distance at projected collision point (??) is less than sum of radii
if (x === 0) x = jiggle(random), l += x * x; // if the x component of l is zero, make it nonzero
if (y === 0) y = jiggle(random), l += y * y; // likewise y
// l = (r - (l = Math.sqrt(l))) / l * strength;
//expanded:
l = Math.sqrt(l) // euclidean distance
l = ((r - l) / l); // idk we are still calling this l but this is the amount of overlap as a percentage of the distance between the two node centers at collision. Not clear why the centre distance is a better denominator than the sum of radii? => So the force can get very large when the nodes are very close, rather than being capped at strength*1
l = l * strength; // apply strength coefficient => overall force coefficient
node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj)); //ughhh
// xl = x*l // = x above; x component of overlap times force coefficient
// rj2 = rj*rj // = rj above; square of data node radius
// r_new = rj2/(ri2+rj2) // mass fraction of data node
// node.vx += xl * r_new // x force times mass fraction of other node
node.vy += (y *= l) * r; // y force times mass frac of other node
data.vx -= x * (r = 1 - r); // x force times mass frac of this node, opposite direction
data.vy -= y * r; // y force times mass frac of this node, opposite direction

// note that only velocities are changed, not positions
// the default strength of 1 will produce what? Something like elastic collisions?
}
}
return;
}
// if the
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}

function prepare(quad) {
// if it's a leaf node, assign its r property to the radius of the contained node
if (quad.data) return quad.r = radii[quad.data.index];
// otherwise, loop over its child nodes and assign its r property to the largest r of those
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
radii = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}

force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};

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

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

force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};

return force;
}
}
Insert cell
{
let a = 4
a = (3+ (a=Math.sqrt(a)) )/a
return a
}
Insert cell
Insert cell
data = {
const k = width / 200;
// const r = d3.randomUniform(k, k * 4);
const r = () => 5
const x = d3.randomUniform(-width/3, width/3)
const n = 4;
return Array.from({length: 200}, (_, i) => ({r: r(), target_x: x(), group: i && (i % n + 1)}));
}
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