Compare commits

..

1 Commits

Author SHA1 Message Date
83283c0657 Block connection testing and Docker improvement
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.
2025-08-22 17:16:10 +03:00
12 changed files with 1977 additions and 7109 deletions

View File

@ -1,14 +1,10 @@
FROM alpine
RUN apk add --no-cache \
yq \
python3 \
py3-pip \
py3-yaml \
graphviz
COPY run.sh /run.sh
RUN chmod +x /run.sh
RUN pip3 install graphviz --break-system-packages # Fix?
CMD ["/run.sh"]
CMD ["./run.sh"]

View File

@ -1,31 +0,0 @@
# Puudot
## Development
Start implementing graph.py with Graphviz Python library.
First created in new_graph.py, but will replace graph.py.
### Graph
Based on current cluster architecture.
### RecordGraph
New architecture for creating dot. No clusters; one node (record) contains one row table with each column for a person.
~~~
dot.node('<this node id>', fr'{(<node id>) <text>}|...')
dot.edge('<this node id>:<other node id>', '<other node id>')
~~~
algorithm:
~~~
for block in blocks
create node
if block has link, set link as id
else create id
add texts to node
for text in block save links
create edges from links
~~~

View File

@ -8,6 +8,8 @@ import uuid
# - 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
@ -40,9 +42,21 @@ def make_link_line(link, config):
return line + "\n"
def make_block_line(block, config):
line = f"subgraph cluster_{block['id']} {{\n{config["subgraph"]}\n"
for node in block["texts"]:
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"
@ -76,7 +90,6 @@ class Graph:
def __init__(self):
self.config = {}
self.layers = {}
self.blocks = []
self.nodes = []
self.links = []
@ -89,12 +102,15 @@ class Graph:
dot = config.get("dot", {})
if dot != {}:
dot["graph"] = dot.get("graph", "")
dot["subgraph"] = dot.get("subgraph", "")
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
@ -108,10 +124,10 @@ class Graph:
new_block = {
"id": block_id,
"label": block.get("label", ""),
"texts": []
"nodes": [],
"hidden": False
}
#prev_node_id = ""
texts_in_block = block.get("texts", [])
for i, text in enumerate(texts_in_block):
node_id = get_id()
@ -120,21 +136,27 @@ class Graph:
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})
# 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)
new_block["nodes"].append(node_id)
for link in text.get("links", []):
linker[link] = node_id
#prev_node_id = node_id
self.blocks.append(new_block)
@ -145,12 +167,12 @@ class Graph:
self.dot_file += add_links(self.links, self.config)
self.dot_file += make_footer()
def make_dot(self, dot_file, out_file, format):
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:
os.system(f"dot -T{format} {dot_file} -o {out_file}")
if format == "svg":
os.system(f"dot -T{format} {dot_file} -o {svg_file}")

View File

@ -1,54 +0,0 @@
import graphviz
# One cluster of nodes per block
class Graph():
def __init__(self):
self.dot = None
# One node per block
class RecordGraph:
def __init__(self):
self.dot = None
self.edges = []
self.node_counter = 0
def create_graph(self, config, data):
# TODO: process config, use for graph init
self.dot = graphviz.Graph('testgraph', graph_attr={'center': 'true', 'compound': 'true'}, node_attr={'shape': 'record'})
self.dot.format = 'svg' # TODO: move to export
blocks = data.get('blocks', [])
for block in blocks:
self.node_counter += 1
# Create node id
node_id = f'nr{self.node_counter}'
links = block.get('links', [])
if len(links) > 0:
node_id = f'n{links[0]}'
# Create text table for node
texts = []
for text in block.get('texts', []):
person = text.get('text', '').replace('\n', '\\n')
link = text.get('links', [])
if len(link) > 0:
person = fr'<l{link[0]}> {person}'
self.edges.append((fr'{node_id}:l{link[0]}', fr'n{link[0]}'))
texts.append(person)
# Add node
table = r"|".join(texts)
self.dot.node(node_id, table)
# Add edges
self.dot.edges(self.edges)
def export_graph(self, out_dir):
self.dot.render(directory=out_dir)
def __str__(self):
return self.dot.source

View File

@ -1,6 +1,6 @@
from db import load_data, load_config
from graph import Graph
import new_graph as ng
import os
DATA_DIR="/data"
@ -10,7 +10,6 @@ def main():
# Get all YAML files in the data directory
yaml_files = [f for f in os.listdir(DATA_DIR) if f.endswith(('.yaml', '.yml'))]
config = load_config(CONFIG_FILE)
puudot_config = config.get('puudot', {})
for yaml_file in yaml_files:
print(f"Processing {yaml_file}...")
@ -20,23 +19,11 @@ def main():
graph.set_config(config)
graph.process_blocks(data)
graph.build_dot()
# Test new dot generation
new_graph = ng.RecordGraph()
new_graph.create_graph(config, data)
#print(new_graph)
new_graph.export_graph(DATA_DIR)
# Use the base name of the YAML file (without extension) as the output name
base_name = os.path.splitext(yaml_file)[0]
dot_file = os.path.join(DATA_DIR, f"{base_name}.gv")
if puudot_config.get('verbose') == True:
print(f"Output formats: {puudot_config.get('output')}")
for format in puudot_config.get('output'):
out_file = os.path.join(DATA_DIR, f"{base_name}.{format}")
graph.make_dot(dot_file, out_file, format)
svg_file = os.path.join(DATA_DIR, f"{base_name}.svg")
graph.make_dot("svg", dot_file, svg_file)
if __name__ == "__main__":
main()

View File

@ -1,25 +1,17 @@
puudot:
verbose: false
output:
- pdf
- svg
dot:
graph: |
graph [splines=true, nodesep=0.25, ranksep="1 equally"]
//graph [splines=ortho, nodesep=0.2, ranksep="0.5 equally"]
graph [splines=polyline, nodesep=0.2, ranksep="0.5 equally"]
//graph [splines=curved, nodesep=0.2, ranksep="0.5 equally"]
//graph [splines=true, nodesep=0.2, ranksep="0.5 equally"]
//graph [splines=line, nodesep=0.2, ranksep="0.5 equally"]
//node [color=white]
//edge [headport=n, tailport=s]
compound=true
center=true
fontname="Helvetica"
charset="UTF-8"
bgcolor="white"
size="100,11.693!"
subgraph: |
labeljust=l
subgraph:
block: |
labeljust=l
hidden: |
//rank=same
//style=invis
node:
text: |
shape=plaintext
@ -30,7 +22,7 @@ dot:
height=0
edge:
default: |
headport=n
tailport=s
//headport=n
//tailport=s
hidden: |
style=invis

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 139 KiB

File diff suppressed because it is too large Load Diff

View File

@ -9,4 +9,3 @@ services:
- ./code:/code
- ./data:/data
- ./config.yaml:/config.yaml
network_mode: none

12
run.sh Normal file → Executable file
View File

@ -6,17 +6,5 @@ if [ ! -f "/code/puudot.py" ]; then
exit 1
fi
# Check if config.yaml exists
if [ ! -f "/config.yaml" ]; then
echo "Error: config.yaml not found in /config directory!"
exit 1
fi
verbose=$(yq eval '.puudot.verbose' /config.yaml)
if [ "$verbose" = true ]; then
echo $(dot -v)
fi
# Run the script
python3 /code/puudot.py