156 lines
4.5 KiB
Python
156 lines
4.5 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
|
|
|
|
|
|
|
|
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": []
|
|
}
|
|
|
|
#prev_node_id = ""
|
|
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:
|
|
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})
|
|
|
|
# Chain nodes in block
|
|
#if prev_node_id:
|
|
# self.links.append({"from": prev_node_id, "to": node_id, "head": "", "hidden": True})
|
|
|
|
new_block["texts"].append(node_id)
|
|
|
|
for link in text.get("links", []):
|
|
linker[link] = node_id
|
|
|
|
#prev_node_id = 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, dot_file, out_file, format):
|
|
if self.dot_file != "":
|
|
with open(dot_file, "w") as f:
|
|
f.write(self.dot_file)
|
|
if format:
|
|
os.system(f"dot -T{format} {dot_file} -o {out_file}")
|
|
|
|
|
|
|