Published
Edited
Oct 5, 2022
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { ForceGraph } from "@d3/force-directed-graph"
Insert cell
ForceGraph({
nodes: items,
links: cooccurrences
}, {
nodeId: d => d.name, // we want to uniquely identify each node
nodeTitle: d => `${d.name}`, // we can provide a title for tooltip
linkSource: d => d.item_a, // we need to tell the ForceGraph how to associate the link source with a node
linkTarget: d => d.item_b, // we need to tell the ForceGraph how to associate the link target with a node
linkStrokeWidth: l => Math.sqrt(l.count / 5), // we can encode the count in the stroke width of the link
width,
invalidation // a promise to stop the simulation when the cell is re-run
})
Insert cell
Insert cell
viewof searchedItems = Inputs.search(items, { query: "campus" }) // let's add a default query so you can see the effect right away
Insert cell
Insert cell
Insert cell
Insert cell
relatedItems = items.filter(d => searchedLinks.find(l => d.name === l.item_a || d.name === l.item_b))
Insert cell
ForceGraph({
nodes: relatedItems,
links: searchedLinks
}, {
nodeId: d => d.name,
nodeGroup: d => !!searchedItems.find(s => s.name === d.name), // we can color the nodes by whether or not they are in the searched items
nodeTitle: d => `${d.name}`,
linkSource: d => d.item_a,
linkTarget: d => d.item_b,
linkStrokeWidth: l => Math.sqrt(l.count),
width,
height: 600,
invalidation // a promise to stop the simulation when the cell is re-run
})
Insert cell
Insert cell
CustomForceGraph({
nodes: relatedItems,
links: searchedLinks
}, {
nodeId: d => d.name,
nodeGroup: d => !!searchedItems.find(s => s.name === d.name),
nodeTitle: d => `${d.name}`,
linkSource: d => d.item_a,
linkTarget: d => d.item_b,
linkStrokeWidth: l => Math.sqrt(l.count),
width,
invalidation // a promise to stop the simulation when the cell is re-run
})
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/force-directed-graph
function CustomForceGraph({
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 = 640, // outer width, in pixels
height = 400, // 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);
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]}));
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("center", d3.forceCenter())
.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");

/*
MODIFICATION
Instead of directly adding circle elements, we append a g (group) element
This will allow us to add a circle and a text element and position them together
*/
const node = svg.append("g")
.attr("fill", nodeFill)
.attr("stroke", nodeStroke)
.attr("stroke-opacity", nodeStrokeOpacity)
.attr("stroke-width", nodeStrokeWidth)
.selectAll("g")
.data(nodes)
.join("g")
.call(drag(simulation))
/*
ADDITION
Here we add event handlers to show and hide the text elements
*/
.on("mouseover", function(d) {
d3.select(this).raise().select("text").style("visibility", "")
})
.on("mouseout", function(d) {
d3.select(this).select("text").style("visibility", "hidden")
})

/*
MODIFICATION
We now have a reference to the circle as well as the node (g element)
*/
const circle = node
.append("circle")
.attr("r", nodeRadius)

if (W) link.attr("stroke-width", ({index: i}) => W[i]);
if (L) link.attr("stroke", ({index: i}) => L[i]);
if (G) circle.attr("fill", ({index: i}) => color(G[i]));
if (T) circle.append("title").text(({index: i}) => T[i]);
/*
ADDITION
Here we add a text element to each node if the user has supplied the title accessor
*/
if (T) node.append("text").text(({index: i}) => T[i])
.attr("stroke", "white")
.attr("fill", "black")
.attr("stroke-width", 3)
.attr("paint-order", "stroke")
.style("pointer-events", "none")
.style("visibility", "hidden")
.attr("dx", 10)
.attr("dy", 5)
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);
/*
MODIFICATION
Instead of modifying the positions of the circles directly
we translate the g elements so the circles and text move together
*/
node
.attr("transform", d => `translate(${d.x}, ${d.y})`)
// .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
Insert cell
Insert cell
import { navigation, previews, notebooks, collection} from "@observablehq/which-items-are-purchased-together"
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