Last active
October 27, 2023 13:48
-
-
Save sagarpanchal/d186f5f75331d88e037c2e503f7437d4 to your computer and use it in GitHub Desktop.
GraphViz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="author" content="Sagar Panchal | [email protected]" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Graph</title> | |
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml," /> | |
<script src="https://unpkg.com/@hpcc-js/wasm@2/dist/graphviz.umd.js" type="javascript/worker"></script> | |
<script src="https://unpkg.com/d3/dist/d3.min.js"></script> | |
<script src="https://unpkg.com/d3-graphviz@5/build/d3-graphviz.min.js"></script> | |
</head> | |
<body> | |
<div id="graph" style="text-align: center"></div> | |
<script type="module"> | |
const selectedNodeList = [] | |
const selectedNodeFillList = [] | |
const selectedNodeStrokeList = [] | |
const selectedNodeStrokeWidthList = [] | |
const selectedEdgeList = [] | |
const selectedEdgeFillList = [] | |
const selectedEdgeStrokeList = [] | |
const selectedEdgeStrokeWidthList = [] | |
const graphviz = d3.select("#graph").graphviz().height(window.innerHeight).width(window.innerWidth).fit(false) | |
const graphObject = await fetch(new Request("./output.json")).then((response) => response.json()) | |
const dotSource = graphToDot(graphObject) | |
console.info(dotSource) | |
render() | |
function graphToDot(graph, options = { splines: "ortho" }) { | |
let dot = [ | |
"digraph Nodes {", | |
" layout = dot;", | |
" rankdir = TB;", | |
" remincross = true;", | |
" compound = true;", | |
" beautify = true;", | |
` splines = ${options.splines};`, | |
" node[shape = box, style = square];", | |
" edge[style = solid, constraint = true];", | |
].join("\n") | |
for (const node of graph.nodes) { | |
dot += `\n ${JSON.stringify(node.v)} [label = ${JSON.stringify(`${node.v} [${node.value?.count ?? 0}]`)}];` | |
} | |
for (const edge of graph.edges) { | |
dot += `\n ${JSON.stringify(edge.v)} -> ${JSON.stringify(edge.w)};` | |
} | |
dot += "\n}" | |
return dot | |
} | |
function render() { | |
graphviz.renderDot(dotSource, startApp) | |
} | |
function startApp() { | |
const nodes = d3.selectAll(".node") | |
const edges = d3.selectAll(".edge") | |
window.nodes = nodes | |
window.edges = edges | |
// click and mousedown on nodes | |
nodes.on("click mousedown", function (event) { | |
if (!(event.ctrlKey || event.metaKey)) return | |
event.preventDefault() | |
event.stopPropagation() | |
console.info("node click or mousedown", event) | |
unSelectNodes() | |
unSelectEdges() | |
selectNodes([d3.select(this)], { highlightNearest: true }) | |
}) | |
// click and mousedown on edges | |
edges.on("click mousedown", function (event) { | |
if (!(event.ctrlKey || event.metaKey)) return | |
event.preventDefault() | |
event.stopPropagation() | |
console.info("edge click or mousedown", event) | |
unSelectNodes() | |
unSelectEdges() | |
selectEdges([d3.select(this)]) | |
}) | |
// right-click outside of nodes | |
d3.select(document).on("contextmenu", (event) => { | |
if (!(event.ctrlKey || event.metaKey)) return | |
event.preventDefault() | |
event.stopPropagation() | |
console.info("document contextmenu", event) | |
unSelectEdges() | |
unSelectNodes() | |
}) | |
function findNodesFromEdge(edge) { | |
const nodeNameList = edge.datum().key.split("->") | |
const nodeList = [] | |
for (const nodeName of nodeNameList) { | |
const nodeIndex = graphObject.nodes.findIndex((node) => node.v === nodeName.trim()) + 1 | |
if (nodeIndex < 1) continue | |
const node = d3.select(`#node${nodeIndex}`) | |
if (!node) continue | |
nodeList.push(node) | |
} | |
// [startNode, endNode] | |
return nodeList | |
} | |
function findOutgoingEdgesFromNode(node) { | |
const nodeName = node.datum().key | |
const output = graphObject.edges | |
.map((edge, index) => (edge.v === nodeName && edge.w !== nodeName ? d3.select(`#edge${index + 1}`) : undefined)) | |
.filter((node) => node !== undefined) | |
return output | |
} | |
function findSelfReferencingEdgeFromNode(node) { | |
const nodeName = node.datum().key | |
const edgeIndex = graphObject.edges.findIndex((edge, index) => edge.v === nodeName && edge.w === nodeName) | |
const output = edgeIndex > -1 ? d3.select(`#edge${edgeIndex + 1}`) : undefined | |
return output | |
} | |
function findIncomingEdgesFromNode(node) { | |
const nodeName = node.datum().key | |
const output = graphObject.edges | |
.map((edge, index) => (edge.v !== nodeName && edge.w === nodeName ? d3.select(`#edge${index + 1}`) : undefined)) | |
.filter((node) => node !== undefined) | |
return output | |
} | |
function selectNodes(nodeList, _options = {}) { | |
const options = { fill: "#229BFE", stroke: "#229BFE", strokeWidth: "2", highlightNearest: false, ..._options } | |
for (const node of nodeList) { | |
if (!node || selectedNodeList.includes(node)) continue | |
selectedNodeList.push(node) | |
selectedNodeFillList.push(node.selectAll("polygon, ellipse").attr("fill")) | |
selectedNodeStrokeList.push(node.selectAll("polygon, ellipse").attr("stroke")) | |
selectedNodeStrokeWidthList.push(node.selectAll("polygon").attr("stroke-width")) | |
node.selectAll("polygon, ellipse").attr("fill", options.fill) | |
node.selectAll("polygon, ellipse").attr("stroke", options.stroke) | |
node.selectAll("polygon").attr("stroke-width", options.strokeWidth) | |
if (options.highlightNearest) { | |
selectEdges(findIncomingEdgesFromNode(node), { | |
fill: "#15FE09", | |
stroke: "#15FE09", | |
ignoreEndNode: true, | |
}) | |
selectEdges([findSelfReferencingEdgeFromNode(node)], { | |
fill: "#229BFE", | |
stroke: "#229BFE", | |
ignoreStartNode: true, | |
ignoreEndNode: true, | |
}) | |
selectEdges(findOutgoingEdgesFromNode(node), { | |
fill: "#FE3CA6", | |
stroke: "#FE3CA6", | |
ignoreStartNode: true, | |
}) | |
} | |
} | |
} | |
function selectEdges(edgeList, _options = {}) { | |
const options = { | |
fill: "#FE3CA6", | |
stroke: "#FE3CA6", | |
strokeWidth: "2", | |
ignoreStartNode: false, | |
ignoreEndNode: false, | |
..._options, | |
} | |
for (const edge of edgeList) { | |
if (!edge || selectedEdgeList.includes(edge)) continue | |
selectedEdgeList.push(edge) | |
selectedEdgeFillList.push(edge.selectAll("polygon").attr("fill")) | |
selectedEdgeStrokeList.push(edge.selectAll("polygon, path").attr("stroke")) | |
selectedEdgeStrokeWidthList.push(edge.selectAll("path").attr("stroke-width")) | |
edge.selectAll("polygon").attr("fill", options.fill) | |
edge.selectAll("polygon, path").attr("stroke", options.stroke) | |
edge.selectAll("path").attr("stroke-width", options.strokeWidth) | |
if (!options.ignoreStartNode || !options.ignoreEndNode) { | |
const [startNode, endNode] = findNodesFromEdge(edge) | |
if (startNode && !options.ignoreStartNode) selectNodes([startNode], { ...options, highlightNearest: false }) | |
if (endNode && !options.ignoreEndNode) selectNodes([endNode], { ...options, highlightNearest: false }) | |
} | |
} | |
} | |
function unSelectNodes() { | |
while (selectedNodeList.length > 0) { | |
const node = selectedNodeList.pop() | |
const selectedNodeFill = selectedNodeFillList.pop() | |
const selectedNodeStroke = selectedNodeStrokeList.pop() | |
const selectedNodeStrokeWidth = selectedNodeStrokeWidthList.pop() | |
if (!node) continue | |
node.selectAll("polygon, ellipse").attr("fill", selectedNodeFill) | |
node.selectAll("polygon, ellipse").attr("stroke", selectedNodeStroke) | |
node.selectAll("polygon").attr("stroke-width", selectedNodeStrokeWidth) | |
} | |
} | |
function unSelectEdges() { | |
while (selectedEdgeList.length > 0) { | |
const edge = selectedEdgeList.pop() | |
const selectedEdgeFill = selectedEdgeFillList.pop() | |
const selectedEdgeStroke = selectedEdgeStrokeList.pop() | |
const selectedEdgeStrokeWidth = selectedEdgeStrokeWidthList.pop() | |
if (!edge) continue | |
edge.selectAll("polygon").attr("fill", selectedEdgeFill) | |
edge.selectAll("polygon, path").attr("stroke", selectedEdgeStroke) | |
edge.selectAll("path").attr("stroke-width", selectedEdgeStrokeWidth) | |
} | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment