petgraph/
dot.rs

1//! Simple graphviz dot file format output.
2
3use alloc::string::String;
4use core::fmt::{self, Display, Write};
5
6use crate::visit::{
7    EdgeRef, GraphProp, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef,
8};
9
10/// `Dot` implements output to graphviz .dot format for a graph.
11///
12/// Formatting and options are rather simple, this is mostly intended
13/// for debugging. Exact output may change.
14///
15/// # Examples
16///
17/// ```
18/// use petgraph::Graph;
19/// use petgraph::dot::{Dot, Config};
20///
21/// let mut graph = Graph::<_, ()>::new();
22/// graph.add_node("A");
23/// graph.add_node("B");
24/// graph.add_node("C");
25/// graph.add_node("D");
26/// graph.extend_with_edges(&[
27///     (0, 1), (0, 2), (0, 3),
28///     (1, 2), (1, 3),
29///     (2, 3),
30/// ]);
31///
32/// println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
33///
34/// // In this case the output looks like this:
35/// //
36/// // digraph {
37/// //     0 [label="\"A\""]
38/// //     1 [label="\"B\""]
39/// //     2 [label="\"C\""]
40/// //     3 [label="\"D\""]
41/// //     0 -> 1
42/// //     0 -> 2
43/// //     0 -> 3
44/// //     1 -> 2
45/// //     1 -> 3
46/// //     2 -> 3
47/// // }
48///
49/// // If you need multiple config options, just list them all in the slice.
50/// ```
51pub struct Dot<'a, G>
52where
53    G: IntoEdgeReferences + IntoNodeReferences,
54{
55    graph: G,
56    get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
57    get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
58    config: Configs,
59}
60
61static TYPE: [&str; 2] = ["graph", "digraph"];
62static EDGE: [&str; 2] = ["--", "->"];
63static INDENT: &str = "    ";
64
65impl<'a, G> Dot<'a, G>
66where
67    G: IntoNodeReferences + IntoEdgeReferences,
68{
69    /// Create a `Dot` formatting wrapper with default configuration.
70    #[inline]
71    pub fn new(graph: G) -> Self {
72        Self::with_config(graph, &[])
73    }
74
75    /// Create a `Dot` formatting wrapper with custom configuration.
76    #[inline]
77    pub fn with_config(graph: G, config: &'a [Config]) -> Self {
78        Self::with_attr_getters(graph, config, &|_, _| String::new(), &|_, _| String::new())
79    }
80
81    #[inline]
82    pub fn with_attr_getters(
83        graph: G,
84        config: &'a [Config],
85        get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
86        get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
87    ) -> Self {
88        let config = Configs::extract(config);
89        Dot {
90            graph,
91            get_edge_attributes,
92            get_node_attributes,
93            config,
94        }
95    }
96}
97
98/// Direction of graph layout.
99///
100/// <https://graphviz.org/docs/attrs/rankdir/>
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum RankDir {
103    /// Top to bottom
104    TB,
105    /// Bottom to top
106    BT,
107    /// Left to right
108    LR,
109    /// Right to left
110    RL,
111}
112
113/// `Dot` configuration.
114///
115/// This enum does not have an exhaustive definition (will be expanded)
116#[non_exhaustive]
117#[derive(Debug, PartialEq, Eq)]
118pub enum Config {
119    /// Use indices for node labels.
120    NodeIndexLabel,
121    /// Use indices for edge labels.
122    EdgeIndexLabel,
123    /// Use no edge labels.
124    EdgeNoLabel,
125    /// Use no node labels.
126    NodeNoLabel,
127    /// Do not print the graph/digraph string.
128    GraphContentOnly,
129    /// Sets direction of graph layout.
130    RankDir(RankDir),
131}
132macro_rules! make_config_struct {
133    ($($variant:ident,)*) => {
134        #[allow(non_snake_case)]
135        #[derive(Default)]
136        struct Configs {
137            $($variant: bool,)*
138            RankDir: Option<RankDir>,
139        }
140        impl Configs {
141            #[inline]
142            fn extract(configs: &[Config]) -> Self {
143                let mut conf = Self::default();
144                for c in configs {
145                    match c {
146                        $(Config::$variant => conf.$variant = true,)*
147                        Config::RankDir(dir) => conf.RankDir = Some(*dir),
148                    }
149                }
150                conf
151            }
152        }
153    }
154}
155make_config_struct!(
156    NodeIndexLabel,
157    EdgeIndexLabel,
158    EdgeNoLabel,
159    NodeNoLabel,
160    GraphContentOnly,
161);
162
163impl<G> Dot<'_, G>
164where
165    G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp,
166{
167    fn graph_fmt<NF, EF>(&self, f: &mut fmt::Formatter, node_fmt: NF, edge_fmt: EF) -> fmt::Result
168    where
169        NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
170        EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
171    {
172        let g = self.graph;
173        if !self.config.GraphContentOnly {
174            writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?;
175        }
176
177        if let Some(rank_dir) = &self.config.RankDir {
178            let value = match rank_dir {
179                RankDir::TB => "TB",
180                RankDir::BT => "BT",
181                RankDir::LR => "LR",
182                RankDir::RL => "RL",
183            };
184            writeln!(f, "{}rankdir=\"{}\"", INDENT, value)?;
185        }
186
187        // output all labels
188        for node in g.node_references() {
189            write!(f, "{}{} [ ", INDENT, g.to_index(node.id()),)?;
190            if !self.config.NodeNoLabel {
191                write!(f, "label = \"")?;
192                if self.config.NodeIndexLabel {
193                    write!(f, "{}", g.to_index(node.id()))?;
194                } else {
195                    Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
196                }
197                write!(f, "\" ")?;
198            }
199            writeln!(f, "{}]", (self.get_node_attributes)(g, node))?;
200        }
201        // output all edges
202        for (i, edge) in g.edge_references().enumerate() {
203            write!(
204                f,
205                "{}{} {} {} [ ",
206                INDENT,
207                g.to_index(edge.source()),
208                EDGE[g.is_directed() as usize],
209                g.to_index(edge.target()),
210            )?;
211            if !self.config.EdgeNoLabel {
212                write!(f, "label = \"")?;
213                if self.config.EdgeIndexLabel {
214                    write!(f, "{}", i)?;
215                } else {
216                    Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
217                }
218                write!(f, "\" ")?;
219            }
220            writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
221        }
222
223        if !self.config.GraphContentOnly {
224            writeln!(f, "}}")?;
225        }
226        Ok(())
227    }
228}
229
230impl<G> fmt::Display for Dot<'_, G>
231where
232    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
233    G::EdgeWeight: fmt::Display,
234    G::NodeWeight: fmt::Display,
235{
236    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
237        self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt)
238    }
239}
240
241impl<G> fmt::LowerHex for Dot<'_, G>
242where
243    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
244    G::EdgeWeight: fmt::LowerHex,
245    G::NodeWeight: fmt::LowerHex,
246{
247    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
248        self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
249    }
250}
251
252impl<G> fmt::UpperHex for Dot<'_, G>
253where
254    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
255    G::EdgeWeight: fmt::UpperHex,
256    G::NodeWeight: fmt::UpperHex,
257{
258    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
259        self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
260    }
261}
262
263impl<G> fmt::Debug for Dot<'_, G>
264where
265    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
266    G::EdgeWeight: fmt::Debug,
267    G::NodeWeight: fmt::Debug,
268{
269    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270        self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt)
271    }
272}
273
274/// Escape for Graphviz
275struct Escaper<W>(W);
276
277impl<W> fmt::Write for Escaper<W>
278where
279    W: fmt::Write,
280{
281    fn write_str(&mut self, s: &str) -> fmt::Result {
282        for c in s.chars() {
283            self.write_char(c)?;
284        }
285        Ok(())
286    }
287
288    fn write_char(&mut self, c: char) -> fmt::Result {
289        match c {
290            '"' | '\\' => self.0.write_char('\\')?,
291            // \l is for left justified linebreak
292            '\n' => return self.0.write_str("\\l"),
293            _ => {}
294        }
295        self.0.write_char(c)
296    }
297}
298
299/// Pass Display formatting through a simple escaping filter
300struct Escaped<T>(T);
301
302impl<T> fmt::Display for Escaped<T>
303where
304    T: fmt::Display,
305{
306    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307        if f.alternate() {
308            writeln!(&mut Escaper(f), "{:#}", &self.0)
309        } else {
310            write!(&mut Escaper(f), "{}", &self.0)
311        }
312    }
313}
314
315/// Format data using a specific format function
316struct FnFmt<'a, T, F>(&'a T, F);
317
318impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
319where
320    F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
321{
322    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323        self.1(self.0, f)
324    }
325}
326
327#[cfg(test)]
328mod test {
329    use alloc::{format, string::String};
330    use core::fmt::Write;
331
332    use super::{Config, Dot, Escaper, RankDir};
333    use crate::prelude::Graph;
334    use crate::visit::NodeRef;
335
336    #[test]
337    fn test_escape() {
338        let mut buff = String::new();
339        {
340            let mut e = Escaper(&mut buff);
341            let _ = e.write_str("\" \\ \n");
342        }
343        assert_eq!(buff, "\\\" \\\\ \\l");
344    }
345
346    fn simple_graph() -> Graph<&'static str, &'static str> {
347        let mut graph = Graph::<&str, &str>::new();
348        let a = graph.add_node("A");
349        let b = graph.add_node("B");
350        graph.add_edge(a, b, "edge_label");
351        graph
352    }
353
354    #[test]
355    fn test_nodeindexlable_option() {
356        let graph = simple_graph();
357        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeIndexLabel]));
358        assert_eq!(dot, "digraph {\n    0 [ label = \"0\" ]\n    1 [ label = \"1\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n");
359    }
360
361    #[test]
362    fn test_edgeindexlable_option() {
363        let graph = simple_graph();
364        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeIndexLabel]));
365        assert_eq!(dot, "digraph {\n    0 [ label = \"\\\"A\\\"\" ]\n    1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"0\" ]\n}\n");
366    }
367
368    #[test]
369    fn test_edgenolable_option() {
370        let graph = simple_graph();
371        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
372        assert_eq!(dot, "digraph {\n    0 [ label = \"\\\"A\\\"\" ]\n    1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ ]\n}\n");
373    }
374
375    #[test]
376    fn test_nodenolable_option() {
377        let graph = simple_graph();
378        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeNoLabel]));
379        assert_eq!(
380            dot,
381            "digraph {\n    0 [ ]\n    1 [ ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
382        );
383    }
384
385    #[test]
386    fn test_rankdir_bt_option() {
387        let graph = simple_graph();
388        let dot = format!(
389            "{:?}",
390            Dot::with_config(&graph, &[Config::RankDir(RankDir::TB)])
391        );
392        assert_eq!(
393            dot,
394            "digraph {\n    rankdir=\"TB\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
395            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
396        );
397    }
398
399    #[test]
400    fn test_rankdir_tb_option() {
401        let graph = simple_graph();
402        let dot = format!(
403            "{:?}",
404            Dot::with_config(&graph, &[Config::RankDir(RankDir::BT)])
405        );
406        assert_eq!(
407            dot,
408            "digraph {\n    rankdir=\"BT\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
409            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
410        );
411    }
412
413    #[test]
414    fn test_rankdir_lr_option() {
415        let graph = simple_graph();
416        let dot = format!(
417            "{:?}",
418            Dot::with_config(&graph, &[Config::RankDir(RankDir::LR)])
419        );
420        assert_eq!(
421            dot,
422            "digraph {\n    rankdir=\"LR\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
423            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
424        );
425    }
426
427    #[test]
428    fn test_rankdir_rl_option() {
429        let graph = simple_graph();
430        let dot = format!(
431            "{:?}",
432            Dot::with_config(&graph, &[Config::RankDir(RankDir::RL)])
433        );
434        assert_eq!(
435            dot,
436            "digraph {\n    rankdir=\"RL\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
437            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
438        );
439    }
440
441    #[test]
442    fn test_with_attr_getters() {
443        let graph = simple_graph();
444        let dot = format!(
445            "{:?}",
446            Dot::with_attr_getters(
447                &graph,
448                &[Config::NodeNoLabel, Config::EdgeNoLabel],
449                &|_, er| format!("label = \"{}\"", er.weight().to_uppercase()),
450                &|_, nr| format!("label = \"{}\"", nr.weight().to_lowercase()),
451            ),
452        );
453        assert_eq!(dot, "digraph {\n    0 [ label = \"a\"]\n    1 [ label = \"b\"]\n    0 -> 1 [ label = \"EDGE_LABEL\"]\n}\n");
454    }
455}