Published unlisted
Edited
Jan 16, 2020
3 stars
Insert cell
Insert cell
options = ({})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = DOM.svg(...size);
const $svg = d3.select(svg).append('g').attr('transform', `translate(${size[0]/2}, ${size[1]/2})`);
// Avoid mutations on the original data objects.
const clone = obj => JSON.parse(JSON.stringify(obj));
const nodes = clone(data.nodes);
const links = clone(data.links);
// This initialization is normally done by d3-force.
const index = {};
nodes.forEach((node, i) => {
node.x = node.y = size / 2;
index[node.id] = node;
});
links.forEach(link => {
link.source = index[link.source];
link.target = index[link.target];
});
const $link = $svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter().append('line');

const $node = $svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter().append('circle')
.attr('r', 5)
.call(d3.drag()
.on('start', onDragStart)
.on('drag', onDrag)
.on('end', onDragEnd)
);

// The update loop.
let updating = false;
worker.subscribe('updateLoop', async (data) => {
if(data[0] === 'update') {
const positions = data[1];
if(positions.length === nodes.length * 2) {
for(let i = 0; i < nodes.length; i++) {
nodes[i].x = positions[i + i];
nodes[i].y = positions[i + i + 1];
}
update();
}
if(!updating) {
updating = true;
await new Promise(requestAnimationFrame);
worker.send('update');
updating = false;
}
}
});

// Unfreeze the simulation.
worker.send('restart');
// Kick off the update loop.
worker.send('update');
worker.send('pin', 0, 0, 0);

function update() {
$link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

$node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}

function onDragStart(d) {
if(!d3.event.active) {
worker.send('alphaTarget', 0.3);
worker.send('restart');
};
worker.send('pin', d.id, d.x, d.y);
}

function onDrag(d) {
worker.send('pin', d.id, d3.event.x, d3.event.y);
}

function onDragEnd(d) {
if(!d3.event.active) worker.send('alphaTarget', 0);
worker.send('release', d.id);
}
return svg;
}
Insert cell
Insert cell
// Our worker will run the simulation and return the updated
// coordinates when requested.
worker = {
// 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) {
self.importScripts('https://d3js.org/d3.v5.min.js');

const sim = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(0, 0));
sim.stop();
let positions, index;
const updateData = (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);
positions = new Float32Array(data.nodes.length * 2);
index = {};
data.nodes.forEach((node, i) => { index[node.id] = node; });
};
updateData(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': {
for(let i = 0; i < data.nodes.length; i++) {
positions[i + i] = data.nodes[i].x;
positions[i + i + 1] = data.nodes[i].y;
}
return self.postMessage(['update', 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 'data': return updateData(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.onmessage = 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
d3 = require('d3')
Insert cell
import {transformValue} from '@mootari/transforming-input-values-on-the-fly'
Insert cell
import {slider, checkbox} from "@jashkenas/inputs"
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