open_hypergraphs_dot/
lib.rs

1use dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, Node, NodeId, Port, Stmt, Vertex};
2use open_hypergraphs::lax::OpenHypergraph;
3use std::fmt::Debug;
4
5pub mod options;
6pub use options::*;
7
8pub fn generate_dot<O, A>(graph: &OpenHypergraph<O, A>) -> Graph
9where
10    O: Clone + Debug + PartialEq,
11    A: Clone + Debug + PartialEq,
12{
13    generate_dot_with(graph, &Options::default())
14}
15
16/// Generates a GraphViz DOT representation of a lax open hypergraph
17pub fn generate_dot_with<O, A>(graph: &OpenHypergraph<O, A>, opts: &Options<O, A>) -> Graph
18where
19    O: Clone + Debug + PartialEq,
20    A: Clone + Debug + PartialEq,
21{
22    let theme = &opts.theme;
23
24    // Create a directed graph
25    let mut dot_graph = Graph::DiGraph {
26        id: Id::Plain(String::from("G")),
27        strict: false,
28        stmts: Vec::new(),
29    };
30
31    // Set graph attributes
32    dot_graph.add_stmt(Stmt::Attribute(Attribute(
33        Id::Plain(String::from("rankdir")),
34        Id::Plain(opts.orientation.to_string()),
35    )));
36
37    // Set background color
38    dot_graph.add_stmt(Stmt::Attribute(Attribute(
39        Id::Plain(String::from("bgcolor")),
40        Id::Plain(format!("\"{}\"", theme.bgcolor.clone())),
41    )));
42
43    // Add default node attributes statement
44    dot_graph.add_stmt(Stmt::Node(Node {
45        id: NodeId(Id::Plain(String::from("node")), None),
46        attributes: vec![
47            Attribute(
48                Id::Plain(String::from("shape")),
49                Id::Plain(String::from("record")),
50            ),
51            Attribute(
52                Id::Plain(String::from("style")),
53                Id::Plain(String::from("rounded")),
54            ),
55            Attribute(
56                Id::Plain(String::from("fontcolor")),
57                Id::Plain(format!("\"{}\"", theme.fontcolor.clone())),
58            ),
59            Attribute(
60                Id::Plain(String::from("color")),
61                Id::Plain(format!("\"{}\"", theme.color.clone())),
62            ),
63        ],
64    }));
65
66    // Add default edge attributes statement
67    dot_graph.add_stmt(Stmt::Node(Node {
68        id: NodeId(Id::Plain(String::from("edge")), None),
69        attributes: vec![
70            Attribute(
71                Id::Plain(String::from("fontcolor")),
72                Id::Plain(format!("\"{}\"", theme.fontcolor.clone())),
73            ),
74            Attribute(
75                Id::Plain(String::from("color")),
76                Id::Plain(format!("\"{}\"", theme.color.clone())),
77            ),
78            Attribute(
79                Id::Plain(String::from("arrowhead")),
80                Id::Plain(String::from("none")),
81            ),
82        ],
83    }));
84
85    // Add nodes for each node in the hypergraph
86    let node_stmts = generate_node_stmts(graph, opts);
87    for stmt in node_stmts {
88        dot_graph.add_stmt(stmt);
89    }
90
91    // Add record nodes for each hyperedge
92    let edge_stmts = generate_edge_stmts(graph, opts);
93    for stmt in edge_stmts {
94        dot_graph.add_stmt(stmt);
95    }
96
97    // Add source and target interface nodes
98    let interface_stmts = generate_interface_stmts(graph);
99    for stmt in interface_stmts {
100        dot_graph.add_stmt(stmt);
101    }
102
103    // Connect nodes to edges
104    let connection_stmts = generate_connection_stmts(graph);
105    for stmt in connection_stmts {
106        dot_graph.add_stmt(stmt);
107    }
108
109    // Add quotient connections (dotted lines between unified nodes)
110    let quotient_stmts = generate_quotient_stmts(graph);
111    for stmt in quotient_stmts {
112        dot_graph.add_stmt(stmt);
113    }
114
115    dot_graph
116}
117
118// Unfortunately this seems to be a fundamental limitation of the dot syntax;
119// See https://forum.graphviz.org/t/how-do-i-properly-escape-arbitrary-text-for-use-in-labels/1762
120// > Unfortunately, due to past mistakes, we realized there is no way to safely put
121// > arbitrary text in graphviz strings, as we made mistakes in handling quotes and escapes.
122fn escape_dot_label(s: &str) -> String {
123    s.chars()
124        .flat_map(|c| match c {
125            '\\' => Some("\\\\".to_string()),
126            '"' => Some("\\\"".to_string()),
127            '{' => Some("\\{".to_string()),
128            '}' => Some("\\}".to_string()),
129            '|' => Some("\\|".to_string()),
130            '<' => Some("\\<".to_string()),
131            '>' => Some("\\>".to_string()),
132            _ => Some(c.to_string()),
133        })
134        .collect()
135}
136
137/// Generate node statements for each node in the hypergraph
138fn generate_node_stmts<O, A>(graph: &OpenHypergraph<O, A>, opts: &Options<O, A>) -> Vec<Stmt>
139where
140    O: Clone + Debug + PartialEq,
141    A: Clone + Debug + PartialEq,
142{
143    let mut stmts = Vec::new();
144
145    for i in 0..graph.hypergraph.nodes.len() {
146        let label = (opts.node_label)(&graph.hypergraph.nodes[i]);
147
148        // Escape special dot characters.
149        let label = escape_dot_label(&label);
150
151        stmts.push(Stmt::Node(Node {
152            id: NodeId(Id::Plain(format!("n_{}", i)), None),
153            attributes: vec![
154                Attribute(
155                    Id::Plain(String::from("shape")),
156                    Id::Plain(String::from("point")),
157                ),
158                Attribute(
159                    Id::Plain(String::from("xlabel")),
160                    Id::Plain(format!("\"{}\"", label)),
161                ),
162            ],
163        }));
164    }
165
166    stmts
167}
168
169/// Generate record node statements for each hyperedge
170fn generate_edge_stmts<O, A>(graph: &OpenHypergraph<O, A>, opts: &Options<O, A>) -> Vec<Stmt>
171where
172    O: Clone + Debug + PartialEq,
173    A: Clone + Debug + PartialEq,
174{
175    let mut stmts = Vec::new();
176
177    for i in 0..graph.hypergraph.edges.len() {
178        let hyperedge = &graph.hypergraph.adjacency[i];
179        let label = (opts.edge_label)(&graph.hypergraph.edges[i]);
180        let label = escape_dot_label(&label);
181
182        // Create port sections for sources
183        let mut source_ports = String::new();
184        for j in 0..hyperedge.sources.len() {
185            source_ports.push_str(&format!("<s_{j}> | "));
186        }
187        if !source_ports.is_empty() {
188            source_ports.truncate(source_ports.len() - 3); // Remove last " | "
189        }
190
191        // Create port sections for targets
192        let mut target_ports = String::new();
193        for j in 0..hyperedge.targets.len() {
194            target_ports.push_str(&format!("<t_{j}> | "));
195        }
196        if !target_ports.is_empty() {
197            target_ports.truncate(target_ports.len() - 3); // Remove last " | "
198        }
199
200        // Create full record label with proper quoting for GraphViz DOT format
201        let record_label = if source_ports.is_empty() && target_ports.is_empty() {
202            format!("\"{}\"", label)
203        } else if source_ports.is_empty() {
204            format!("\"{{ {} | {{ {} }} }}\"", label, target_ports)
205        } else if target_ports.is_empty() {
206            format!("\"{{ {{ {} }} | {} }}\"", source_ports, label)
207        } else {
208            format!(
209                "\"{{ {{ {} }} | {} | {{ {} }} }}\"",
210                source_ports, label, target_ports
211            )
212        };
213
214        stmts.push(Stmt::Node(Node {
215            id: NodeId(Id::Plain(format!("e_{}", i)), None),
216            attributes: vec![
217                Attribute(Id::Plain(String::from("label")), Id::Plain(record_label)),
218                Attribute(
219                    Id::Plain(String::from("shape")),
220                    Id::Plain(String::from("record")),
221                ),
222            ],
223        }));
224    }
225
226    stmts
227}
228
229/// Generate statements connecting nodes to edges
230fn generate_connection_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
231where
232    O: Clone + Debug + PartialEq,
233    A: Clone + Debug + PartialEq,
234{
235    let mut stmts = Vec::new();
236
237    // Connect source nodes to edge ports
238    for (i, hyperedge) in graph.hypergraph.adjacency.iter().enumerate() {
239        for &node_id in hyperedge.sources.iter() {
240            let node_idx = node_id.0; // Convert NodeId to usize
241
242            let edge = Edge {
243                ty: EdgeTy::Pair(
244                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
245                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), None)),
246                ),
247                attributes: vec![],
248            };
249            stmts.push(Stmt::Edge(edge));
250        }
251
252        // Connect edge target ports to target nodes
253        for (j, &node_id) in hyperedge.targets.iter().enumerate() {
254            let node_idx = node_id.0; // Convert NodeId to usize
255
256            // Create a port with the correct format
257            let port = Some(Port(None, Some(format!("t_{}", j))));
258
259            let edge = Edge {
260                ty: EdgeTy::Pair(
261                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), port)),
262                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
263                ),
264                attributes: vec![],
265            };
266            stmts.push(Stmt::Edge(edge));
267        }
268    }
269
270    stmts
271}
272
273/// Generate interface nodes for sources and targets of the hypergraph
274fn generate_interface_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
275where
276    O: Clone + Debug + PartialEq,
277    A: Clone + Debug + PartialEq,
278{
279    let mut stmts = Vec::new();
280
281    // Create source interface record node
282    if !graph.sources.is_empty() {
283        // Create port sections for sources
284        let mut source_ports = String::new();
285        for i in 0..graph.sources.len() {
286            source_ports.push_str(&format!("<p_{i}> | "));
287        }
288        // Remove last " | "
289        if !source_ports.is_empty() {
290            source_ports.truncate(source_ports.len() - 3);
291        }
292
293        // Create the source interface node
294        stmts.push(Stmt::Node(Node {
295            id: NodeId(Id::Plain(String::from("sources")), None),
296            attributes: vec![
297                Attribute(
298                    Id::Plain(String::from("label")),
299                    Id::Plain(format!("\"{{ {{}} | {{ {} }} }}\"", source_ports)),
300                ),
301                Attribute(
302                    Id::Plain(String::from("shape")),
303                    Id::Plain(String::from("record")),
304                ),
305                Attribute(
306                    Id::Plain(String::from("style")),
307                    Id::Plain(String::from("invisible")),
308                ),
309                Attribute(
310                    Id::Plain(String::from("rank")),
311                    Id::Plain(String::from("source")),
312                ),
313            ],
314        }));
315
316        // Connect source interface ports to the source nodes
317        for (i, &source_node_id) in graph.sources.iter().enumerate() {
318            let edge = Edge {
319                ty: EdgeTy::Pair(
320                    Vertex::N(NodeId(
321                        Id::Plain(String::from("sources")),
322                        Some(Port(None, Some(format!("p_{}", i)))),
323                    )),
324                    Vertex::N(NodeId(Id::Plain(format!("n_{}", source_node_id.0)), None)),
325                ),
326                attributes: vec![Attribute(
327                    Id::Plain(String::from("style")),
328                    Id::Plain(String::from("dashed")),
329                )],
330            };
331            stmts.push(Stmt::Edge(edge));
332        }
333    }
334
335    // Create target interface record node
336    if !graph.targets.is_empty() {
337        // Create port sections for targets
338        let mut target_ports = String::new();
339        for i in 0..graph.targets.len() {
340            target_ports.push_str(&format!("<p_{i}> | "));
341        }
342        // Remove last " | "
343        if !target_ports.is_empty() {
344            target_ports.truncate(target_ports.len() - 3);
345        }
346
347        // Create the target interface node
348        stmts.push(Stmt::Node(Node {
349            id: NodeId(Id::Plain(String::from("targets")), None),
350            attributes: vec![
351                Attribute(
352                    Id::Plain(String::from("label")),
353                    Id::Plain(format!("\"{{ {{ {} }} | {{}} }}\"", target_ports)),
354                ),
355                Attribute(
356                    Id::Plain(String::from("shape")),
357                    Id::Plain(String::from("record")),
358                ),
359                Attribute(
360                    Id::Plain(String::from("style")),
361                    Id::Plain(String::from("invisible")),
362                ),
363                Attribute(
364                    Id::Plain(String::from("rank")),
365                    Id::Plain(String::from("sink")),
366                ),
367            ],
368        }));
369
370        // Connect target nodes to target interface ports
371        for (i, &target_node_id) in graph.targets.iter().enumerate() {
372            let edge = Edge {
373                ty: EdgeTy::Pair(
374                    Vertex::N(NodeId(Id::Plain(format!("n_{}", target_node_id.0)), None)),
375                    Vertex::N(NodeId(
376                        Id::Plain(String::from("targets")),
377                        Some(Port(None, Some(format!("p_{}", i)))),
378                    )),
379                ),
380                attributes: vec![Attribute(
381                    Id::Plain(String::from("style")),
382                    Id::Plain(String::from("dashed")),
383                )],
384            };
385            stmts.push(Stmt::Edge(edge));
386        }
387    }
388
389    stmts
390}
391
392/// Generate statements for quotient connections (dotted lines between unified nodes)
393fn generate_quotient_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
394where
395    O: Clone + Debug + PartialEq,
396    A: Clone + Debug + PartialEq,
397{
398    let mut stmts = Vec::new();
399
400    // Extract unified node pairs from the quotient
401    let (lefts, rights) = &graph.hypergraph.quotient;
402
403    // Create a map to track which nodes are unified
404    let mut unified_nodes = std::collections::HashMap::new();
405
406    for (left, right) in lefts.iter().zip(rights.iter()) {
407        let left_idx = left.0; // Access the internal usize
408        let right_idx = right.0;
409
410        // Check if we've already seen this pair (in any order)
411        let pair_key = if left_idx < right_idx {
412            (left_idx, right_idx)
413        } else {
414            (right_idx, left_idx)
415        };
416
417        if unified_nodes.insert(pair_key, true).is_none() {
418            // Create a dashed edge between unified nodes
419            let edge = Edge {
420                ty: EdgeTy::Pair(
421                    Vertex::N(NodeId(Id::Plain(format!("n_{}", left_idx)), None)),
422                    Vertex::N(NodeId(Id::Plain(format!("n_{}", right_idx)), None)),
423                ),
424                attributes: vec![
425                    Attribute(
426                        Id::Plain(String::from("style")),
427                        Id::Plain(String::from("dotted")),
428                    ),
429                    Attribute(
430                        Id::Plain(String::from("dir")),
431                        Id::Plain(String::from("none")),
432                    ),
433                ],
434            };
435            stmts.push(Stmt::Edge(edge));
436        }
437    }
438
439    stmts
440}