this repo has no description
1use crate::{Graph, Result as GigabrainResult, GigabrainError};
2use crate::{NodeId};
3use crate::core::relationship::Direction;
4use crate::visualization::VisualizationOptions;
5use std::collections::HashSet;
6use std::sync::Arc;
7
8/// DOT format renderer for Graphviz visualization
9pub struct DotRenderer<'a> {
10 options: &'a VisualizationOptions,
11}
12
13impl<'a> DotRenderer<'a> {
14 pub fn new(options: &'a VisualizationOptions) -> Self {
15 Self { options }
16 }
17
18 pub async fn render(&self, graph: &Arc<Graph>) -> GigabrainResult<String> {
19 let nodes = graph.get_all_nodes();
20 let limited_nodes: Vec<NodeId> = if let Some(max) = self.options.max_nodes {
21 nodes.into_iter().take(max).collect()
22 } else {
23 nodes
24 };
25
26 let mut output = String::new();
27
28 // DOT header
29 output.push_str("digraph G {\n");
30 output.push_str(" rankdir=TB;\n");
31 output.push_str(" node [shape=ellipse];\n");
32 output.push_str(" edge [fontsize=10];\n");
33
34 // Apply layout-specific settings
35 match self.options.layout {
36 crate::visualization::LayoutAlgorithm::Hierarchical => {
37 output.push_str(" rankdir=TD;\n");
38 output.push_str(" ranksep=1.0;\n");
39 }
40 crate::visualization::LayoutAlgorithm::Circular => {
41 output.push_str(" layout=circo;\n");
42 }
43 crate::visualization::LayoutAlgorithm::Spring => {
44 output.push_str(" layout=fdp;\n");
45 output.push_str(" K=2.0;\n");
46 }
47 _ => {
48 output.push_str(" layout=dot;\n");
49 }
50 }
51
52 // Apply color scheme
53 let (node_color, edge_color, bg_color) = self.get_color_scheme();
54 output.push_str(&format!(" bgcolor=\"{}\";\n", bg_color));
55
56 // Apply node size
57 let node_size = self.get_node_size();
58 output.push_str(&format!(" node [width={}, height={}];\n", node_size.0, node_size.1));
59
60 // Apply font size
61 let font_size = self.get_font_size();
62 output.push_str(&format!(" node [fontsize={}];\n", font_size));
63 output.push_str(&format!(" edge [fontsize={}];\n", font_size - 2));
64
65 output.push_str("\n");
66
67 // Define nodes
68 for &node_id in &limited_nodes {
69 if let Some(node) = graph.get_node(node_id) {
70 let node_label = self.format_node_label(graph, node_id, &node)?;
71 let node_attrs = self.get_node_attributes(graph, &node)?;
72
73 output.push_str(&format!(" n{} [label=\"{}\", color=\"{}\", {}];\n",
74 node_id.0, node_label, node_color, node_attrs));
75 }
76 }
77
78 output.push_str("\n");
79
80 // Define edges
81 let mut processed_edges = HashSet::new();
82 let mut edge_count = 0;
83 let max_edges = self.options.max_relationships.unwrap_or(usize::MAX);
84
85 for &node_id in &limited_nodes {
86 if edge_count >= max_edges {
87 break;
88 }
89
90 let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None);
91 for rel in relationships {
92 if edge_count >= max_edges {
93 break;
94 }
95
96 let edge_key = (rel.start_node, rel.end_node, rel.rel_type);
97 if processed_edges.contains(&edge_key) {
98 continue;
99 }
100 processed_edges.insert(edge_key);
101
102 // Only include edges between nodes in our limited set
103 if limited_nodes.contains(&rel.end_node) {
104 let edge_label = self.format_edge_label(graph, &rel)?;
105 let edge_attrs = self.get_edge_attributes(&rel)?;
106
107 output.push_str(&format!(" n{} -> n{} [label=\"{}\", color=\"{}\", {}];\n",
108 rel.start_node.0, rel.end_node.0, edge_label, edge_color, edge_attrs));
109 edge_count += 1;
110 }
111 }
112 }
113
114 // Add graph metadata as comment
115 output.push_str("\n");
116 output.push_str(&format!(" // Graph metadata:\n"));
117 output.push_str(&format!(" // Nodes: {}\n", limited_nodes.len()));
118 output.push_str(&format!(" // Edges: {}\n", edge_count));
119 output.push_str(&format!(" // Layout: {:?}\n", self.options.layout));
120 output.push_str(&format!(" // Color scheme: {:?}\n", self.options.color_scheme));
121
122 output.push_str("}\n");
123
124 Ok(output)
125 }
126
127 fn format_node_label(&self, graph: &Arc<Graph>, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult<String> {
128 let mut label = format!("N{}", node_id.0);
129
130 if self.options.include_labels && !node.labels.is_empty() {
131 let schema = graph.schema().read();
132 let labels: Vec<String> = node.labels.iter()
133 .filter_map(|&label_id| schema.get_label_name(label_id))
134 .map(|name| name.to_string())
135 .collect();
136
137 if !labels.is_empty() {
138 label = format!("{}\\n:{}", label, labels.join(":"));
139 }
140 }
141
142 if self.options.include_properties && !node.properties.is_empty() {
143 let schema = graph.schema().read();
144 let mut props = Vec::new();
145
146 for (key_id, value) in node.properties.iter().take(3) { // Limit to first 3 properties
147 if let Some(key_name) = schema.get_property_key_name(*key_id) {
148 let value_str = self.format_property_value(value);
149 props.push(format!("{}:{}", key_name, value_str));
150 }
151 }
152
153 if !props.is_empty() {
154 label = format!("{}\\n{{{}}}", label, props.join("\\n"));
155 }
156
157 if node.properties.len() > 3 {
158 label = format!("{}\\n...", label);
159 }
160 }
161
162 Ok(label)
163 }
164
165 fn format_edge_label(&self, graph: &Arc<Graph>, rel: &crate::core::Relationship) -> GigabrainResult<String> {
166 let schema = graph.schema().read();
167 let rel_type_name = schema.get_relationship_type_name(rel.rel_type)
168 .map(|s| s.to_string())
169 .unwrap_or_else(|| "UNKNOWN".to_string());
170
171 let mut label = rel_type_name.to_string();
172
173 if self.options.include_properties && !rel.properties.is_empty() {
174 let mut props = Vec::new();
175
176 for (key_id, value) in rel.properties.iter().take(2) { // Limit to first 2 properties
177 if let Some(key_name) = schema.get_property_key_name(*key_id) {
178 let value_str = self.format_property_value(value);
179 props.push(format!("{}:{}", key_name, value_str));
180 }
181 }
182
183 if !props.is_empty() {
184 label = format!("{}\\n{{{}}}", label, props.join("\\n"));
185 }
186 }
187
188 Ok(label)
189 }
190
191 fn format_property_value(&self, value: &crate::core::PropertyValue) -> String {
192 match value {
193 crate::core::PropertyValue::String(s) => {
194 if s.len() > 10 {
195 format!("\"{}...\"", &s[..10])
196 } else {
197 format!("\"{}\"", s)
198 }
199 }
200 crate::core::PropertyValue::Integer(i) => i.to_string(),
201 crate::core::PropertyValue::Float(f) => format!("{:.2}", f),
202 crate::core::PropertyValue::Boolean(b) => b.to_string(),
203 crate::core::PropertyValue::Null => "null".to_string(),
204 crate::core::PropertyValue::List(list) => {
205 let items: Vec<String> = list.iter().take(3).map(|v| self.format_property_value(v)).collect();
206 if list.len() > 3 {
207 format!("[{}, ...]", items.join(", "))
208 } else {
209 format!("[{}]", items.join(", "))
210 }
211 }
212 crate::core::PropertyValue::Map(map) => {
213 let items: Vec<String> = map.iter().take(2).map(|(k, v)| {
214 format!("{}:{}", k, self.format_property_value(v))
215 }).collect();
216 if map.len() > 2 {
217 format!("{{{}, ...}}", items.join(", "))
218 } else {
219 format!("{{{}}}", items.join(", "))
220 }
221 }
222 }
223 }
224
225 fn get_node_attributes(&self, _graph: &Arc<Graph>, node: &crate::core::Node) -> GigabrainResult<String> {
226 let mut attrs = Vec::new();
227
228 // Node style based on number of labels
229 if node.labels.len() > 1 {
230 attrs.push("style=filled".to_string());
231 attrs.push("fillcolor=\"lightblue\"".to_string());
232 } else if node.labels.len() == 1 {
233 attrs.push("style=filled".to_string());
234 attrs.push("fillcolor=\"lightgray\"".to_string());
235 }
236
237 // Node shape based on properties
238 if node.properties.is_empty() {
239 attrs.push("shape=circle".to_string());
240 } else {
241 attrs.push("shape=ellipse".to_string());
242 }
243
244 Ok(attrs.join(", "))
245 }
246
247 fn get_edge_attributes(&self, _rel: &crate::core::Relationship) -> GigabrainResult<String> {
248 let mut attrs = Vec::new();
249
250 // Edge style based on properties
251 if !_rel.properties.is_empty() {
252 attrs.push("style=bold".to_string());
253 }
254
255 attrs.push("arrowhead=normal".to_string());
256
257 Ok(attrs.join(", "))
258 }
259
260 fn get_color_scheme(&self) -> (&'static str, &'static str, &'static str) {
261 match self.options.color_scheme {
262 crate::visualization::ColorScheme::Dark => ("#ffffff", "#cccccc", "#2d2d2d"),
263 crate::visualization::ColorScheme::Light => ("#000000", "#333333", "#ffffff"),
264 crate::visualization::ColorScheme::Colorful => ("#1f77b4", "#ff7f0e", "#ffffff"),
265 crate::visualization::ColorScheme::Monochrome => ("#000000", "#666666", "#ffffff"),
266 crate::visualization::ColorScheme::Default => ("#000000", "#333333", "#ffffff"),
267 }
268 }
269
270 fn get_node_size(&self) -> (f32, f32) {
271 match self.options.node_size {
272 crate::visualization::NodeSize::Small => (0.3, 0.3),
273 crate::visualization::NodeSize::Medium => (0.5, 0.5),
274 crate::visualization::NodeSize::Large => (0.8, 0.8),
275 }
276 }
277
278 fn get_font_size(&self) -> u32 {
279 match self.options.font_size {
280 crate::visualization::FontSize::Small => 8,
281 crate::visualization::FontSize::Medium => 10,
282 crate::visualization::FontSize::Large => 14,
283 }
284 }
285}