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 = 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"); 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 (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]); 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 } }); } const data = { nodes: [{ id: 'Jonathan' }, { id: 'Judith' }, { id: 'Hela' }, { id: 'Senua' }, ], links: [] } const chart = ForceGraph(data, { nodeId: d => d.id, nodeGroup: d => d.group, nodeTitle: d => `${d.id}\n`, linkStrokeWidth: l => Math.sqrt(l.value), width: 700, height: 400, // invalidation // a promise to stop the simulation when the cell is re-run }) // console.log(chart); // d3.select('#chart').append(chart); document.querySelector('#chart').appendChild(chart);
<div id="chart"></div>