Public
Edited
Mar 22, 2023
Insert cell
Insert cell
chart = ForceGraph(radio, {
nodeId: d => d.id,
nodeGroup: d => d.group,
nodeTitle: d => `${d.id}`,
nodeLink: d => d.link,
linkStrokeWidth: l => 1.5**l.value,
width:1500,
height: 1000,
invalidation
})
Insert cell
radio = FileAttachment("network@19.json").json()
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/force-directed-graph

function ForceGraph({
nodes,
links
},{
nodeId = d => d.id, // 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
nodeLink = d => d.link,
nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
nodeStroke = "#fff", // node stroke color
nodeStrokeWidth = 1.5, // node stroke width, in pixels
nodeStrokeOpacity = 5, // node stroke opacity
nodeRadius = 6, // node radius, in pixels
nodeStrength = -1550, // increase this number to make the nodes less attracted to each other
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 = "#999", // link stroke color
linkStrokeOpacity = 0.6, // link stroke opacity
linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
linkStrokeLinecap = "round", // link stroke linecap
linkStrength,
colors = d3.schemeTableau10, // an array of color strings, for the node groups
font_sizes = [24,14,16],
font_thickness = [700,100,500],
width = 640, // outer width, in pixels
height = 800, // outer height, in pixels
invalidation // when this promise resolves, stop the simulation
} = {}) {

// Compute values.
const N = d3.map(nodes, nodeId).map(intern);
const U = d3.map(nodes, nodeLink).map(intern);
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);

// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i], link: U[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);
const font_size = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, font_sizes);
const font_weight = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, font_thickness);
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
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())
.on("tick", ticked);

const zoom = d3.zoom()
.translateExtent([[-1.2*width,-1.2*height], [1.2*width, 1.2*height]])
.scaleExtent([.5, 1.2])
.on("zoom", function () {
link.attr("transform", d3.zoomTransform(this))
node.attr("transform", d3.zoomTransform(this))
});
// creating the viewbox within which nodes and links are contained
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic; background-color: rgba(123, 124, 126,1);")
.call(zoom)

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");
// g SVG element is a container for grouping other SVG elements
const node = svg.append("g")
.selectAll("text")
.data(nodes)
.join("text")
.on("click", function(d,i) {
window.open(
i['link'],'_blank'
);
})
.text(nodeTitle)
.style("fill", '#ffffff')
.call(drag(simulation))
.on("mouseenter", (evt, d) => {
var links = {}
link
.attr("stroke-opacity", 0.1)
.style("fill", '#ffffff')
.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-opacity", 0.6)
.style("fill", '#ffffff')
node
.style("opacity",0.1)
.style("fill", '#ffffff')
.filter(n => n.id === d.id || isConnected(n.id,d.id))
.style("opacity",0.6)
.style("fill", '#ffffff')
})
.on("mouseleave", evt => {
link.attr("display", "block")
.attr("stroke-opacity", 0.6)
.style("fill", '#ffffff')
node
.style("opacity",1)
})
svg
.call(zoom.scaleBy, 0.5)
.call(zoom.translateBy, 450, 250)
.attr("overscroll-behavior", "none");

if (W) link.attr("stroke-width", ({index: i}) => W[i]);
if (L) link.attr("stroke", ({index: i}) => L[i]);
if (G) node.style("font-size", ({index: i}) => font_size(G[i]));
if (G) node.attr("font-weight", ({index: i}) => font_weight(G[i]));
if (invalidation != null) invalidation.then(() => simulation.stop());

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

function isConnected(a,b) {
for (let i = 0; i < links.length; i++) {
if (links[i].source.id === a && links[i].target.id === b) {
return 1;
}
else if (links[i].source.id === b && links[i].target.id === a) {
return 1;
}
}
}
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("x", d => d.x)
.attr("y", d => d.y);
}

function linkArc(d) {
const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
return `
M${d.source.x},${d.source.y}
A${r},${r} 0 0,1 ${d.target.x},${d.target.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;
link
.attr("stroke-opacity", 0.1)
.filter(l => l.source.id === event.subject.id || l.target.id === event.subject.id)
.attr("stroke-opacity", 0.6)
node
.style("opacity",0.1)
.filter(n => n.id === event.subject.id || isConnected(n.id,event.subject.id))
.style("opacity",0.6)
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
link.attr("display", "block")
.attr("stroke-opacity", 0.6);
node
.style("opacity",1)
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}

return Object.assign(svg.node(), {scales: {color}});
}
Insert cell
d3.schemeTableau10
Insert cell
import {howto} from "@d3/example-components"
Insert cell
import {Swatches} from "@d3/color-legend"
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