import math 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 # https://candide-guevara.github.io/cs_related/2019/09/10/graphviz-examples.html # https://forum.graphviz.org/t/set-nodes-from-left-to-right-and-other-from-top-to-bottom-on-the-same-rank/1860 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): configs = [] if block['hidden']: configs.append(config["subgraph"]["hidden"]) else: configs.append(config["subgraph"]["block"]) config_line = "\n".join(configs) line = f"subgraph cluster_{block['id']} {{\n{config_line}\nlabel=\"{block['label']}\"\n" for node in block["nodes"]: line += f"{node}\n" #if block['hidden']: #line += "{rank=same\nedge [constraint=false]\n" #line += f"{block['nodes'][0]} -- {block['nodes'][1]}\n" #line += "}" 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.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["subgraph"]["block"] = dot["subgraph"].get("block", "") dot["subgraph"]["hidden"] = dot["subgraph"].get("hidden", "") 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"]["middle"] = dot["edge"].get("middle", "") 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", ""), "nodes": [], "hidden": False } texts_in_block = block.get("texts", []) for i, text in enumerate(texts_in_block): node_id = get_id() # Link to middle node if i == math.ceil(len(texts_in_block) / 2) - 1: for link in block.get("links", []): if link in linker: """link_node1 = get_id() link_node2 = get_id() self.nodes.append({"id": link_node1, "text": "", "hidden": True}) self.nodes.append({"id": link_node2, "text": "", "hidden": True}) link_block_id = get_id() self.blocks.append({"id": link_block_id, "label": "", "nodes": [link_node1, link_node2], "hidden": True}) self.links.append({"from": linker[link], "to": link_node1, "head": "", "hidden": False}) #self.links.append({"from": link_node1, "to": link_node2, "head": "", "hidden": False}) self.links.append({"from": link_node2, "to": node_id, "head": block_id, "hidden": False})""" self.links.append({"from": linker[link], "to": node_id, "head": block_id, "hidden": False}) del linker[link] self.nodes.append({"id": node_id, "text": text.get("text", ""), "hidden": False}) new_block["nodes"].append(node_id) for link in text.get("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}")