use crate::graph::traits::GraphQuery;
use crate::graph::Graph;
use std::fmt::Write;
#[derive(Clone, Debug)]
pub struct DotOptions {
pub show_node_labels: bool,
pub show_edge_labels: bool,
pub show_node_indices: bool,
pub graph_name: Option<String>,
pub graph_attributes: Vec<(String, String)>,
pub node_attributes: Vec<(String, String)>,
pub edge_attributes: Vec<(String, String)>,
}
impl Default for DotOptions {
fn default() -> Self {
Self {
show_node_labels: true,
show_edge_labels: true,
show_node_indices: true,
graph_name: None,
graph_attributes: Vec::new(),
node_attributes: vec![
("shape".to_string(), "circle".to_string()),
("style".to_string(), "filled".to_string()),
("fillcolor".to_string(), "lightblue".to_string()),
],
edge_attributes: vec![
("color".to_string(), "gray".to_string()),
("arrowhead".to_string(), "vee".to_string()),
],
}
}
}
impl DotOptions {
pub fn new() -> Self {
Self::default()
}
pub fn undirected(mut self) -> Self {
self.edge_attributes.retain(|(k, _)| k != "arrowhead");
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.graph_name = Some(name.into());
self
}
pub fn hide_node_labels(mut self) -> Self {
self.show_node_labels = false;
self
}
pub fn hide_edge_labels(mut self) -> Self {
self.show_edge_labels = false;
self
}
}
pub fn to_dot<T, E>(graph: &Graph<T, E>) -> String
where
T: std::fmt::Display,
E: std::fmt::Display,
{
to_dot_with_options(graph, &DotOptions::default())
}
pub fn to_dot_with_options<T, E>(graph: &Graph<T, E>, options: &DotOptions) -> String
where
T: std::fmt::Display,
E: std::fmt::Display,
{
let mut output = String::new();
let graph_type = "digraph";
let name = options.graph_name.as_deref().unwrap_or("G");
writeln!(&mut output, "{} {} {{", graph_type, name).unwrap();
for (key, value) in &options.graph_attributes {
writeln!(&mut output, " {} = {};", key, value).unwrap();
}
if !options.node_attributes.is_empty() {
write!(&mut output, " node [").unwrap();
for (i, (key, value)) in options.node_attributes.iter().enumerate() {
if i > 0 {
write!(&mut output, ", ").unwrap();
}
write!(&mut output, "{} = {}", key, value).unwrap();
}
writeln!(&mut output, "];").unwrap();
}
if !options.edge_attributes.is_empty() {
write!(&mut output, " edge [").unwrap();
for (i, (key, value)) in options.edge_attributes.iter().enumerate() {
if i > 0 {
write!(&mut output, ", ").unwrap();
}
write!(&mut output, "{} = {}", key, value).unwrap();
}
writeln!(&mut output, "];").unwrap();
}
writeln!(&mut output).unwrap();
for node in graph.nodes() {
let idx = node.index();
let label = if options.show_node_labels && options.show_node_indices {
format!("{}: {}", idx, node.data())
} else if options.show_node_labels {
format!("{}", node.data())
} else if options.show_node_indices {
format!("{}", idx)
} else {
String::new()
};
if label.is_empty() {
writeln!(&mut output, " {};", idx).unwrap();
} else {
writeln!(&mut output, " {} [label=\"{}\"];", idx, escape_dot(&label)).unwrap();
}
}
writeln!(&mut output).unwrap();
for edge in graph.edges() {
let source = edge.source().index();
let target = edge.target().index();
let edge_def = if options.show_edge_labels {
format!(" [label=\"{}\"]", escape_dot(&format!("{}", edge.data())))
} else {
String::new()
};
writeln!(&mut output, " {} -> {}{};", source, target, edge_def).unwrap();
}
writeln!(&mut output, "}}").unwrap();
output
}
pub fn to_dot_undirected<T, E>(graph: &Graph<T, E>) -> String
where
T: std::fmt::Display,
E: std::fmt::Display,
{
let mut output = String::new();
writeln!(&mut output, "graph G {{").unwrap();
writeln!(
&mut output,
" node [shape=circle, style=filled, fillcolor=lightblue];"
)
.unwrap();
writeln!(&mut output).unwrap();
for node in graph.nodes() {
let idx = node.index();
writeln!(
&mut output,
" {} [label=\"{}: {}\"];",
idx,
idx,
node.data()
)
.unwrap();
}
writeln!(&mut output).unwrap();
for edge in graph.edges() {
let source = edge.source().index();
let target = edge.target().index();
writeln!(
&mut output,
" {} -- {} [label=\"{}\"];",
source,
target,
edge.data()
)
.unwrap();
}
writeln!(&mut output, "}}").unwrap();
output
}
fn escape_dot(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
pub fn write_dot_to_file(dot: &str, path: &str) -> std::io::Result<()> {
std::fs::write(path, dot)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::builders::GraphBuilder;
#[test]
fn test_dot_export_basic() {
let graph = GraphBuilder::directed()
.with_nodes(vec!["A", "B"])
.with_edge(0, 1, 1.0)
.build()
.unwrap();
let dot = to_dot(&graph);
assert!(dot.contains("digraph"));
assert!(dot.contains("A"));
assert!(dot.contains("B"));
assert!(dot.contains("->"));
}
#[test]
fn test_dot_export_with_options() {
let graph = GraphBuilder::directed()
.with_nodes(vec!["A", "B", "C"])
.with_edges(vec![(0, 1, 1.0), (1, 2, 2.0)])
.build()
.unwrap();
let options = DotOptions::new().with_name("MyGraph").hide_edge_labels();
let dot = to_dot_with_options(&graph, &options);
assert!(dot.contains("digraph MyGraph"));
assert!(!dot.contains("-> [label="));
}
#[test]
fn test_dot_escaping() {
assert_eq!(escape_dot("hello"), "hello");
assert_eq!(escape_dot("he\"llo"), "he\\\"llo");
assert_eq!(escape_dot("he\\llo"), "he\\\\llo");
assert_eq!(escape_dot("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_dot_empty_graph() {
let graph = GraphBuilder::<String, f64>::directed().build().unwrap();
let dot = to_dot(&graph);
assert!(dot.contains("digraph"));
assert!(dot.contains("{"));
assert!(dot.contains("}"));
}
#[test]
fn test_dot_undirected() {
let graph = GraphBuilder::undirected()
.with_nodes(vec!["A", "B"])
.with_edge(0, 1, 1.0)
.build()
.unwrap();
let dot = to_dot_undirected(&graph);
assert!(dot.contains("graph"));
assert!(dot.contains("--")); }
}