this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 285 lines 11 kB view raw
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}