Skip to content

Instantly share code, notes, and snippets.

@sagarpanchal
Last active October 27, 2023 13:48
Show Gist options
  • Save sagarpanchal/d186f5f75331d88e037c2e503f7437d4 to your computer and use it in GitHub Desktop.
Save sagarpanchal/d186f5f75331d88e037c2e503f7437d4 to your computer and use it in GitHub Desktop.
GraphViz
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="author" content="Sagar Panchal | sagar.panchal@outlook.com" />
<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