use crate::{Graph, Result as GigabrainResult, GigabrainError}; use crate::{NodeId}; use crate::core::relationship::Direction; use crate::visualization::VisualizationOptions; use std::collections::HashSet; use std::sync::Arc; /// DOT format renderer for Graphviz visualization pub struct DotRenderer<'a> { options: &'a VisualizationOptions, } impl<'a> DotRenderer<'a> { pub fn new(options: &'a VisualizationOptions) -> Self { Self { options } } pub async fn render(&self, graph: &Arc) -> GigabrainResult { let nodes = graph.get_all_nodes(); let limited_nodes: Vec = if let Some(max) = self.options.max_nodes { nodes.into_iter().take(max).collect() } else { nodes }; let mut output = String::new(); // DOT header output.push_str("digraph G {\n"); output.push_str(" rankdir=TB;\n"); output.push_str(" node [shape=ellipse];\n"); output.push_str(" edge [fontsize=10];\n"); // Apply layout-specific settings match self.options.layout { crate::visualization::LayoutAlgorithm::Hierarchical => { output.push_str(" rankdir=TD;\n"); output.push_str(" ranksep=1.0;\n"); } crate::visualization::LayoutAlgorithm::Circular => { output.push_str(" layout=circo;\n"); } crate::visualization::LayoutAlgorithm::Spring => { output.push_str(" layout=fdp;\n"); output.push_str(" K=2.0;\n"); } _ => { output.push_str(" layout=dot;\n"); } } // Apply color scheme let (node_color, edge_color, bg_color) = self.get_color_scheme(); output.push_str(&format!(" bgcolor=\"{}\";\n", bg_color)); // Apply node size let node_size = self.get_node_size(); output.push_str(&format!(" node [width={}, height={}];\n", node_size.0, node_size.1)); // Apply font size let font_size = self.get_font_size(); output.push_str(&format!(" node [fontsize={}];\n", font_size)); output.push_str(&format!(" edge [fontsize={}];\n", font_size - 2)); output.push_str("\n"); // Define nodes for &node_id in &limited_nodes { if let Some(node) = graph.get_node(node_id) { let node_label = self.format_node_label(graph, node_id, &node)?; let node_attrs = self.get_node_attributes(graph, &node)?; output.push_str(&format!(" n{} [label=\"{}\", color=\"{}\", {}];\n", node_id.0, node_label, node_color, node_attrs)); } } output.push_str("\n"); // Define edges let mut processed_edges = HashSet::new(); let mut edge_count = 0; let max_edges = self.options.max_relationships.unwrap_or(usize::MAX); for &node_id in &limited_nodes { if edge_count >= max_edges { break; } let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None); for rel in relationships { if edge_count >= max_edges { break; } let edge_key = (rel.start_node, rel.end_node, rel.rel_type); if processed_edges.contains(&edge_key) { continue; } processed_edges.insert(edge_key); // Only include edges between nodes in our limited set if limited_nodes.contains(&rel.end_node) { let edge_label = self.format_edge_label(graph, &rel)?; let edge_attrs = self.get_edge_attributes(&rel)?; output.push_str(&format!(" n{} -> n{} [label=\"{}\", color=\"{}\", {}];\n", rel.start_node.0, rel.end_node.0, edge_label, edge_color, edge_attrs)); edge_count += 1; } } } // Add graph metadata as comment output.push_str("\n"); output.push_str(&format!(" // Graph metadata:\n")); output.push_str(&format!(" // Nodes: {}\n", limited_nodes.len())); output.push_str(&format!(" // Edges: {}\n", edge_count)); output.push_str(&format!(" // Layout: {:?}\n", self.options.layout)); output.push_str(&format!(" // Color scheme: {:?}\n", self.options.color_scheme)); output.push_str("}\n"); Ok(output) } fn format_node_label(&self, graph: &Arc, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult { let mut label = format!("N{}", node_id.0); if self.options.include_labels && !node.labels.is_empty() { let schema = graph.schema().read(); let labels: Vec = node.labels.iter() .filter_map(|&label_id| schema.get_label_name(label_id)) .map(|name| name.to_string()) .collect(); if !labels.is_empty() { label = format!("{}\\n:{}", label, labels.join(":")); } } if self.options.include_properties && !node.properties.is_empty() { let schema = graph.schema().read(); let mut props = Vec::new(); for (key_id, value) in node.properties.iter().take(3) { // Limit to first 3 properties if let Some(key_name) = schema.get_property_key_name(*key_id) { let value_str = self.format_property_value(value); props.push(format!("{}:{}", key_name, value_str)); } } if !props.is_empty() { label = format!("{}\\n{{{}}}", label, props.join("\\n")); } if node.properties.len() > 3 { label = format!("{}\\n...", label); } } Ok(label) } fn format_edge_label(&self, graph: &Arc, rel: &crate::core::Relationship) -> GigabrainResult { let schema = graph.schema().read(); let rel_type_name = schema.get_relationship_type_name(rel.rel_type) .map(|s| s.to_string()) .unwrap_or_else(|| "UNKNOWN".to_string()); let mut label = rel_type_name.to_string(); if self.options.include_properties && !rel.properties.is_empty() { let mut props = Vec::new(); for (key_id, value) in rel.properties.iter().take(2) { // Limit to first 2 properties if let Some(key_name) = schema.get_property_key_name(*key_id) { let value_str = self.format_property_value(value); props.push(format!("{}:{}", key_name, value_str)); } } if !props.is_empty() { label = format!("{}\\n{{{}}}", label, props.join("\\n")); } } Ok(label) } fn format_property_value(&self, value: &crate::core::PropertyValue) -> String { match value { crate::core::PropertyValue::String(s) => { if s.len() > 10 { format!("\"{}...\"", &s[..10]) } else { format!("\"{}\"", s) } } crate::core::PropertyValue::Integer(i) => i.to_string(), crate::core::PropertyValue::Float(f) => format!("{:.2}", f), crate::core::PropertyValue::Boolean(b) => b.to_string(), crate::core::PropertyValue::Null => "null".to_string(), crate::core::PropertyValue::List(list) => { let items: Vec = list.iter().take(3).map(|v| self.format_property_value(v)).collect(); if list.len() > 3 { format!("[{}, ...]", items.join(", ")) } else { format!("[{}]", items.join(", ")) } } crate::core::PropertyValue::Map(map) => { let items: Vec = map.iter().take(2).map(|(k, v)| { format!("{}:{}", k, self.format_property_value(v)) }).collect(); if map.len() > 2 { format!("{{{}, ...}}", items.join(", ")) } else { format!("{{{}}}", items.join(", ")) } } } } fn get_node_attributes(&self, _graph: &Arc, node: &crate::core::Node) -> GigabrainResult { let mut attrs = Vec::new(); // Node style based on number of labels if node.labels.len() > 1 { attrs.push("style=filled".to_string()); attrs.push("fillcolor=\"lightblue\"".to_string()); } else if node.labels.len() == 1 { attrs.push("style=filled".to_string()); attrs.push("fillcolor=\"lightgray\"".to_string()); } // Node shape based on properties if node.properties.is_empty() { attrs.push("shape=circle".to_string()); } else { attrs.push("shape=ellipse".to_string()); } Ok(attrs.join(", ")) } fn get_edge_attributes(&self, _rel: &crate::core::Relationship) -> GigabrainResult { let mut attrs = Vec::new(); // Edge style based on properties if !_rel.properties.is_empty() { attrs.push("style=bold".to_string()); } attrs.push("arrowhead=normal".to_string()); Ok(attrs.join(", ")) } fn get_color_scheme(&self) -> (&'static str, &'static str, &'static str) { match self.options.color_scheme { crate::visualization::ColorScheme::Dark => ("#ffffff", "#cccccc", "#2d2d2d"), crate::visualization::ColorScheme::Light => ("#000000", "#333333", "#ffffff"), crate::visualization::ColorScheme::Colorful => ("#1f77b4", "#ff7f0e", "#ffffff"), crate::visualization::ColorScheme::Monochrome => ("#000000", "#666666", "#ffffff"), crate::visualization::ColorScheme::Default => ("#000000", "#333333", "#ffffff"), } } fn get_node_size(&self) -> (f32, f32) { match self.options.node_size { crate::visualization::NodeSize::Small => (0.3, 0.3), crate::visualization::NodeSize::Medium => (0.5, 0.5), crate::visualization::NodeSize::Large => (0.8, 0.8), } } fn get_font_size(&self) -> u32 { match self.options.font_size { crate::visualization::FontSize::Small => 8, crate::visualization::FontSize::Medium => 10, crate::visualization::FontSize::Large => 14, } } }