Published
Edited
Jun 15, 2022
5 stars
Insert cell
Insert cell
chart = {
const links = data.links.map(d => Object.create(d)); // read the data and use the links key to create links
const nodes = data.nodes.map(d => Object.create(d)); // read the data and use the nodes key to create nodes

const simulation = d3 // use d3 to simulate physical force
.forceSimulation(nodes) // use the force simulation function from d3 to model how nodes interact
.force("link", d3.forceLink(links).id(d => d.id)) // use the forceLink function to model how links interact
.force("charge", d3.forceManyBody()) // use the ManyBody function to simulate gravity between nodes
.force("center", d3.forceCenter(width / 2, height / 2)) // center the "gravitational pull" in the middle of the visualization
.force("collision", d3.forceCollide(d => d.degree + 2)); // nodes can't collide with each other and have a 2 pixel pad

const svg = d3 // use d3 to create the svg or scaleable vector graphics box to draw the chart on
.create("svg") // make the svg
.attr("height", height) // make the svg as tall as the height variable
.attr("width", width); // make the svg as wide as Observable's default width

const link = svg // draw the links on the svg
.append("g") // make all the links html elements called g
.attr("stroke", "#999") // define the color of the link lines
.attr("stroke-opacity", 0.6) // define the opacity of the link lines
.selectAll("line") // select all line objects
.data(links) // use the links entries from the data to draw the lines
.join("line") // adjust the lines when something moves, like if we drag a node or there's a collission
.attr("stroke-width", d => Math.sqrt(d.value)); // make the links as wide as the square root of the value column (this is a good way to scale things if you have a wide range in values).

const node = svg // draw the nodes on the svg
.append("g") // make all nodes html elements called g
.attr("stroke", "#fff") // define the default color of the nodes
.attr("stroke-width", 1.5) // draw a stroke 1.5px wide around each node
.selectAll("circle") // select all circle objects
.data(nodes) // use the nodes entries in the data to draw the circles
.join("circle") // adjust the nodes when something moves
.attr("r", d => d.degree + 2) // draw each node as a circle with the radius of its degree value plus 2px
.attr("fill", color) // use the color function defined below to assign a color to each modularity class
.call(drag(simulation)); // use the drag function defined below to allow us to click and drag nodes. This can be commented out if you don't want to use the drag function below

const textElems = svg // add text labels to the nodes on the svg
.append('g')
.selectAll('text') // select all text elements
.data(nodes) // use the nodes information to get the text
.join('text') // adjust the labels when something moves
.text(d => d.id) // use the id attribute of the nodes to write the labels
.attr('font-size', 12) // give each label the css font-size attribute of 12 pixels
.call(drag(simulation)); // use the drag function defined below to move the labels when nodes are dragged. This can be commented out if you don't want to use the drag function below

// this is a standard simulation function that is the same across basically all force directed network visualizations in d3. It keeps the sources and targets of each interacting node pair linked even when the graph is moving around because of collisions or dragging. I honestly just copy/paste the same simulation function across all my network graphs instead of rewriting it every time.

// every time the graph changes due to dragging or movement generated by the layout, update the links, nodes, and text
simulation.on("tick", () => {
link // adjust the position of every 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); // adjust the position of the nodes

textElems // adjust the text labels
.attr("x", d => d.x + 10) // draw the label 10px to the left of the center of the node
.attr("y", d => d.y) // but draw it at the same height as the center of the node
.attr("visibility", function(d) {
// add the css visibility attribute to all text labels
if (d.degree >= 10) {
// if the degree value of the node is greater than or equal to 10
return "visible"; // make its label visible using a css attribute
} else {
// if the degree of the node is smaller than 10
return "hidden"; // make its label hidden using a css attribute
}
});
});

return svg.node(); // draw all the crap above
}
Insert cell
Insert cell
data = FileAttachment("graph.json").json() // network data is almost always encoded in
// json format, so we tell Observable to read the data file
// as json
Insert cell
Insert cell
Insert cell
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return d => scale(d.modularity);
}
Insert cell
Insert cell
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
Insert cell
d3 = require("d3@6")
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