Skip to main content

god_graph/export/
dot.rs

1//! DOT 格式导出(Graphviz)
2//!
3//! 支持将图导出为 Graphviz DOT 格式,用于可视化
4//!
5//! ## 使用示例
6//!
7//! ```
8//! use god_gragh::prelude::*;
9//!
10//! let mut graph = Graph::<&str, f64>::directed();
11//! let a = graph.add_node("A").unwrap();
12//! let b = graph.add_node("B").unwrap();
13//! graph.add_edge(a, b, 1.0).unwrap();
14//!
15//! let dot = to_dot(&graph);
16//! println!("{}", dot);
17//! ```
18
19use crate::graph::traits::GraphQuery;
20use crate::graph::Graph;
21use std::fmt::Write;
22
23/// DOT 格式导出选项
24#[derive(Clone, Debug)]
25pub struct DotOptions {
26    /// 是否显示节点标签
27    pub show_node_labels: bool,
28    /// 是否显示边标签
29    pub show_edge_labels: bool,
30    /// 是否显示节点索引
31    pub show_node_indices: bool,
32    /// 图的名称(用于 label)
33    pub graph_name: Option<String>,
34    /// 额外的图属性
35    pub graph_attributes: Vec<(String, String)>,
36    /// 节点默认属性
37    pub node_attributes: Vec<(String, String)>,
38    /// 边默认属性
39    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    /// 创建默认选项
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// 设置为无向图样式
70    pub fn undirected(mut self) -> Self {
71        self.edge_attributes.retain(|(k, _)| k != "arrowhead");
72        self
73    }
74
75    /// 设置图名称
76    pub fn with_name(mut self, name: impl Into<String>) -> Self {
77        self.graph_name = Some(name.into());
78        self
79    }
80
81    /// 隐藏节点标签
82    pub fn hide_node_labels(mut self) -> Self {
83        self.show_node_labels = false;
84        self
85    }
86
87    /// 隐藏边标签
88    pub fn hide_edge_labels(mut self) -> Self {
89        self.show_edge_labels = false;
90        self
91    }
92}
93
94/// 使用默认选项将图导出为 DOT 格式
95pub 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
103/// 使用自定义选项将图导出为 DOT 格式
104///
105/// # 参数
106/// * `graph` - 要导出的图
107/// * `options` - DOT 导出选项
108///
109/// # 返回
110/// DOT 格式字符串
111pub 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    // 图声明:digraph(有向)或 graph(无向)
119    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    // 图属性
124    for (key, value) in &options.graph_attributes {
125        writeln!(&mut output, "  {} = {};", key, value).unwrap();
126    }
127
128    // 节点默认属性
129    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    // 边默认属性
141    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    // 导出节点
155    for node in graph.nodes() {
156        let idx = node.index();
157
158        // 构建节点标签
159        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    // 导出边
179    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
196/// 导出无向图的 DOT 格式
197///
198/// 使用 `--` 而不是 `->` 表示边
199pub 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    // 导出节点
215    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    // 导出边(无向图使用 --)
230    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
247/// 转义 DOT 字符串中的特殊字符
248fn escape_dot(s: &str) -> String {
249    s.replace('\\', "\\\\")
250        .replace('"', "\\\"")
251        .replace('\n', "\\n")
252        .replace('\r', "\\r")
253        .replace('\t', "\\t")
254}
255
256/// 将 DOT 字符串写入文件
257///
258/// # 参数
259/// * `dot` - DOT 格式字符串
260/// * `path` - 输出文件路径
261///
262/// # 返回
263/// 成功返回 Ok(()),失败返回 IO 错误
264pub 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        // 边标签已隐藏,但节点标签仍然存在
301        // 检查没有边标签(格式为 [label="x"] 在边定义中)
302        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("--")); // 无向图使用 --
334    }
335}