<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Graph Explorer</title> <script src="https://d3js.org/d3.v7.min.js"></script> <script type='text/javascript'> var NODE_GRAPH=<!-- REPLACE_THIS_WITH_GRAPH -->; </script> <style> text { font-family: sans-serif; font-size: 10px; } </style> <script> // 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 = 640, // outer width, in pixels height = 400, // outer height, in pixels invalidation, // when this promise resolves, stop the simulation linkLabel = null, } = {}) { // 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;"); // Add arrowheads svg.append('defs').append('marker') .attr('id', 'arrowhead') .attr('viewBox', '-0 -2.5 5 5') .attr('refX', 10) .attr('refY', 0) .attr('orient', 'auto') .attr('markerWidth', 7) .attr('markerHeight', 7) .attr('xoverflow', 'visible') .append('svg:path') .attr('d', 'M 0,-2.5 L 5 ,0 L 0,2.5') .attr('fill', '#999') .style('stroke','none'); const link = svg.append("g") .attr("stroke", linkStroke) .attr("stroke-opacity", linkStrokeOpacity) .attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null) .attr("stroke-linecap", linkStrokeLinecap) .attr('marker-end','url(#arrowhead)') .selectAll("line") .data(links) .join("line"); let edgelabels = null; let edgepaths = null; if (linkLabel) { edgepaths = svg.selectAll(".edgepath") .data(links) .enter() .append('path') .attr('class', 'edgepath') .attr('fill-opacity', 0) .attr('stroke-opacity', 0) .attr('id', function (d, i) {return 'edgepath' + i}) .style("pointer-events", "none"); edgelabels = svg.selectAll(".edgelabel") .data(links) .enter() .append('text') .style("pointer-events", "none") .attr('class', 'edgelabel') .attr('id', function (d, i) {return 'edgelabel' + i}) .attr('font-size', 10) .attr('fill', '#666'); edgelabels.append('textPath') .attr('xlink:href', function (d, i) {return '#edgepath' + i}) .style("text-anchor", "middle") .style("pointer-events", "none") .attr("startOffset", "50%") .text(linkLabel); } 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); if (linkLabel) { edgepaths.attr('d', function (d) { return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; }); edgelabels.attr('transform', function (d) { if (d.target.x < d.source.x) { var bbox = this.getBBox(); rx = bbox.x + bbox.width / 2; ry = bbox.y + bbox.height / 2; return 'rotate(180 ' + rx + ' ' + ry + ')'; } else { return 'rotate(0)'; } }); } } 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}}); } </script> <style> body, html { padding: 0; margin: 0; } #graph_holder { padding: 0; margin: 0; width: 100vw; height: 100vh; } </style> </head> <body> <div id="graph_holder"> </div> <script> var edges = []; for (var source of Object.keys(NODE_GRAPH)) { for (var target of NODE_GRAPH[source].links) { var node_id = target.target; if (node_id.startsWith('id:')) { node_id = node_id.substring(3); } if (NODE_GRAPH[node_id] === undefined) { continue; } edges.push({source, target: node_id, relation: target.relation}); } } var graph = { nodes: Object.keys(NODE_GRAPH), links: edges, }; var holder = document.getElementById('graph_holder'); var chart = ForceGraph(graph, { nodeId: d => d, nodeGroup: d => NODE_GRAPH[d].depth, nodeTitle: d => `${NODE_GRAPH[d].title}`, nodeRadius: d => 3 + Math.max(1, 4 - NODE_GRAPH[d.id].depth) * 2, width: holder.clientWidth, height: holder.clientHeight, linkStrokeWidth: 1, // linkStrength: -5, // invalidation, // a promise to stop the simulation when the cell is re-run linkLabel: (d) => { const e = edges[d.index]; if (e.relation) { return e.relation; } else { return ''; } }, }); holder.appendChild(chart); chart.height = '100vh'; chart.width = '100vw'; </script> </body> </html>