Unlisted
Edited
Feb 6, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
scaleSize = d3
.scaleLinear()
.domain([1, getMaxCount(nodeData)])
.range([40, getMaxCount(nodeData) / 50])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
linkData = FileAttachment("accent_edges@2.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/force-directed-graph

// changes made by Kathy Reid as indicated
function ForceGraph(
{
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{source, target}, …])
},
{
nodeId, // given d in nodes, returns a unique identifier (string)
nodeGroup, // given d in nodes, returns an (ordinal) value for color
nodeGroups, // an array of ordinal values representing the node groups
nodeTitle, // given d in nodes, a title string
nodeLabel, // given d in nodes, a title for the label
nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke, // given d in nodes, a Boolean to indicate if the node stroke should be differentiated
nodeStrokeWidth = 12, // node stroke width, in pixels
nodeStrokeOpacity = 1, // node stroke opacity
nodeRadius, // node radius, in pixels
collisionOffset, // how much to offset node overlap by, in pixels
labelOffset, // how much to offset label placement, in pixels
nodeStrength,
linkSource = ({ source }) => source, // given d in links, returns a node identifier string
linkTarget = ({ target }) => target, // given d in links, returns a node identifier string
linkStroke = "#222", // link stroke color
linkStrokeOpacity = 0.7, // link stroke opacity
linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap = "round", // link stroke linecap
linkStrength,
colors = colourArray, // an array of color strings, for the node groups
width = 3000, // outer width, in pixels
height = 4000, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}
) {
// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const B = nodeLabel == null ? null : d3.map(nodes, nodeLabel);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);

const R = nodeRadius == null ? null : d3.map(nodes, nodeRadius).map(intern);
const W =
typeof linkStrokeWidth !== "function"
? null
: d3.map(links, linkStrokeWidth);
const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);
const S = nodeStroke == null ? null : d3.map(nodes, nodeStroke);

// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({ id: N[i] }));
links = d3.map(links, (_, i) => ({ source: LS[i], target: LT[i] }));

// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);

// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);

// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({ index: i }) => N[i]);

// Amendment by Kathy Reid to prevent the circles from overlapping
const forceCollide = d3
.forceCollide()
.radius(({ index: i }) => R[i] + collisionOffset);

if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);

const simulation = d3
.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", forceNode)
.force("center", d3.forceCenter())
.force("collision", forceCollide)
.on("tick", ticked);

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

const link = svg
.append("g")
.attr("stroke", typeof linkStroke !== "function" ? linkStroke : null)
.attr("stroke-opacity", linkStrokeOpacity)
.attr(
"stroke-width",
typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null
)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line");

const node = svg
.append("g")
.attr("fill", nodeFill)
//.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("circle")
.data(nodes)
.join("circle")
.call(drag(simulation))

// isolate the links of a node for easier exploration
.on("mouseenter", (evt, d) => {
link
.attr("display", "none")
.filter((l) => l.source.id === d.id || l.target.id === d.id)
.attr("display", "block");
})
.on("mouseleave", (evt) => {
link.attr("display", "block");
});

// labels for the nodes
const label = svg
.append("g")
.selectAll("text")
.data(nodes)
.enter()
.append("text")
.text(({ index: i }) => T[i])
.attr("class", "labels")
.attr("text-anchor", "middle")
.attr("dx", 0)
.attr("y", ({ index: i }) => R[i] + labelOffset);

if (W) link.attr("stroke-width", ({ index: i }) => W[i]);
if (L) link.attr("stroke", ({ index: i }) => L[i]);
if (G) node.attr("fill", ({ index: i }) => color(G[i]));
if (T) node.append("title").text(({ index: i }) => T[i]);

// use the predetermined attribute of a node to determine stroke colour
if (S)
node.attr("stroke", ({ index: i }) => {
if (S[i]) {
return "#000000";
} else {
return "#fefefe";
}
});
if (B) label.text(({ index: i }) => B[i]);
if (R) node.attr("r", ({ index: i }) => R[i]);

if (invalidation != null) invalidation.then(() => simulation.stop());

function intern(value) {
return value !== null && typeof value === "object"
? value.valueOf()
: value;
}

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);
label.attr("dx", (d) => d.x).attr("dy", (d) => d.y);
}

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

return Object.assign(svg.node(), { scales: { color } });
}
Insert cell
import {howto} from "@d3/example-components"
Insert cell
import {Swatches} from "@d3/color-legend"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more