//! Simple graphviz dot file format output. use std::fmt::{self, Display, Write}; use crate::visit::{ Data, EdgeRef, GraphBase, GraphProp, GraphRef, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef, }; /// `Dot` implements output to graphviz .dot format for a graph. /// /// Formatting and options are rather simple, this is mostly intended /// for debugging. Exact output may change. /// /// # Examples /// /// ``` /// use petgraph::Graph; /// use petgraph::dot::{Dot, Config}; /// /// let mut graph = Graph::<_, ()>::new(); /// graph.add_node("A"); /// graph.add_node("B"); /// graph.add_node("C"); /// graph.add_node("D"); /// graph.extend_with_edges(&[ /// (0, 1), (0, 2), (0, 3), /// (1, 2), (1, 3), /// (2, 3), /// ]); /// /// println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); /// /// // In this case the output looks like this: /// // /// // digraph { /// // 0 [label="\"A\""] /// // 1 [label="\"B\""] /// // 2 [label="\"C\""] /// // 3 [label="\"D\""] /// // 0 -> 1 /// // 0 -> 2 /// // 0 -> 3 /// // 1 -> 2 /// // 1 -> 3 /// // 2 -> 3 /// // } /// /// // If you need multiple config options, just list them all in the slice. /// ``` pub struct Dot<'a, G> where G: IntoEdgeReferences + IntoNodeReferences, { graph: G, config: &'a [Config], get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String, get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String, } static TYPE: [&str; 2] = ["graph", "digraph"]; static EDGE: [&str; 2] = ["--", "->"]; static INDENT: &str = " "; impl<'a, G> Dot<'a, G> where G: GraphRef + IntoEdgeReferences + IntoNodeReferences, { /// Create a `Dot` formatting wrapper with default configuration. pub fn new(graph: G) -> Self { Self::with_config(graph, &[]) } /// Create a `Dot` formatting wrapper with custom configuration. pub fn with_config(graph: G, config: &'a [Config]) -> Self { Self::with_attr_getters(graph, config, &|_, _| "".to_string(), &|_, _| { "".to_string() }) } pub fn with_attr_getters( graph: G, config: &'a [Config], get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String, get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String, ) -> Self { Dot { graph, config, get_edge_attributes, get_node_attributes, } } } /// `Dot` configuration. /// /// This enum does not have an exhaustive definition (will be expanded) #[derive(Debug, PartialEq, Eq)] pub enum Config { /// Use indices for node labels. NodeIndexLabel, /// Use indices for edge labels. EdgeIndexLabel, /// Use no edge labels. EdgeNoLabel, /// Use no node labels. NodeNoLabel, /// Do not print the graph/digraph string. GraphContentOnly, #[doc(hidden)] _Incomplete(()), } impl<'a, G> Dot<'a, G> where G: GraphBase + IntoNodeReferences + IntoEdgeReferences, { fn graph_fmt( &self, g: G, f: &mut fmt::Formatter, mut node_fmt: NF, mut edge_fmt: EF, ) -> fmt::Result where G: NodeIndexable + IntoNodeReferences + IntoEdgeReferences, G: GraphProp + GraphBase, G: Data, NF: FnMut(&NW, &mut dyn FnMut(&dyn Display) -> fmt::Result) -> fmt::Result, EF: FnMut(&EW, &mut dyn FnMut(&dyn Display) -> fmt::Result) -> fmt::Result, { if !self.config.contains(&Config::GraphContentOnly) { writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?; } // output all labels for node in g.node_references() { write!(f, "{}{} [ ", INDENT, g.to_index(node.id()),)?; if !self.config.contains(&Config::NodeNoLabel) { write!(f, "label = \"")?; if self.config.contains(&Config::NodeIndexLabel) { write!(f, "{}", g.to_index(node.id()))?; } else { node_fmt(node.weight(), &mut |d| Escaped(d).fmt(f))?; } write!(f, "\" ")?; } writeln!(f, "{}]", (self.get_node_attributes)(g, node))?; } // output all edges for (i, edge) in g.edge_references().enumerate() { write!( f, "{}{} {} {} [ ", INDENT, g.to_index(edge.source()), EDGE[g.is_directed() as usize], g.to_index(edge.target()), )?; if !self.config.contains(&Config::EdgeNoLabel) { write!(f, "label = \"")?; if self.config.contains(&Config::EdgeIndexLabel) { write!(f, "{}", i)?; } else { edge_fmt(edge.weight(), &mut |d| Escaped(d).fmt(f))?; } write!(f, "\" ")?; } writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?; } if !self.config.contains(&Config::GraphContentOnly) { writeln!(f, "}}")?; } Ok(()) } } impl<'a, G> fmt::Display for Dot<'a, G> where G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp, G::EdgeWeight: fmt::Display, G::NodeWeight: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.graph_fmt(self.graph, f, |n, cb| cb(n), |e, cb| cb(e)) } } impl<'a, G> fmt::Debug for Dot<'a, G> where G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp, G::EdgeWeight: fmt::Debug, G::NodeWeight: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.graph_fmt( self.graph, f, |n, cb| cb(&DebugFmt(n)), |e, cb| cb(&DebugFmt(e)), ) } } /// Escape for Graphviz struct Escaper(W); impl fmt::Write for Escaper where W: fmt::Write, { fn write_str(&mut self, s: &str) -> fmt::Result { for c in s.chars() { self.write_char(c)?; } Ok(()) } fn write_char(&mut self, c: char) -> fmt::Result { match c { '"' | '\\' => self.0.write_char('\\')?, // \l is for left justified linebreak '\n' => return self.0.write_str("\\l"), _ => {} } self.0.write_char(c) } } /// Pass Display formatting through a simple escaping filter struct Escaped(T); impl fmt::Display for Escaped where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if f.alternate() { writeln!(&mut Escaper(f), "{:#}", &self.0) } else { write!(&mut Escaper(f), "{}", &self.0) } } } /// Pass Debug formatting to Display struct DebugFmt(T); impl fmt::Display for DebugFmt where T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) } } #[cfg(test)] mod test { use super::{Config, Dot, Escaper}; use crate::prelude::Graph; use crate::visit::NodeRef; use std::fmt::Write; #[test] fn test_escape() { let mut buff = String::new(); { let mut e = Escaper(&mut buff); let _ = e.write_str("\" \\ \n"); } assert_eq!(buff, "\\\" \\\\ \\l"); } fn simple_graph() -> Graph<&'static str, &'static str> { let mut graph = Graph::<&str, &str>::new(); let a = graph.add_node("A"); let b = graph.add_node("B"); graph.add_edge(a, b, "edge_label"); graph } #[test] fn test_nodeindexlable_option() { let graph = simple_graph(); let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeIndexLabel])); assert_eq!(dot, "digraph {\n 0 [ label = \"0\" ]\n 1 [ label = \"1\" ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n"); } #[test] fn test_edgeindexlable_option() { let graph = simple_graph(); let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeIndexLabel])); assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ label = \"0\" ]\n}\n"); } #[test] fn test_edgenolable_option() { let graph = simple_graph(); let dot = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); assert_eq!(dot, "digraph {\n 0 [ label = \"\\\"A\\\"\" ]\n 1 [ label = \"\\\"B\\\"\" ]\n 0 -> 1 [ ]\n}\n"); } #[test] fn test_nodenolable_option() { let graph = simple_graph(); let dot = format!("{:?}", Dot::with_config(&graph, &[Config::NodeNoLabel])); assert_eq!( dot, "digraph {\n 0 [ ]\n 1 [ ]\n 0 -> 1 [ label = \"\\\"edge_label\\\"\" ]\n}\n" ); } #[test] fn test_with_attr_getters() { let graph = simple_graph(); let dot = format!( "{:?}", Dot::with_attr_getters( &graph, &[Config::NodeNoLabel, Config::EdgeNoLabel], &|_, er| format!("label = \"{}\"", er.weight().to_uppercase()), &|_, nr| format!("label = \"{}\"", nr.weight().to_lowercase()), ), ); assert_eq!(dot, "digraph {\n 0 [ label = \"a\"]\n 1 [ label = \"b\"]\n 0 -> 1 [ label = \"EDGE_LABEL\"]\n}\n"); } }