Initial commit

This commit is contained in:
FliegendeWurst 2022-05-29 15:05:23 +02:00
commit 72dec5ebc9
6 changed files with 231 additions and 0 deletions

43
README.md Normal file
View File

@ -0,0 +1,43 @@
# Hypergraph drawing using Graphviz and Asymptote
## Usage
Requires Python 3, [pydot](https://pypi.org/project/pydot/) (tested with 1.4.2), [Asymptote](https://asymptote.sourceforge.io/) (tested with 2.67) and LaTeX (tested with TeX Live 2021).
To render a hypergraph specified in `graph.dot`, execute the following:
```bash
cat graph.dot | dot -Tdot | python3 ./dot_hypergraph_to_asymptote.py /dev/stdin > graph.asy
asy -f png graph.asy
```
You may adjust the font size, width/height of the output and more by editing the values at the beginning of the script.
## Example
| Original graph | Asymptote rendering |
| -------------- | ------------------- |
| ![dot rendering](./graph_dot.png) | ![asymptote rendering](./graph.png) |
`graph.dot`:
```
digraph {
edge [dir="back"];
"Node 1" -> "Node 2" [label="Edge 1"]
"Node 1" -> "Node 3" [label="Edge 1"]
"Node 3" -> "Node 4" [label="Edge 2"]
"Node 3" -> "Node 5" [label="Edge 2"]
"Node 5" -> "Node 6" [label="Edge 3"]
"Node 4" -> "Node 6" [label="Edge 3"]
"Node 2" -> "Node 7" [label="Edge 4"]
"Node 6" -> "Node 7" [label="Edge 4"]
"Node 2" -> "Node 8" [label="Edge 5"]
"Node 7" -> "Node 8" [label="Edge 5"]
}
```
## Credits
The generated code is based on https://tex.stackexchange.com/a/108099/185782 by g.kov.
## License
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)

167
dot_hypergraph_to_asymptote.py Executable file
View File

@ -0,0 +1,167 @@
#! /usr/bin/env python3
import sys
import itertools
import pydot # tested with 1.4.2
if len(sys.argv) != 2:
print("Usage: dot_hypergraph_to_asymptote.py <dot file>")
sys.exit(1)
# output size
width = None # 400
height = 639
# size of node and edge labels
fontsize = "20pt"
# specify minimum text width
# (useful if your labels contain wide unicode characters)
text_width_base = 35 # 40
text_width_factor = 5.5 # 9
# draw edges reversed
reverse_edges = True
# output \begin{asy} ... \end{asy}
output_latex = False
graphs = pydot.graph_from_dot_file(sys.argv[1])
graph = graphs[0]
nodes = dict()
node_list = []
for node in graph.get_nodes():
pos = node.get_pos()
if pos is None:
continue
pos = pos[1:-1]
nodes[node.get_name()] = tuple(map(float, pos.split(",")))
node_list.append(node.get_name())
edges = dict()
for edge in graph.get_edges():
label = edge.get_label()
if label is None:
continue
label = label.replace('"', "")
src = edge.get_source()
dest = edge.get_destination()
if label not in edges:
edges[label] = [(src, dest)]
else:
edges[label].append((src, dest))
hyperedges = []
for label in edges:
xmin = min(itertools.chain(*map(lambda x: [nodes[x[0]][0], nodes[x[1]][0]], edges[label])))
xmax = max(itertools.chain(*map(lambda x: [nodes[x[0]][0], nodes[x[1]][0]], edges[label])))
ymin = min(map(lambda x: nodes[x[0]][1], edges[label]))
ymax = max(map(lambda x: nodes[x[1]][1], edges[label]))
x = (xmax + xmin) / 2
y = (ymax + ymin) / 2
#print(label, xmin, xmax, ymin, ymax, x, y)
hyperedges.append(((x, y), label))
#input("")
if output_latex:
print("\\begin{asy}")
# code based on https://tex.stackexchange.com/a/108099/185782 (g.kov)
if width is None:
width = 0
if height is None:
height = 0
print("size(", width, ",", height, ");")
print("import flowchart;")
if fontsize is not None:
print("defaultpen(fontsize(", fontsize, "));")
print("pair[]pv={")
for node in node_list:
print(nodes[node], ",")
print("};")
print("string[]pv_labels={")
for name in node_list:
if not name.startswith('"'):
name = '"' + name
if not name[-1] == '"':
name = name + '"'
print(name, ',')
print("};")
print("real[]pv_size={")
for node in node_list:
print(text_width_base + len(node[1:-1])*text_width_factor, ',')
print("};")
print("pair[][] pe={")
for x in hyperedges:
print("{", x[0], ",E},")
print("};")
print("string[] pe_labels={")
for x in hyperedges:
print('"', x[1].replace("_", "\\_"), '",')
print("};")
print("""
pen nodeFill=white;
pen nodeLine=darkblue+1.2pt+opacity(0.5);
block[] v=new block[pv.length];
block[] e=new block[pe.length];
for(int i=0;i<pv.length;++i){
v[i]=rectangle(pv_labels[i],pv[i],nodeFill,nodeLine,minwidth=pv_size[i]);
draw(v[i]);
}
string s;
for(int i=0;i<pe.length;++i){
s=pe_labels[i];
e[i]=circle(s,pe[i][0]);
label(s,pe[i][0],pe[i][1]);
dot(pe[i][0]);
}
add(
new void(picture pic, transform t) {
blockconnector operator --=blockconnector(pic,t);
real tg;
pen linePen=darkblue+1.2pt;
currentpen=linePen;
arrowfactor=4;
""")
for n, edge in enumerate(hyperedges):
label = edge[1]
for e in edges[label]:
i1 = node_list.index(e[0])
i2 = node_list.index(e[1])
if reverse_edges:
print("tg=90;")
print("draw(pic,v[%d].top(t){dir(90)} .. {dir(tg)}t*e[%d].center{dir(tg)} .. v[%d].bottom(t){dir(90)}, Arrow(HookHead));" % (i2, n, i1))
else:
print("tg=270;")
print("draw(pic,v[%d].bottom(t){dir(270)} .. {dir(tg)}t*e[%d].center{dir(tg)} .. v[%d].top(t){dir(270)},Arrow(HookHead));" % (i1, n, i2))
print("});")
if output_latex:
print("\\end{asy}")

13
graph.dot Normal file
View File

@ -0,0 +1,13 @@
digraph {
edge [dir="back"];
"Node 1" -> "Node 2" [label="Edge 1"]
"Node 1" -> "Node 3" [label="Edge 1"]
"Node 3" -> "Node 4" [label="Edge 2"]
"Node 3" -> "Node 5" [label="Edge 2"]
"Node 5" -> "Node 6" [label="Edge 3"]
"Node 4" -> "Node 6" [label="Edge 3"]
"Node 2" -> "Node 7" [label="Edge 4"]
"Node 6" -> "Node 7" [label="Edge 4"]
"Node 2" -> "Node 8" [label="Edge 5"]
"Node 7" -> "Node 8" [label="Edge 5"]
}

BIN
graph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
graph_dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

8
shell.nix Normal file
View File

@ -0,0 +1,8 @@
with (import <nixpkgs> {});
mkShell {
nativeBuildInputs = with pkgs; [
(python3.withPackages (ps: with ps; [ pydot ]))
asymptote
texlive.combined.scheme-medium
];
}