2022-05-15 22:06:37 +00:00
|
|
|
<!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>
|
2022-08-19 17:36:32 +00:00
|
|
|
<style>
|
|
|
|
text {
|
|
|
|
font-family: sans-serif;
|
|
|
|
font-size: 10px;
|
|
|
|
}
|
|
|
|
</style>
|
2022-05-15 22:06:37 +00:00
|
|
|
<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
|
2022-05-16 21:28:43 +00:00
|
|
|
invalidation, // when this promise resolves, stop the simulation
|
|
|
|
linkLabel = null,
|
2022-05-15 22:06:37 +00:00
|
|
|
} = {}) {
|
|
|
|
// 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;");
|
|
|
|
|
2022-05-16 21:28:43 +00:00
|
|
|
|
|
|
|
// Add arrowheads
|
|
|
|
svg.append('defs').append('marker')
|
|
|
|
.attr('id', 'arrowhead')
|
|
|
|
.attr('viewBox', '-0 -5 10 10')
|
|
|
|
.attr('refX', 13)
|
|
|
|
.attr('refY', 0)
|
|
|
|
.attr('orient', 'auto')
|
|
|
|
.attr('markerWidth', 13)
|
|
|
|
.attr('markerHeight', 13)
|
|
|
|
.attr('xoverflow', 'visible')
|
|
|
|
.append('svg:path')
|
|
|
|
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
|
|
|
|
.attr('fill', '#999')
|
|
|
|
.style('stroke','none');
|
|
|
|
|
|
|
|
const link = svg.append("g")
|
2022-05-15 22:06:37 +00:00
|
|
|
.attr("stroke", linkStroke)
|
|
|
|
.attr("stroke-opacity", linkStrokeOpacity)
|
|
|
|
.attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null)
|
|
|
|
.attr("stroke-linecap", linkStrokeLinecap)
|
2022-05-16 21:28:43 +00:00
|
|
|
.attr('marker-end','url(#arrowhead)')
|
2022-05-15 22:06:37 +00:00
|
|
|
.selectAll("line")
|
|
|
|
.data(links)
|
|
|
|
.join("line");
|
|
|
|
|
2022-05-16 21:28:43 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-05-15 22:06:37 +00:00
|
|
|
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);
|
2022-05-16 21:28:43 +00:00
|
|
|
|
|
|
|
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)';
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-05-15 22:06:37 +00:00
|
|
|
}
|
|
|
|
|
2022-05-16 21:28:43 +00:00
|
|
|
function drag(simulation) {
|
2022-05-15 22:06:37 +00:00
|
|
|
function dragstarted(event) {
|
|
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
|
|
event.subject.fx = event.subject.x;
|
|
|
|
event.subject.fy = event.subject.y;
|
|
|
|
}
|
2022-05-16 21:28:43 +00:00
|
|
|
|
2022-05-15 22:06:37 +00:00
|
|
|
function dragged(event) {
|
|
|
|
event.subject.fx = event.x;
|
|
|
|
event.subject.fy = event.y;
|
|
|
|
}
|
2022-05-16 21:28:43 +00:00
|
|
|
|
2022-05-15 22:06:37 +00:00
|
|
|
function dragended(event) {
|
|
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
|
|
event.subject.fx = null;
|
|
|
|
event.subject.fy = null;
|
|
|
|
}
|
2022-05-16 21:28:43 +00:00
|
|
|
|
2022-05-15 22:06:37 +00:00
|
|
|
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;
|
2022-05-16 21:28:43 +00:00
|
|
|
|
2022-05-15 22:06:37 +00:00
|
|
|
if (node_id.startsWith('id:')) {
|
|
|
|
node_id = node_id.substring(3);
|
|
|
|
}
|
|
|
|
if (NODE_GRAPH[node_id] === undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-05-16 21:28:43 +00:00
|
|
|
edges.push({source, target: node_id, relation: target.relation});
|
2022-05-15 22:06:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
var graph = {
|
|
|
|
nodes: Object.keys(NODE_GRAPH),
|
|
|
|
links: edges,
|
|
|
|
};
|
|
|
|
var holder = document.getElementById('graph_holder');
|
|
|
|
var chart = ForceGraph(graph, {
|
|
|
|
nodeId: d => d,
|
2022-05-16 21:28:43 +00:00
|
|
|
nodeGroup: d => NODE_GRAPH[d].depth,
|
2022-05-15 22:06:37 +00:00
|
|
|
nodeTitle: d => `${NODE_GRAPH[d].title}`,
|
2022-05-16 21:28:43 +00:00
|
|
|
nodeRadius: d => 3 + Math.max(1, 4 - NODE_GRAPH[d.id].depth) * 2,
|
2022-05-15 22:06:37 +00:00
|
|
|
width: holder.clientWidth,
|
|
|
|
height: holder.clientHeight,
|
2022-05-16 21:28:43 +00:00
|
|
|
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 ''; } },
|
2022-05-15 22:06:37 +00:00
|
|
|
});
|
|
|
|
holder.appendChild(chart);
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
|