petgraph/dot/
mod.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    /// Create a `Dot` that uses the given functions to generate edge and node attributes.
82    ///
83    /// NOTE: `Config::EdgeNoLabel` and `Config::NodeNoLabel` should be set if you intend to
84    /// generate your own `label` attributes.
85    /// The getter functions should return an attribute list as a String. For example, if you
86    /// want to calculate a `label` for a node, then return `"label = \"your label here\""`.
87    /// Each function should take as arguments the graph and that graph's `EdgeRef` or `NodeRef`, respectively.
88    /// Check the documentation for the graph type to see how it implements `IntoNodeReferences`.
89    /// The [Graphviz docs] list the available attributes.
90    ///
91    /// Note that some attribute values, such as labels, should be strings and must be quoted. These can be
92    /// written using escapes (`"label = \"foo\""`) or [raw strings] (`r#"label = "foo""#`).
93    ///
94    /// For example, using a `Graph<&str, &str>` where we want the node labels to be the nodes' weights
95    /// shortened to 4 characters, and all the edges are colored blue with no labels:
96    /// ```
97    /// use petgraph::Graph;
98    /// use petgraph::dot::{Config, Dot};
99    ///
100    /// let mut deps = Graph::<&str, &str>::new();
101    /// let pg = deps.add_node("petgraph");
102    /// let fb = deps.add_node("fixedbitset");
103    /// let qc = deps.add_node("quickcheck");
104    /// let rand = deps.add_node("rand");
105    /// let libc = deps.add_node("libc");
106    /// deps.extend_with_edges(&[(pg, fb), (pg, qc), (qc, rand), (rand, libc), (qc, libc)]);
107    ///
108    /// println!(
109    ///     "{:?}",
110    ///     Dot::with_attr_getters(
111    ///         &deps,
112    ///         &[Config::EdgeNoLabel, Config::NodeNoLabel],
113    ///         &|_, _| "color = blue".to_string(),
114    ///         &|_, (_, s)| format!(r#"label = "{}""#, s.chars().take(4).collect::<String>()),
115    ///     )
116    /// );
117    /// // This outputs:
118    /// // digraph {
119    /// //     0 [ label = "petg"]
120    /// //     1 [ label = "fixe"]
121    /// //     2 [ label = "quic"]
122    /// //     3 [ label = "rand"]
123    /// //     4 [ label = "libc"]
124    /// //     0 -> 1 [ color = blue]
125    /// //     0 -> 2 [ color = blue]
126    /// //     2 -> 3 [ color = blue]
127    /// //     3 -> 4 [ color = blue]
128    /// //     2 -> 4 [ color = blue]
129    /// // }
130    /// ```
131    ///
132    /// [Graphviz docs]: https://graphviz.org/doc/info/attrs.html
133    /// [raw strings]: https://doc.rust-lang.org/rust-by-example/std/str.html#literals-and-escapes
134    #[inline]
135    pub fn with_attr_getters(
136        graph: G,
137        config: &'a [Config],
138        get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
139        get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
140    ) -> Self {
141        let config = Configs::extract(config);
142        Dot {
143            graph,
144            get_edge_attributes,
145            get_node_attributes,
146            config,
147        }
148    }
149}
150
151/// Direction of graph layout.
152///
153/// <https://graphviz.org/docs/attrs/rankdir/>
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum RankDir {
156    /// Top to bottom
157    TB,
158    /// Bottom to top
159    BT,
160    /// Left to right
161    LR,
162    /// Right to left
163    RL,
164}
165
166/// `Dot` configuration.
167///
168/// This enum does not have an exhaustive definition (will be expanded)
169#[non_exhaustive]
170#[derive(Debug, PartialEq, Eq)]
171pub enum Config {
172    /// Use indices for node labels.
173    NodeIndexLabel,
174    /// Use indices for edge labels.
175    EdgeIndexLabel,
176    /// Do not generate `label` attributes for edges.
177    EdgeNoLabel,
178    /// Do not generate `label` attributes for nodes.
179    NodeNoLabel,
180    /// Do not print the graph/digraph string.
181    GraphContentOnly,
182    /// Sets direction of graph layout.
183    RankDir(RankDir),
184}
185macro_rules! make_config_struct {
186    ($($variant:ident,)*) => {
187        #[allow(non_snake_case)]
188        #[derive(Default)]
189        struct Configs {
190            $($variant: bool,)*
191            RankDir: Option<RankDir>,
192        }
193        impl Configs {
194            #[inline]
195            fn extract(configs: &[Config]) -> Self {
196                let mut conf = Self::default();
197                for c in configs {
198                    match c {
199                        $(Config::$variant => conf.$variant = true,)*
200                        Config::RankDir(dir) => conf.RankDir = Some(*dir),
201                    }
202                }
203                conf
204            }
205        }
206    }
207}
208make_config_struct!(
209    NodeIndexLabel,
210    EdgeIndexLabel,
211    EdgeNoLabel,
212    NodeNoLabel,
213    GraphContentOnly,
214);
215
216/// A low-level function allows specifying fmt functions for nodes and edges separately.
217impl<G> Dot<'_, G>
218where
219    G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp,
220{
221    pub fn graph_fmt<NF, EF>(
222        &self,
223        f: &mut fmt::Formatter,
224        node_fmt: NF,
225        edge_fmt: EF,
226    ) -> fmt::Result
227    where
228        NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
229        EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
230    {
231        let g = self.graph;
232        if !self.config.GraphContentOnly {
233            writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?;
234        }
235
236        if let Some(rank_dir) = &self.config.RankDir {
237            let value = match rank_dir {
238                RankDir::TB => "TB",
239                RankDir::BT => "BT",
240                RankDir::LR => "LR",
241                RankDir::RL => "RL",
242            };
243            writeln!(f, "{INDENT}rankdir=\"{value}\"")?;
244        }
245
246        // output all labels
247        for node in g.node_references() {
248            write!(f, "{}{} [ ", INDENT, g.to_index(node.id()),)?;
249            if !self.config.NodeNoLabel {
250                write!(f, "label = \"")?;
251                if self.config.NodeIndexLabel {
252                    write!(f, "{}", g.to_index(node.id()))?;
253                } else {
254                    Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
255                }
256                write!(f, "\" ")?;
257            }
258            writeln!(f, "{}]", (self.get_node_attributes)(g, node))?;
259        }
260        // output all edges
261        for (i, edge) in g.edge_references().enumerate() {
262            write!(
263                f,
264                "{}{} {} {} [ ",
265                INDENT,
266                g.to_index(edge.source()),
267                EDGE[g.is_directed() as usize],
268                g.to_index(edge.target()),
269            )?;
270            if !self.config.EdgeNoLabel {
271                write!(f, "label = \"")?;
272                if self.config.EdgeIndexLabel {
273                    write!(f, "{i}")?;
274                } else {
275                    Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
276                }
277                write!(f, "\" ")?;
278            }
279            writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
280        }
281
282        if !self.config.GraphContentOnly {
283            writeln!(f, "}}")?;
284        }
285        Ok(())
286    }
287}
288
289impl<G> fmt::Display for Dot<'_, G>
290where
291    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
292    G::EdgeWeight: fmt::Display,
293    G::NodeWeight: fmt::Display,
294{
295    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
296        self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt)
297    }
298}
299
300impl<G> fmt::LowerHex for Dot<'_, G>
301where
302    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
303    G::EdgeWeight: fmt::LowerHex,
304    G::NodeWeight: fmt::LowerHex,
305{
306    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
307        self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
308    }
309}
310
311impl<G> fmt::UpperHex for Dot<'_, G>
312where
313    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
314    G::EdgeWeight: fmt::UpperHex,
315    G::NodeWeight: fmt::UpperHex,
316{
317    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
318        self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
319    }
320}
321
322impl<G> fmt::Debug for Dot<'_, G>
323where
324    G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
325    G::EdgeWeight: fmt::Debug,
326    G::NodeWeight: fmt::Debug,
327{
328    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
329        self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt)
330    }
331}
332
333/// Escape for Graphviz
334struct Escaper<W>(W);
335
336impl<W> fmt::Write for Escaper<W>
337where
338    W: fmt::Write,
339{
340    fn write_str(&mut self, s: &str) -> fmt::Result {
341        for c in s.chars() {
342            self.write_char(c)?;
343        }
344        Ok(())
345    }
346
347    fn write_char(&mut self, c: char) -> fmt::Result {
348        match c {
349            '"' | '\\' => self.0.write_char('\\')?,
350            // \l is for left justified linebreak
351            '\n' => return self.0.write_str("\\l"),
352            _ => {}
353        }
354        self.0.write_char(c)
355    }
356}
357
358/// Pass Display formatting through a simple escaping filter
359struct Escaped<T>(T);
360
361impl<T> fmt::Display for Escaped<T>
362where
363    T: fmt::Display,
364{
365    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366        if f.alternate() {
367            writeln!(&mut Escaper(f), "{:#}", &self.0)
368        } else {
369            write!(&mut Escaper(f), "{}", &self.0)
370        }
371    }
372}
373
374/// Format data using a specific format function
375struct FnFmt<'a, T, F>(&'a T, F);
376
377impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
378where
379    F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
380{
381    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
382        self.1(self.0, f)
383    }
384}
385
386#[cfg(feature = "dot_parser")]
387#[macro_use]
388pub mod dot_parser;
389
390#[cfg(test)]
391mod test {
392    use alloc::{format, string::String};
393    use core::fmt::Write;
394
395    use super::{Config, Dot, Escaper, RankDir};
396    use crate::prelude::Graph;
397    use crate::visit::NodeRef;
398
399    #[test]
400    fn test_escape() {
401        let mut buff = String::new();
402        {
403            let mut e = Escaper(&mut buff);
404            let _ = e.write_str("\" \\ \n");
405        }
406        assert_eq!(buff, "\\\" \\\\ \\l");
407    }
408
409    fn simple_graph() -> Graph<&'static str, &'static str> {
410        let mut graph = Graph::<&str, &str>::new();
411        let a = graph.add_node("A");
412        let b = graph.add_node("B");
413        graph.add_edge(a, b, "edge_label");
414        graph
415    }
416
417    #[test]
418    fn test_nodeindexlable_option() {
419        let graph = simple_graph();
420        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeIndexLabel]));
421        assert_eq!(dot, "digraph {\n    0 [ label = \"0\" ]\n    1 [ label = \"1\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n");
422    }
423
424    #[test]
425    fn test_edgeindexlable_option() {
426        let graph = simple_graph();
427        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeIndexLabel]));
428        assert_eq!(dot, "digraph {\n    0 [ label = \"\\\"A\\\"\" ]\n    1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"0\" ]\n}\n");
429    }
430
431    #[test]
432    fn test_edgenolable_option() {
433        let graph = simple_graph();
434        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
435        assert_eq!(dot, "digraph {\n    0 [ label = \"\\\"A\\\"\" ]\n    1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ ]\n}\n");
436    }
437
438    #[test]
439    fn test_nodenolable_option() {
440        let graph = simple_graph();
441        let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeNoLabel]));
442        assert_eq!(
443            dot,
444            "digraph {\n    0 [ ]\n    1 [ ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
445        );
446    }
447
448    #[test]
449    fn test_rankdir_bt_option() {
450        let graph = simple_graph();
451        let dot = format!(
452            "{:?}",
453            Dot::with_config(&graph, &[Config::RankDir(RankDir::TB)])
454        );
455        assert_eq!(
456            dot,
457            "digraph {\n    rankdir=\"TB\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
458            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
459        );
460    }
461
462    #[test]
463    fn test_rankdir_tb_option() {
464        let graph = simple_graph();
465        let dot = format!(
466            "{:?}",
467            Dot::with_config(&graph, &[Config::RankDir(RankDir::BT)])
468        );
469        assert_eq!(
470            dot,
471            "digraph {\n    rankdir=\"BT\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
472            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
473        );
474    }
475
476    #[test]
477    fn test_rankdir_lr_option() {
478        let graph = simple_graph();
479        let dot = format!(
480            "{:?}",
481            Dot::with_config(&graph, &[Config::RankDir(RankDir::LR)])
482        );
483        assert_eq!(
484            dot,
485            "digraph {\n    rankdir=\"LR\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
486            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
487        );
488    }
489
490    #[test]
491    fn test_rankdir_rl_option() {
492        let graph = simple_graph();
493        let dot = format!(
494            "{:?}",
495            Dot::with_config(&graph, &[Config::RankDir(RankDir::RL)])
496        );
497        assert_eq!(
498            dot,
499            "digraph {\n    rankdir=\"RL\"\n    0 [ label = \"\\\"A\\\"\" ]\n    \
500            1 [ label = \"\\\"B\\\"\" ]\n    0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"
501        );
502    }
503
504    #[test]
505    fn test_with_attr_getters() {
506        let graph = simple_graph();
507        let dot = format!(
508            "{:?}",
509            Dot::with_attr_getters(
510                &graph,
511                &[Config::NodeNoLabel, Config::EdgeNoLabel],
512                &|_, er| format!("label = \"{}\"", er.weight().to_uppercase()),
513                &|_, nr| format!("label = \"{}\"", nr.weight().to_lowercase()),
514            ),
515        );
516        assert_eq!(dot, "digraph {\n    0 [ label = \"a\"]\n    1 [ label = \"b\"]\n    0 -> 1 [ label = \"EDGE_LABEL\"]\n}\n");
517    }
518}