use crate::{Graph, Result as GigabrainResult, GigabrainError}; use crate::{NodeId, RelationshipId}; use std::collections::HashMap; use std::sync::Arc; pub mod ascii; pub mod dot; pub mod svg; pub use ascii::*; pub use dot::*; pub use svg::*; /// Graph visualization formats #[derive(Debug, Clone, Copy, PartialEq)] pub enum VisualizationFormat { Ascii, Dot, Svg, Json, } impl std::fmt::Display for VisualizationFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VisualizationFormat::Ascii => write!(f, "ascii"), VisualizationFormat::Dot => write!(f, "dot"), VisualizationFormat::Svg => write!(f, "svg"), VisualizationFormat::Json => write!(f, "json"), } } } impl std::str::FromStr for VisualizationFormat { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "ascii" => Ok(VisualizationFormat::Ascii), "dot" => Ok(VisualizationFormat::Dot), "svg" => Ok(VisualizationFormat::Svg), "json" => Ok(VisualizationFormat::Json), _ => Err(format!("Unknown visualization format: {}", s)), } } } /// Visualization options #[derive(Debug, Clone)] pub struct VisualizationOptions { pub format: VisualizationFormat, pub max_nodes: Option, pub max_relationships: Option, pub include_properties: bool, pub include_labels: bool, pub layout: LayoutAlgorithm, pub color_scheme: ColorScheme, pub node_size: NodeSize, pub font_size: FontSize, } impl Default for VisualizationOptions { fn default() -> Self { Self { format: VisualizationFormat::Ascii, max_nodes: Some(50), max_relationships: Some(100), include_properties: true, include_labels: true, layout: LayoutAlgorithm::Spring, color_scheme: ColorScheme::Default, node_size: NodeSize::Medium, font_size: FontSize::Medium, } } } /// Layout algorithms for graph visualization #[derive(Debug, Clone, Copy, PartialEq)] pub enum LayoutAlgorithm { Spring, Hierarchical, Circular, Grid, Random, } /// Color schemes for visualization #[derive(Debug, Clone, Copy, PartialEq)] pub enum ColorScheme { Default, Dark, Light, Colorful, Monochrome, } /// Node size options #[derive(Debug, Clone, Copy, PartialEq)] pub enum NodeSize { Small, Medium, Large, } /// Font size options #[derive(Debug, Clone, Copy, PartialEq)] pub enum FontSize { Small, Medium, Large, } /// Main visualization engine pub struct GraphVisualizer { graph: Arc, options: VisualizationOptions, } impl GraphVisualizer { pub fn new(graph: Arc) -> Self { Self { graph, options: VisualizationOptions::default(), } } pub fn with_options(graph: Arc, options: VisualizationOptions) -> Self { Self { graph, options, } } /// Generate visualization in the specified format pub async fn visualize(&self) -> GigabrainResult { match self.options.format { VisualizationFormat::Ascii => self.generate_ascii().await, VisualizationFormat::Dot => self.generate_dot().await, VisualizationFormat::Svg => self.generate_svg().await, VisualizationFormat::Json => self.generate_json().await, } } /// Generate visualization and save to file pub async fn visualize_to_file(&self, filename: &str) -> GigabrainResult<()> { let content = self.visualize().await?; std::fs::write(filename, content) .map_err(|e| GigabrainError::Storage(format!("Failed to write visualization file: {}", e)))?; Ok(()) } /// Generate ASCII art visualization async fn generate_ascii(&self) -> GigabrainResult { let ascii_renderer = AsciiRenderer::new(&self.options); ascii_renderer.render(&self.graph).await } /// Generate DOT format for Graphviz async fn generate_dot(&self) -> GigabrainResult { let dot_renderer = DotRenderer::new(&self.options); dot_renderer.render(&self.graph).await } /// Generate SVG visualization async fn generate_svg(&self) -> GigabrainResult { let svg_renderer = SvgRenderer::new(&self.options); svg_renderer.render(&self.graph).await } /// Generate JSON representation for web visualization async fn generate_json(&self) -> GigabrainResult { let nodes = self.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 json_nodes = Vec::new(); let mut json_edges = Vec::new(); let mut edge_count = 0; let max_edges = self.options.max_relationships.unwrap_or(usize::MAX); // Collect nodes for &node_id in &limited_nodes { if let Some(node) = self.graph.get_node(node_id) { let mut node_obj = serde_json::Map::new(); node_obj.insert("id".to_string(), serde_json::Value::String(format!("n{}", node_id.0))); node_obj.insert("label".to_string(), serde_json::Value::String(format!("Node {}", node_id.0))); if self.options.include_labels { let labels: Vec = node.labels.iter().map(|id| { let schema = self.graph.schema().read(); schema.get_label_name(*id).map(|s| s.to_string()).unwrap_or_else(|| "Unknown".to_string()) }).collect(); node_obj.insert("labels".to_string(), serde_json::Value::Array( labels.iter().map(|l| serde_json::Value::String(l.clone())).collect() )); } if self.options.include_properties && !node.properties.is_empty() { let mut props = serde_json::Map::new(); for (key_id, value) in &node.properties { let schema = self.graph.schema().read(); if let Some(key_name) = schema.get_property_key_name(*key_id) { props.insert(key_name.to_string(), serde_json::Value::String(format!("{:?}", value))); } } node_obj.insert("properties".to_string(), serde_json::Value::Object(props)); } json_nodes.push(serde_json::Value::Object(node_obj)); } } // Collect relationships for &node_id in &limited_nodes { if edge_count >= max_edges { break; } let relationships = self.graph.get_node_relationships( node_id, crate::core::relationship::Direction::Outgoing, None ); for rel in relationships { if edge_count >= max_edges { break; } // Only include relationships between nodes in our limited set if limited_nodes.contains(&rel.end_node) { let mut edge_obj = serde_json::Map::new(); edge_obj.insert("source".to_string(), serde_json::Value::String(format!("n{}", rel.start_node.0))); edge_obj.insert("target".to_string(), serde_json::Value::String(format!("n{}", rel.end_node.0))); let schema = self.graph.schema().read(); if let Some(type_name) = schema.get_relationship_type_name(rel.rel_type) { edge_obj.insert("label".to_string(), serde_json::Value::String(type_name.to_string())); } if self.options.include_properties && !rel.properties.is_empty() { let mut props = serde_json::Map::new(); for (key_id, value) in &rel.properties { if let Some(key_name) = schema.get_property_key_name(*key_id) { props.insert(key_name.to_string(), serde_json::Value::String(format!("{:?}", value))); } } edge_obj.insert("properties".to_string(), serde_json::Value::Object(props)); } json_edges.push(serde_json::Value::Object(edge_obj)); edge_count += 1; } } } let result = serde_json::json!({ "nodes": json_nodes, "edges": json_edges, "metadata": { "total_nodes": limited_nodes.len(), "total_edges": edge_count, "layout": format!("{:?}", self.options.layout), "options": { "include_properties": self.options.include_properties, "include_labels": self.options.include_labels, "max_nodes": self.options.max_nodes, "max_relationships": self.options.max_relationships } } }); serde_json::to_string_pretty(&result) .map_err(|e| GigabrainError::Storage(format!("JSON serialization failed: {}", e))) } /// Update visualization options pub fn set_options(&mut self, options: VisualizationOptions) { self.options = options; } /// Get current options pub fn get_options(&self) -> &VisualizationOptions { &self.options } } /// Graph statistics for visualization purposes #[derive(Debug)] pub struct VisualizationStats { pub node_count: usize, pub edge_count: usize, pub avg_degree: f64, pub max_degree: usize, pub components: usize, pub diameter: Option, } impl GraphVisualizer { /// Get visualization statistics pub fn get_stats(&self) -> VisualizationStats { let nodes = self.graph.get_all_nodes(); let node_count = nodes.len(); let mut edge_count = 0; let mut total_degree = 0; let mut max_degree = 0; for &node_id in &nodes { let relationships = self.graph.get_node_relationships( node_id, crate::core::relationship::Direction::Both, None ); let degree = relationships.len(); total_degree += degree; edge_count += relationships.len(); max_degree = max_degree.max(degree); } // Relationships are counted twice (once for each direction) edge_count /= 2; let avg_degree = if node_count > 0 { total_degree as f64 / node_count as f64 } else { 0.0 }; VisualizationStats { node_count, edge_count, avg_degree, max_degree, components: 1, // Placeholder - would need proper component analysis diameter: None, // Placeholder - would need shortest path analysis } } }