Tested different block connection methods to achive orthogonal lines. Added run script and mounted code to the container to allow code changes without rebuilding the image. Implement Docker improvements in main.
178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
|
|
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}")
|
|
|
|
|
|
|