372 lines
10 KiB
Rust
372 lines
10 KiB
Rust
//! Simple graphviz dot file format output.
|
|
|
|
use std::fmt::{self, Display, Write};
|
|
|
|
use crate::visit::{
|
|
EdgeRef, GraphProp, 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,
|
|
get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
|
|
get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
|
|
config: Configs,
|
|
}
|
|
|
|
static TYPE: [&str; 2] = ["graph", "digraph"];
|
|
static EDGE: [&str; 2] = ["--", "->"];
|
|
static INDENT: &str = " ";
|
|
|
|
impl<'a, G> Dot<'a, G>
|
|
where
|
|
G: IntoNodeReferences + IntoEdgeReferences,
|
|
{
|
|
/// Create a `Dot` formatting wrapper with default configuration.
|
|
#[inline]
|
|
pub fn new(graph: G) -> Self {
|
|
Self::with_config(graph, &[])
|
|
}
|
|
|
|
/// Create a `Dot` formatting wrapper with custom configuration.
|
|
#[inline]
|
|
pub fn with_config(graph: G, config: &'a [Config]) -> Self {
|
|
Self::with_attr_getters(graph, config, &|_, _| String::new(), &|_, _| String::new())
|
|
}
|
|
|
|
#[inline]
|
|
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 {
|
|
let config = Configs::extract(config);
|
|
Dot {
|
|
graph,
|
|
get_edge_attributes,
|
|
get_node_attributes,
|
|
config,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `Dot` configuration.
|
|
///
|
|
/// This enum does not have an exhaustive definition (will be expanded)
|
|
// TODO: #[non_exhaustive] once MSRV >= 1.40,
|
|
// and/or for a breaking change make this something like an EnumSet: https://docs.rs/enumset
|
|
#[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(()),
|
|
}
|
|
macro_rules! make_config_struct {
|
|
($($variant:ident,)*) => {
|
|
#[allow(non_snake_case)]
|
|
#[derive(Default)]
|
|
struct Configs {
|
|
$($variant: bool,)*
|
|
}
|
|
impl Configs {
|
|
#[inline]
|
|
fn extract(configs: &[Config]) -> Self {
|
|
let mut conf = Self::default();
|
|
for c in configs {
|
|
match *c {
|
|
$(Config::$variant => conf.$variant = true,)*
|
|
Config::_Incomplete(()) => {}
|
|
}
|
|
}
|
|
conf
|
|
}
|
|
}
|
|
}
|
|
}
|
|
make_config_struct!(
|
|
NodeIndexLabel,
|
|
EdgeIndexLabel,
|
|
EdgeNoLabel,
|
|
NodeNoLabel,
|
|
GraphContentOnly,
|
|
);
|
|
|
|
impl<G> Dot<'_, G>
|
|
where
|
|
G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp,
|
|
{
|
|
fn graph_fmt<NF, EF>(&self, f: &mut fmt::Formatter, node_fmt: NF, edge_fmt: EF) -> fmt::Result
|
|
where
|
|
NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
|
|
EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
|
|
{
|
|
let g = self.graph;
|
|
if !self.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.NodeNoLabel {
|
|
write!(f, "label = \"")?;
|
|
if self.config.NodeIndexLabel {
|
|
write!(f, "{}", g.to_index(node.id()))?;
|
|
} else {
|
|
Escaped(FnFmt(node.weight(), &node_fmt)).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.EdgeNoLabel {
|
|
write!(f, "label = \"")?;
|
|
if self.config.EdgeIndexLabel {
|
|
write!(f, "{}", i)?;
|
|
} else {
|
|
Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
|
|
}
|
|
write!(f, "\" ")?;
|
|
}
|
|
writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
|
|
}
|
|
|
|
if !self.config.GraphContentOnly {
|
|
writeln!(f, "}}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<G> fmt::Display for Dot<'_, 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(f, fmt::Display::fmt, fmt::Display::fmt)
|
|
}
|
|
}
|
|
|
|
impl<G> fmt::LowerHex for Dot<'_, G>
|
|
where
|
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
|
|
G::EdgeWeight: fmt::LowerHex,
|
|
G::NodeWeight: fmt::LowerHex,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
|
|
}
|
|
}
|
|
|
|
impl<G> fmt::UpperHex for Dot<'_, G>
|
|
where
|
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp,
|
|
G::EdgeWeight: fmt::UpperHex,
|
|
G::NodeWeight: fmt::UpperHex,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
|
|
}
|
|
}
|
|
|
|
impl<G> fmt::Debug for Dot<'_, 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(f, fmt::Debug::fmt, fmt::Debug::fmt)
|
|
}
|
|
}
|
|
|
|
/// Escape for Graphviz
|
|
struct Escaper<W>(W);
|
|
|
|
impl<W> fmt::Write for Escaper<W>
|
|
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>(T);
|
|
|
|
impl<T> fmt::Display for Escaped<T>
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format data using a specific format function
|
|
struct FnFmt<'a, T, F>(&'a T, F);
|
|
|
|
impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
|
|
where
|
|
F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
self.1(self.0, 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");
|
|
}
|
|
}
|