1use crate::graph::traits::GraphQuery;
20use crate::graph::Graph;
21use std::fmt::Write;
22
23#[derive(Clone, Debug)]
25pub struct DotOptions {
26 pub show_node_labels: bool,
28 pub show_edge_labels: bool,
30 pub show_node_indices: bool,
32 pub graph_name: Option<String>,
34 pub graph_attributes: Vec<(String, String)>,
36 pub node_attributes: Vec<(String, String)>,
38 pub edge_attributes: Vec<(String, String)>,
40}
41
42impl Default for DotOptions {
43 fn default() -> Self {
44 Self {
45 show_node_labels: true,
46 show_edge_labels: true,
47 show_node_indices: true,
48 graph_name: None,
49 graph_attributes: Vec::new(),
50 node_attributes: vec![
51 ("shape".to_string(), "circle".to_string()),
52 ("style".to_string(), "filled".to_string()),
53 ("fillcolor".to_string(), "lightblue".to_string()),
54 ],
55 edge_attributes: vec![
56 ("color".to_string(), "gray".to_string()),
57 ("arrowhead".to_string(), "vee".to_string()),
58 ],
59 }
60 }
61}
62
63impl DotOptions {
64 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn undirected(mut self) -> Self {
71 self.edge_attributes.retain(|(k, _)| k != "arrowhead");
72 self
73 }
74
75 pub fn with_name(mut self, name: impl Into<String>) -> Self {
77 self.graph_name = Some(name.into());
78 self
79 }
80
81 pub fn hide_node_labels(mut self) -> Self {
83 self.show_node_labels = false;
84 self
85 }
86
87 pub fn hide_edge_labels(mut self) -> Self {
89 self.show_edge_labels = false;
90 self
91 }
92}
93
94pub fn to_dot<T, E>(graph: &Graph<T, E>) -> String
96where
97 T: std::fmt::Display,
98 E: std::fmt::Display,
99{
100 to_dot_with_options(graph, &DotOptions::default())
101}
102
103pub fn to_dot_with_options<T, E>(graph: &Graph<T, E>, options: &DotOptions) -> String
112where
113 T: std::fmt::Display,
114 E: std::fmt::Display,
115{
116 let mut output = String::new();
117
118 let graph_type = "digraph";
120 let name = options.graph_name.as_deref().unwrap_or("G");
121 writeln!(&mut output, "{} {} {{", graph_type, name).unwrap();
122
123 for (key, value) in &options.graph_attributes {
125 writeln!(&mut output, " {} = {};", key, value).unwrap();
126 }
127
128 if !options.node_attributes.is_empty() {
130 write!(&mut output, " node [").unwrap();
131 for (i, (key, value)) in options.node_attributes.iter().enumerate() {
132 if i > 0 {
133 write!(&mut output, ", ").unwrap();
134 }
135 write!(&mut output, "{} = {}", key, value).unwrap();
136 }
137 writeln!(&mut output, "];").unwrap();
138 }
139
140 if !options.edge_attributes.is_empty() {
142 write!(&mut output, " edge [").unwrap();
143 for (i, (key, value)) in options.edge_attributes.iter().enumerate() {
144 if i > 0 {
145 write!(&mut output, ", ").unwrap();
146 }
147 write!(&mut output, "{} = {}", key, value).unwrap();
148 }
149 writeln!(&mut output, "];").unwrap();
150 }
151
152 writeln!(&mut output).unwrap();
153
154 for node in graph.nodes() {
156 let idx = node.index();
157
158 let label = if options.show_node_labels && options.show_node_indices {
160 format!("{}: {}", idx, node.data())
161 } else if options.show_node_labels {
162 format!("{}", node.data())
163 } else if options.show_node_indices {
164 format!("{}", idx)
165 } else {
166 String::new()
167 };
168
169 if label.is_empty() {
170 writeln!(&mut output, " {};", idx).unwrap();
171 } else {
172 writeln!(&mut output, " {} [label=\"{}\"];", idx, escape_dot(&label)).unwrap();
173 }
174 }
175
176 writeln!(&mut output).unwrap();
177
178 for edge in graph.edges() {
180 let source = edge.source().index();
181 let target = edge.target().index();
182
183 let edge_def = if options.show_edge_labels {
184 format!(" [label=\"{}\"]", escape_dot(&format!("{}", edge.data())))
185 } else {
186 String::new()
187 };
188
189 writeln!(&mut output, " {} -> {}{};", source, target, edge_def).unwrap();
190 }
191
192 writeln!(&mut output, "}}").unwrap();
193 output
194}
195
196pub fn to_dot_undirected<T, E>(graph: &Graph<T, E>) -> String
200where
201 T: std::fmt::Display,
202 E: std::fmt::Display,
203{
204 let mut output = String::new();
205
206 writeln!(&mut output, "graph G {{").unwrap();
207 writeln!(
208 &mut output,
209 " node [shape=circle, style=filled, fillcolor=lightblue];"
210 )
211 .unwrap();
212 writeln!(&mut output).unwrap();
213
214 for node in graph.nodes() {
216 let idx = node.index();
217 writeln!(
218 &mut output,
219 " {} [label=\"{}: {}\"];",
220 idx,
221 idx,
222 node.data()
223 )
224 .unwrap();
225 }
226
227 writeln!(&mut output).unwrap();
228
229 for edge in graph.edges() {
231 let source = edge.source().index();
232 let target = edge.target().index();
233 writeln!(
234 &mut output,
235 " {} -- {} [label=\"{}\"];",
236 source,
237 target,
238 edge.data()
239 )
240 .unwrap();
241 }
242
243 writeln!(&mut output, "}}").unwrap();
244 output
245}
246
247fn escape_dot(s: &str) -> String {
249 s.replace('\\', "\\\\")
250 .replace('"', "\\\"")
251 .replace('\n', "\\n")
252 .replace('\r', "\\r")
253 .replace('\t', "\\t")
254}
255
256pub fn write_dot_to_file(dot: &str, path: &str) -> std::io::Result<()> {
265 std::fs::write(path, dot)
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::graph::builders::GraphBuilder;
272
273 #[test]
274 fn test_dot_export_basic() {
275 let graph = GraphBuilder::directed()
276 .with_nodes(vec!["A", "B"])
277 .with_edge(0, 1, 1.0)
278 .build()
279 .unwrap();
280
281 let dot = to_dot(&graph);
282 assert!(dot.contains("digraph"));
283 assert!(dot.contains("A"));
284 assert!(dot.contains("B"));
285 assert!(dot.contains("->"));
286 }
287
288 #[test]
289 fn test_dot_export_with_options() {
290 let graph = GraphBuilder::directed()
291 .with_nodes(vec!["A", "B", "C"])
292 .with_edges(vec![(0, 1, 1.0), (1, 2, 2.0)])
293 .build()
294 .unwrap();
295
296 let options = DotOptions::new().with_name("MyGraph").hide_edge_labels();
297
298 let dot = to_dot_with_options(&graph, &options);
299 assert!(dot.contains("digraph MyGraph"));
300 assert!(!dot.contains("-> [label="));
303 }
304
305 #[test]
306 fn test_dot_escaping() {
307 assert_eq!(escape_dot("hello"), "hello");
308 assert_eq!(escape_dot("he\"llo"), "he\\\"llo");
309 assert_eq!(escape_dot("he\\llo"), "he\\\\llo");
310 assert_eq!(escape_dot("line1\nline2"), "line1\\nline2");
311 }
312
313 #[test]
314 fn test_dot_empty_graph() {
315 let graph = GraphBuilder::<String, f64>::directed().build().unwrap();
316
317 let dot = to_dot(&graph);
318 assert!(dot.contains("digraph"));
319 assert!(dot.contains("{"));
320 assert!(dot.contains("}"));
321 }
322
323 #[test]
324 fn test_dot_undirected() {
325 let graph = GraphBuilder::undirected()
326 .with_nodes(vec!["A", "B"])
327 .with_edge(0, 1, 1.0)
328 .build()
329 .unwrap();
330
331 let dot = to_dot_undirected(&graph);
332 assert!(dot.contains("graph"));
333 assert!(dot.contains("--")); }
335}