puudot/code/graph.py
Lauri Koskenniemi 93e2554457 Improved links and configuration file for dot
Restructured data to support block and node level links.
Added hidden nodes and edges in blocks to improve link
visualization.

Introduced support for configuring dot file parameters with
a config file.
2025-05-30 22:54:36 +03:00

149 lines
4.4 KiB
Python

import os
import uuid
# TODO:
# Add hidden nodes in each cluster above visible nodes connected by hidden edges and attach visible edges to them
# - also see: https://stackoverflow.com/questions/53862417/how-to-set-head-and-tail-position-in-nodes-graphviz
# Solve layering of clusters
# - https://observablehq.com/@gordonsmith/church
def make_header(config):
return "graph {\n" + config["graph"] + "\n"
def make_footer():
return "}\n"
def make_node_line(node, config):
line = f"{node['id']} [label=\"{node['text']}\"\n"
if node['hidden']:
line += config["node"]["hidden"]
else:
line += config["node"]["text"]
return line + "]\n"
def make_link_line(link, config):
configs = []
if link['head'] != "":
configs.append(f"lhead=cluster_{link['head']}")
if link['hidden']:
configs.append(config["edge"]["hidden"])
else:
configs.append(config["edge"]["default"])
line = f"{link['from']} -- {link['to']}"
if len(configs) > 0:
line += " [" + ", ".join(configs) + "]"
return line + "\n"
def make_block_line(block, config):
line = f"subgraph cluster_{block['id']} {{\n{config["subgraph"]}\nlabel=\"{block['label']}\"\n"
for node in block["texts"]:
line += f"{node}\n"
return line + "}\n\n"
def add_nodes(nodes, config):
line = ""
for node in nodes:
line += make_node_line(node, config)
return line
def add_links(links, config):
line = ""
for link in links:
line += make_link_line(link, config)
return line
def add_blocks(blocks, config):
line = ""
for block in blocks:
line += make_block_line(block, config)
return line
def get_id():
return "id" + str(uuid.uuid4().hex)
class Graph:
def __init__(self):
self.config = {}
self.layers = {}
self.blocks = []
self.nodes = []
self.links = []
self.dot_file = ""
def __str__(self):
return self.dot_file
def set_config(self, config):
dot = config.get("dot", {})
if dot != {}:
dot["graph"] = dot.get("graph", "")
dot["subgraph"] = dot.get("subgraph", "")
dot["node"] = dot.get("node", {})
dot["node"]["text"] = dot["node"].get("text", "")
dot["node"]["hidden"] = dot["node"].get("hidden", "")
dot["edge"] = dot.get("edge", {})
dot["edge"]["default"] = dot["edge"].get("default", "")
dot["edge"]["hidden"] = dot["edge"].get("hidden", "")
self.config = dot
def process_blocks(self, data):
blocks = data.get('blocks', [])
linker = {}
for block in blocks:
block_id = get_id()
new_block = {
"id": block_id,
"label": block.get("label", ""),
"texts": []
}
hidden_node_id = get_id()
self.nodes.append({"id": hidden_node_id, "text": "", "hidden": True})
new_block["texts"].append(hidden_node_id)
links = block.get("links", [])
for link in links:
if link in linker:
self.links.append({"from": linker[link], "to": hidden_node_id, "head": block_id, "hidden": False})
del linker[link]
for text in block.get("texts", []):
node_id = get_id()
self.nodes.append({"id": node_id, "text": text.get("text", ""), "hidden": False})
self.links.append({"from": hidden_node_id, "to": node_id, "head": "", "hidden": True})
new_block["texts"].append(node_id)
links = text.get("links", [])
for link in links:
linker[link] = node_id
self.blocks.append(new_block)
def build_dot(self):
self.dot_file = make_header(self.config)
self.dot_file += add_nodes(self.nodes, self.config)
self.dot_file += add_blocks(self.blocks, self.config)
self.dot_file += add_links(self.links, self.config)
self.dot_file += make_footer()
def make_dot(self, format="svg", dot_file="dot.gv", svg_file="graph.svg"):
if self.dot_file != "":
with open(dot_file, "w") as f:
f.write(self.dot_file)
if format == "svg":
os.system(f"dot -T{format} {dot_file} -o {svg_file}")