Public
Edited
Sep 6, 2023
Insert cell
Insert cell
nodes.json
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
links.json
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
nodes.json
X
count
Y
count
group
Color
#1eb8d0
Size
Facet X
Facet Y
Mark
Auto
Type Chart, then Shift-Enter. Ctrl-space for more options.

Insert cell
Plot.plot({
marks: [
Plot.rectY(
await FileAttachment("nodes.json").json(),
Plot.binX(
{ y: "count" },
{ x: "count", y: "group", fill: "#1eb8d0", tip: true }
)
),
Plot.ruleY([0])
],
x: {
domain: [0, 20]
}
})
Insert cell
nodes = await FileAttachment("nodes.json").json()
Insert cell
Insert cell
nodes.find(node => node.id === 5)
Insert cell
{
const nodes = await FileAttachment("nodes.json").json();
const links = await FileAttachment("links.json").json();
// nodes.forEach((n, i) => {
// // if (!n.id) n.id = n.scientificName;
// // if (n.id) n.id = n.id.toString();
// n.id = i;
// })
// links.forEach(n => {
// n.sourceId = n.sourceId.toString()
// n.targetId = n.targetId.toString()
// })
debugger;
const width = 800;
const height = 800;
// const nodes = [{}, {}, {}, {}, {}];
// const links = [
// { source: 0, target: 1, type: "herbivour" },
// { source: 1, target: 2, type: "carnivour" },
// { source: 3, target: 1, type: "herbivour" },
// { source: 2, target: 0 }
// ];
// nodes.forEach((n, i) => {
// n.count = random(0, 100);
// n.id = i;
// });
let selectedId;
const quadtree = d3
.quadtree()
.x((d) => d.x)
.y((d) => d.y);
const drag = d3
.drag()
.on("start", handleDragStart)
.on("drag", handleDrag)
.on("end", handleDragEnd);
const sqrtScale = d3
.scaleSqrt()
.domain(d3.extent(nodes, (d) => d.count))
.range([5, 25]);
const ordinalScale = d3
.scaleOrdinal()
.domain(["herbivour", "carnivour"])
.range(["#008000", "#FF0000"])
.unknown("#ccc");
const svg = d3
.create('svg')
.attr("width", width)
.attr("height", height);
svg.append("g").classed("nodes", true);
svg.append("g").classed("links", true);
svg
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 3);

console.log(links)
const simulation = d3
.forceSimulation(nodes)
.force("center", d3.forceCenter(width / 2, height / 2))
// .force("charge", d3.forceManyBody().strength(-30))
.force("link", d3.forceLink().links(links).id(d => d.id))
.force('collide', d3.forceCollide(d => sqrtScale(d.count)))
.on("tick", ticked);
function updateNodes() {
svg
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", (d) => sqrtScale(d.count))
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("fill", (d) => (d.id === selectedId ? "#FF0000" : "black"))
.raise()
.call(drag);
}
function updateLinks() {
svg
.selectAll("line")
.data(links)
.join("line")
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
})
.attr("stroke", (d) => ordinalScale(d.type))
.attr("stroke-width", "3");
}
function ticked() {
updateLinks();
updateNodes();
updateQuadTree();
}
function initEvents() {
svg.on("mousemove", handleMousemove);
}
function handleMousemove(e) {
let pos = d3.pointer(e, this);
let d = quadtree.find(pos[0], pos[1], 20);
selectedId = d ? d.id : undefined;
updateNodes();
}
function updateQuadTree() {
quadtree.addAll(nodes);
}
function random(l, h) {
return l + Math.floor(Math.random() * h - l);
}
function handleDragStart(e) {
if (!e.active) simulation.alphaTarget(0.3).restart();
e.subject.fx = e.subject.x;
e.subject.fy = e.subject.y;
}
function handleDragEnd(e) {
if (!e.active) simulation.alphaTarget(0);
e.subject.fx = null;
e.subject.fy = null;
}
function handleDrag(e) {
e.subject.fx = e.x;
e.subject.fy = e.y;
ticked();
}
initEvents();

invalidation.then(() => simulation.stop());
return svg.node();
}
Insert cell
chart = {
// Specify the dimensions of the chart.
const width = 928;
const height = 600;

// Specify the color scale.
const color = d3.scaleOrdinal(d3.schemeCategory10);

// The force simulation mutates links and nodes, so create a copy
// so that re-evaluating this cell produces the same result.
const links = data.links.map(d => ({...d}));
const nodes = data.nodes.map(d => ({...d}));

// Create a simulation with several forces.
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);

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

// Add a line for each link, and a circle for each node.
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll()
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value));

const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", d => color(d.group));

node.append("title")
.text(d => d.id);

// Add a drag behavior.
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));

// Set the position attributes of links and nodes each time the simulation ticks.
function ticked() {
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);
}

// Reheat the simulation when drag starts, and fix the subject position.
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

// Update the subject (dragged node) position during drag.
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

// Restore the target alpha so the simulation cools after dragging ends.
// Unfix the subject position now that it’s no longer being dragged.
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

// When this cell is re-run, stop the previous simulation. (This doesn’t
// really matter since the target alpha is zero and the simulation will
// stop naturally, but it’s a good practice.)
invalidation.then(() => simulation.stop());

return svg.node();
}
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