commit 72dec5ebc99e38738123a6a99a263dfe2920299d Author: FliegendeWurst <2012gdwu+github@posteo.de> Date: Sun May 29 15:05:23 2022 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0731c1 --- /dev/null +++ b/README.md @@ -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/) diff --git a/dot_hypergraph_to_asymptote.py b/dot_hypergraph_to_asymptote.py new file mode 100755 index 0000000..beaf860 --- /dev/null +++ b/dot_hypergraph_to_asymptote.py @@ -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 ") + 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 "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"] +} \ No newline at end of file diff --git a/graph.png b/graph.png new file mode 100644 index 0000000..6225259 Binary files /dev/null and b/graph.png differ diff --git a/graph_dot.png b/graph_dot.png new file mode 100644 index 0000000..f16818e Binary files /dev/null and b/graph_dot.png differ diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..0058ec0 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +with (import {}); +mkShell { + nativeBuildInputs = with pkgs; [ + (python3.withPackages (ps: with ps; [ pydot ])) + asymptote + texlive.combined.scheme-medium + ]; +}