Public
Edited
Dec 9, 2022
Insert cell
Insert cell
{
// Invalidation throws an exception if used outside of observable
return ForceGraph(graph, {
nodeId: (d) => d.id,
nodeGroup: (d) => d.group,
nodeTitle: (d) => `${d.id} (${d.group})`,
width: 680,
height: 680,
invalidation // a promise to stop the simulation when the cell is re-run
});

// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/disjoint-force-directed-graph
function ForceGraph(
{
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{source, target}, …])
},
{
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
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 = 1, // node stroke opacity
nodeRadius = 5, // node radius, 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 = "#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
width = 300, // outer width, in pixels
height = 300, // 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 G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
const W =
typeof linkStrokeWidth !== "function"
? null
: d3.map(links, linkStrokeWidth);

// 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]);
if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
if (linkStrength !== undefined) forceLink.strength(linkStrength);

const simulation = d3
.forceSimulation(nodes)
.force("link", forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.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", linkStroke)
.attr("stroke-opacity", linkStrokeOpacity)
.attr(
"stroke-width",
typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null
)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line");

if (W) link.attr("stroke-width", ({ index: i }) => W[i]);

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")
.attr("r", nodeRadius)
.call(drag(simulation));

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

// Handle invalidation.
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);
}

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
Insert cell
JSON.parse(
'{"nodes": [{"id": "1", "group1": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "2", "group1": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "3", "group1": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "4", "group2": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "5", "group2": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "6", "group2": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "7", "group3": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "8", "group3": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "9", "group3": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "10", "group4": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "11", "group4": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "12", "group4": "Cited", "radius": 2, "citing_patents_count": 2},{"id": "13", "group4": "Cited", "radius": 2, "citing_patents_count": 2}],"links": [{"source": "1", "target": "2", "value": 2}, {"source": "2", "target": "3", "value": 2}, {"source": "3", "target": "6", "value": 2}, {"source": "2", "target": "3", "value": 2}, {"source": "5", "target": "6", "value": 2}, {"source": "7", "target": "8", "value": 2}, {"source": "8", "target": "9", "value": 2}, {"source": "10", "target": "11", "value": 2}, {"source": "3", "target": "5", "value": 2}, {"source": "1", "target": "5", "value": 2} ]}'
)
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