Public
Edited
Aug 28, 2023
Insert cell
Insert cell
Insert cell
chart = {
const width = 928;
const height = 680;

const simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink().id(d => d.id))
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);

const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

let link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line");

let node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle");

function ticked() {
node.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);

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);
}

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

return Object.assign(svg.node(), {
update({nodes, links}) {

// Make a shallow copy to protect against mutation, while
// recycling old nodes to preserve position and velocity.
const old = new Map(node.data().map(d => [d.id, d]));
nodes = nodes.map(d => ({...old.get(d.id), ... d}));
links = links.map(d => ({...d}));

node = node
.data(nodes, d => d.id)
.join(enter => enter.append("circle")
.attr("r", 5)
.call(drag(simulation))
.call(node => node.append("title").text(d => d.id)));

link = link
.data(links, d => [d.source, d.target])
.join("line");

simulation.nodes(nodes);
simulation.force("link").links(links);
/**
Per the D3 docs
https://github.com/d3/d3-force#simulation_tick
"The natural number of ticks when the simulation is started is ⌈log(alphaMin) / log(1 - alphaDecay)⌉; by default, this is 300.""
I've found that for refining where the simulation locks nodes is best achieved by playing around with combinations of small
alphaMin(), and some amount of tick(). By arbitrarily choosing 100, we're sying apply the forces on the simuation with 1/3 the
frequency as is default. You can fiddle with this number to see how it changes things.
*/
simulation.alpha(1).restart().tick(100);
}
});
}
Insert cell
update = {
const nodes = data.nodes.filter(d => contains(d, time));
const links = data.links.filter(d => contains(d, time));
chart.update({nodes, links});
}
Insert cell
data = {
const {nodes, links} = await FileAttachment("sfhh@4.json").json();
for (const d of [...nodes, ...links]) {
d.start = d3.isoParse(d.start);
d.end = d3.isoParse(d.end);
};
return {nodes, links};
}
Insert cell
times = d3.scaleTime()
.domain([d3.min(data.nodes, d => d.start), d3.max(data.nodes, d => d.end)])
.ticks(1000)
.filter(time => data.nodes.some(d => contains(d, time)))
Insert cell
contains = ({start, end}, time) => start <= time && time < end
Insert cell
drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
import {Scrubber} from "@mbostock/scrubber"
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