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    for (i, hyperedge) in graph.hypergraph.adjacency.iter().enumerate() {
238        // Connections n_i → e_j:p_k
239        for (j, &node_id) in hyperedge.sources.iter().enumerate() {
240            let node_idx = node_id.0; // Convert NodeId to usize
241
242            // Create a port with the correct format
243            let port = Some(Port(None, Some(format!("s_{}", j))));
244
245            let edge = Edge {
246                ty: EdgeTy::Pair(
247                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
248                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), port)),
249                ),
250                attributes: vec![],
251            };
252            stmts.push(Stmt::Edge(edge));
253        }
254
255        // Connect edge target ports to target nodes
256        // Connections e_j:p_k → n_i
257        for (j, &node_id) in hyperedge.targets.iter().enumerate() {
258            let node_idx = node_id.0; // Convert NodeId to usize
259
260            // Create a port with the correct format
261            let port = Some(Port(None, Some(format!("t_{}", j))));
262
263            let edge = Edge {
264                ty: EdgeTy::Pair(
265                    Vertex::N(NodeId(Id::Plain(format!("e_{}", i)), port)),
266                    Vertex::N(NodeId(Id::Plain(format!("n_{}", node_idx)), None)),
267                ),
268                attributes: vec![],
269            };
270            stmts.push(Stmt::Edge(edge));
271        }
272    }
273
274    stmts
275}
276
277/// Generate interface nodes for sources and targets of the hypergraph
278fn generate_interface_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
279where
280    O: Clone + Debug + PartialEq,
281    A: Clone + Debug + PartialEq,
282{
283    let mut stmts = Vec::new();
284
285    // Create source interface record node
286    if !graph.sources.is_empty() {
287        // Create port sections for sources
288        let mut source_ports = String::new();
289        for i in 0..graph.sources.len() {
290            source_ports.push_str(&format!("<p_{i}> | "));
291        }
292        // Remove last " | "
293        if !source_ports.is_empty() {
294            source_ports.truncate(source_ports.len() - 3);
295        }
296
297        // Create the source interface node
298        stmts.push(Stmt::Node(Node {
299            id: NodeId(Id::Plain(String::from("sources")), None),
300            attributes: vec![
301                Attribute(
302                    Id::Plain(String::from("label")),
303                    Id::Plain(format!("\"{{ {{}} | {{ {} }} }}\"", source_ports)),
304                ),
305                Attribute(
306                    Id::Plain(String::from("shape")),
307                    Id::Plain(String::from("record")),
308                ),
309                Attribute(
310                    Id::Plain(String::from("style")),
311                    Id::Plain(String::from("invisible")),
312                ),
313                Attribute(
314                    Id::Plain(String::from("rank")),
315                    Id::Plain(String::from("source")),
316                ),
317            ],
318        }));
319
320        // Connect source interface ports to the source nodes
321        for (i, &source_node_id) in graph.sources.iter().enumerate() {
322            let edge = Edge {
323                ty: EdgeTy::Pair(
324                    Vertex::N(NodeId(
325                        Id::Plain(String::from("sources")),
326                        Some(Port(None, Some(format!("p_{}", i)))),
327                    )),
328                    Vertex::N(NodeId(Id::Plain(format!("n_{}", source_node_id.0)), None)),
329                ),
330                attributes: vec![Attribute(
331                    Id::Plain(String::from("style")),
332                    Id::Plain(String::from("dashed")),
333                )],
334            };
335            stmts.push(Stmt::Edge(edge));
336        }
337    }
338
339    // Create target interface record node
340    if !graph.targets.is_empty() {
341        // Create port sections for targets
342        let mut target_ports = String::new();
343        for i in 0..graph.targets.len() {
344            target_ports.push_str(&format!("<p_{i}> | "));
345        }
346        // Remove last " | "
347        if !target_ports.is_empty() {
348            target_ports.truncate(target_ports.len() - 3);
349        }
350
351        // Create the target interface node
352        stmts.push(Stmt::Node(Node {
353            id: NodeId(Id::Plain(String::from("targets")), None),
354            attributes: vec![
355                Attribute(
356                    Id::Plain(String::from("label")),
357                    Id::Plain(format!("\"{{ {{ {} }} | {{}} }}\"", target_ports)),
358                ),
359                Attribute(
360                    Id::Plain(String::from("shape")),
361                    Id::Plain(String::from("record")),
362                ),
363                Attribute(
364                    Id::Plain(String::from("style")),
365                    Id::Plain(String::from("invisible")),
366                ),
367                Attribute(
368                    Id::Plain(String::from("rank")),
369                    Id::Plain(String::from("sink")),
370                ),
371            ],
372        }));
373
374        // Connect target nodes to target interface ports
375        for (i, &target_node_id) in graph.targets.iter().enumerate() {
376            let edge = Edge {
377                ty: EdgeTy::Pair(
378                    Vertex::N(NodeId(Id::Plain(format!("n_{}", target_node_id.0)), None)),
379                    Vertex::N(NodeId(
380                        Id::Plain(String::from("targets")),
381                        Some(Port(None, Some(format!("p_{}", i)))),
382                    )),
383                ),
384                attributes: vec![Attribute(
385                    Id::Plain(String::from("style")),
386                    Id::Plain(String::from("dashed")),
387                )],
388            };
389            stmts.push(Stmt::Edge(edge));
390        }
391    }
392
393    stmts
394}
395
396/// Generate statements for quotient connections (dotted lines between unified nodes)
397fn generate_quotient_stmts<O, A>(graph: &OpenHypergraph<O, A>) -> Vec<Stmt>
398where
399    O: Clone + Debug + PartialEq,
400    A: Clone + Debug + PartialEq,
401{
402    let mut stmts = Vec::new();
403
404    // Extract unified node pairs from the quotient
405    let (lefts, rights) = &graph.hypergraph.quotient;
406
407    // Create a map to track which nodes are unified
408    let mut unified_nodes = std::collections::HashMap::new();
409
410    for (left, right) in lefts.iter().zip(rights.iter()) {
411        let left_idx = left.0; // Access the internal usize
412        let right_idx = right.0;
413
414        // Check if we've already seen this pair (in any order)
415        let pair_key = if left_idx < right_idx {
416            (left_idx, right_idx)
417        } else {
418            (right_idx, left_idx)
419        };
420
421        if unified_nodes.insert(pair_key, true).is_none() {
422            // Create a dashed edge between unified nodes
423            let edge = Edge {
424                ty: EdgeTy::Pair(
425                    Vertex::N(NodeId(Id::Plain(format!("n_{}", left_idx)), None)),
426                    Vertex::N(NodeId(Id::Plain(format!("n_{}", right_idx)), None)),
427                ),
428                attributes: vec![
429                    Attribute(
430                        Id::Plain(String::from("style")),
431                        Id::Plain(String::from("dotted")),
432                    ),
433                    Attribute(
434                        Id::Plain(String::from("dir")),
435                        Id::Plain(String::from("none")),
436                    ),
437                ],
438            };
439            stmts.push(Stmt::Edge(edge));
440        }
441    }
442
443    stmts
444}