Published
Edited
Jan 21, 2021
Importers
29 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
canvas = {
while(true) {
if(renderer.needsUpdate) {
renderer.render();
renderer.needsUpdate = false;
}
yield renderer.renderer.domElement;
}
}
Insert cell
stepper = {
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function setupData(nodes, links) {
const index = {};
nodes.forEach((node, i) => {
node.x = 0;
node.y = 0;
index[node.id] = node;
});
links.forEach(link => {
link.source = index[link.source];
link.target = index[link.target];
});
return index;
}
function updatePositions(renderer, coordinates) {
renderer.setPositions(coordinates);
renderer.needsUpdate = true;
renderer.autoZoom(10);
}
// Avoid mutations on the original data objects.
const nodes = clone(graph.nodes);
const links = clone(graph.links);
// This initialization is normally done by d3-force.
const index = setupData(nodes, links);
const pairs = [].concat(...links.map(link => [ link.source.id, link.target.id] ));
renderer.setLinks(nodes, pairs);

// The update loop.
let expectedBufferSize = nodes.length * Float32Array.BYTES_PER_ELEMENT * 3;
let updateBuffer = null;
let needsUpdate = false;
const g = createGraph();
const l = createLayout(g, {
springLength: 10,
springCoeff: 0.0008,
gravity: -100,
theta: 0.5,
dragCoeff: 0,
timeStep: 10
});
nodes.forEach(node => { g.addNode(node.id); });
links.forEach(link => { g.addLink(link.source.id, link.target.id); });
const step = () => {
l.step();
const positions = [];
g.forEachNode(node => {
const position = l.getNodePosition(node.id);
positions.push(position.x, position.y, 0);
});
renderer.setPositions(positions);
renderer.needsUpdate = true;
renderer.autoZoom(10);
}
/*
worker.listen('updateLoop', (e) => {
if(e.data[0] === 'update') {
updateBuffer = e.data[1];
needsUpdate = true;
}
});
// Unfreeze the simulation.
worker.send('restart');
// Kick off the update loop.
worker.send('update');
worker.send('pin', 0, 0, 0);
worker.send('alphaDecay', .0001);
const step = () => {
if(needsUpdate) {
if(updateBuffer) {
//if(updateBuffer && updateBuffer.byteLength === expectedBufferSize) {
const positions = JSON.parse(updateBuffer);
//const positions = new Float32Array(updateBuffer);
updatePositions(renderer, positions);
}
updateBuffer = null;
needsUpdate = false;
worker.send('update');
}
}
invalidation.then(() => {
console.debug('cleaning up');
worker.clear('updateLoop');
});
*/
while(true) {
step();
yield true;
}
}
Insert cell
renderer = {
const renderer = new Renderer({
numPoints: 20000,
numLines: 20000,
width: width,
height: Math.floor(width / (16/10)),
lineColor: '#000',
//lineColor: null,
pointColor: '#000',
// pointColor: null,
backgroundColor: '#fffffa',
pointSize: 5,
textureSize: 256,
cameraFov: 45,
cameraNear: 1,
cameraFar: 100000,
cameraX: 0,
cameraY: 0,
cameraZ: 800,
DebugAxes: 100
});
invalidation.then(() => {
renderer.renderer.dispose();
});

return renderer;
}
Insert cell
class Renderer {

/**
*/
constructor(options) {
const { numPoints, numLines } = options;

this.camera = this.createCamera(options);
this.scene = this.createScene(options);
this.renderer = this.createRenderer(options);

const positions = new THREE.BufferAttribute(new Float32Array(numPoints * 3), 3);
const indices = new THREE.BufferAttribute(new Uint16Array(numLines * 2), 1);
this.points = this.createPointsMesh(positions, options);
this.lines = this.createLinesMesh(positions, indices, options);
this.scene.add(this.lines);
this.scene.add(this.points);
}
autoZoom(pad = 0) {
this.__zoomOrthographic(this.points, this.camera, this.renderer, pad);
}

/**
*/
render() {
this.renderer.render(this.scene, this.camera);
}

/**
*/
createCamera(options) {
const {
cameraFar,
cameraFov,
cameraNear,
cameraX,
cameraY,
cameraZ,
height,
width
} = options;
//const camera = new THREE.PerspectiveCamera(cameraFov, width / height, cameraNear, cameraFar);
const wh = width / 2, hh = height / 2;
const camera = new THREE.OrthographicCamera( -wh, wh, hh, -hh, 1, cameraFar);
camera.position.set(cameraX, cameraY, cameraZ);

return camera;
}

/**
*/
createScene(options) {
const {backgroundColor} = options;
const scene = new THREE.Scene();
scene.background = new THREE.Color(backgroundColor);
return scene;
}

/**
*/
createRenderer(options) {
const {height, width} = options;
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
return renderer;
}

/**
*/
createPointTexture(size) {
const ctx = DOM.context2d(size, size, 1);
ctx.fillStyle = 'rgba(255,255,255,0)';
ctx.fillRect(0, 0, size, size);

const sh = size / 2;
ctx.arc(sh, sh, sh, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();

const texture = new THREE.CanvasTexture(ctx.canvas);
//texture.premultipliedAlpha = true;
return texture;
}

/**
*/
createPointsMesh(positions, options) {
const { pointColor, pointSize, textureSize } = options;

const geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', positions);

const material = new THREE.PointsMaterial({
size: pointSize,
sizeAttenuation: true,
color: pointColor,
map: this.createPointTexture(textureSize),
alphaTest: .5,
transparent: true,
depthTest: true,
depthWrite: true
});
const mesh = new THREE.Points(geometry, material);
return mesh;
}

/**
*/
createLinesMesh(positions, indices, options) {
const { lineColor } = options;

const geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', positions);
geometry.setIndex(indices);

const material = new THREE.LineBasicMaterial({color: lineColor});
const mesh = new THREE.LineSegments(geometry, material);
return mesh;
}

/**
*/
setLinks(nodes, pairs) {
const map = {};
nodes.forEach((node, i) => map[node.id] = i);
const indices = this.lines.geometry.getIndex();
for(let i = 0; i < pairs.length; i++) {
indices.array[i] = map[pairs[i]];
}
indices.needsUpdate = true;
this.lines.geometry.setDrawRange(0, pairs.length);
}

/**
*/
setPositions(coordinates) {
const positions = this.points.geometry.attributes.position;
for(let i = 0; i < coordinates.length; i++) {
positions.array[i] = coordinates[i];
}
this.points.geometry.setDrawRange(0, coordinates.length / 3);
positions.needsUpdate = true;
}
/**
*/
__zoomOrthographic(target, camera, renderer, pad = 0) {
target.geometry.computeBoundingBox();
const box = target.geometry.boundingBox;
let width = box.max.x - box.min.x;
let height = box.max.y - box.min.y;
if(!width || !height) {
return;
}

const cx = box.min.x + width / 2;
const cy = box.min.y + height / 2;

const size = renderer.getSize();
const ratio = size.width / size.height;
const boxRatio = width / height;
if(boxRatio >= ratio) {
height = width / ratio;
}
else {
width = height * ratio;
}
const wh = width / 2;
const hh = height / 2;

camera.left = cx - wh - pad;
camera.right = cx + wh + pad;
camera.bottom = cy - hh - pad;
camera.top = cy + hh + pad;
camera.updateProjectionMatrix();
}

}

Insert cell
Insert cell
// Our worker will run the simulation and return the updates
// coordinates when requested.
worker = {
return null;
// Initial data for the worker. Because it will get
// inlined we don't have to worry about mutations.
const workerData = {
nodes: [],
links: []
};
// Note: Executes in worker context.
const worker = createWorker('force-layout', workerData, function onInit(self, data) {
const createGraph = require('https://bundle.run/ngraph.graph@0.0.14');
self.importScripts('https://d3js.org/d3.v5.min.js');

const sim = d3.forceSimulation()
//.force('link', d3.forceLink()
// .id(d => d.id)
// .strength(link => 1)
//)
.force('charge', d3.forceManyBody().theta(1.5))
.force('center', d3.forceCenter(0, 0))
;
sim.stop();

const positions = [];
let index;
const updateGraph = (nodes, links) => {
let i = -1;
while(data.nodes[++i] && nodes[i]) {
nodes[i].x = data.nodes[i].x;
nodes[i].y = data.nodes[i].y;
nodes[i].fx = data.nodes[i].fx;
nodes[i].fy = data.nodes[i].fy;
}
sim.nodes(data.nodes = nodes);
//sim.force('link').links(data.links = links);
index = {};
positions.length = nodes.length * 3;
data.nodes.forEach((node, i) => { index[node.id] = node; });
};
updateGraph(data.nodes, data.links);
const setFixed = (id, fx, fy) => {
index[id].fx = fx;
index[id].fy = fy;
};
const processMessage = (msg, ctx) => {
const command = msg[0];
const args = msg.slice(1);
// Custom commands.
switch(command) {
case 'update':
const nodes = data.nodes;
const length = nodes.length;
let j, n;
for(let i = 0; i < length; i++) {
j = i + i + i;
positions[j ] = nodes[i].x;
positions[j + 1] = nodes[i].y;
positions[j + 2] = 0;
//n = nodes[i];
//positions[j + 2] = -(n.fx * n.fx + n.fy * n.fy) || 0;
}
return self.postMessage(['update', JSON.stringify(positions)]);
case 'pin':
return setFixed(args[0], args[1], args[2]);
case 'release':
return setFixed(args[0], null, null);
case 'get':
return self.postMessage([ 'get', args[0], ctx[args[0]]() ]);
case 'setGraph':
return updateGraph(args[0], args[1]);
case 'reheat':
sim.alpha(Math.max(sim.alpha(), args[0]));
return sim.restart();
}
// Pass through to simulation.
return ctx[command].apply(ctx, args);
}
self.addEventListener('message', function(e) {
let ctx = sim, data = e.data;
if(!Array.isArray(data[0])) {
data = [data];
}
// Handle chained commands.
while(data.length) {
ctx = processMessage.call(null, data.shift(), ctx);
}
});
});
return worker;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {slider, checkbox} from "@jashkenas/inputs"
Insert cell
Insert cell
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