use crate::{Graph, Result as GigabrainResult, GigabrainError}; use crate::{NodeId}; use crate::core::relationship::Direction; use crate::visualization::VisualizationOptions; use std::collections::{HashMap, HashSet}; use std::sync::Arc; /// SVG renderer for web-based graph visualization pub struct SvgRenderer<'a> { options: &'a VisualizationOptions, } impl<'a> SvgRenderer<'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 }; if limited_nodes.is_empty() { return Ok(self.create_empty_svg()); } // Generate layout let positions = self.generate_layout(&limited_nodes)?; // Calculate SVG dimensions let (width, height) = self.calculate_dimensions(&positions); let mut svg = String::new(); // SVG header svg.push_str(&format!( r#""#, width, height )); svg.push('\n'); // Add styles svg.push_str(&self.generate_styles()); // Add background let bg_color = self.get_background_color(); svg.push_str(&format!( r#" "#, bg_color )); svg.push('\n'); // Create group for the graph svg.push_str(" \n"); // Render relationships first (so they appear behind nodes) svg.push_str(&self.render_relationships(graph, &limited_nodes, &positions).await?); // Render nodes svg.push_str(&self.render_nodes(graph, &limited_nodes, &positions).await?); svg.push_str(" \n"); // Add title svg.push_str(&format!( r#" Graph Visualization ({} nodes)"#, limited_nodes.len() )); svg.push('\n'); // Add legend svg.push_str(&self.generate_legend(graph, &limited_nodes)); svg.push_str("\n"); Ok(svg) } fn generate_layout(&self, nodes: &[NodeId]) -> GigabrainResult> { let mut positions = HashMap::new(); let node_count = nodes.len(); match self.options.layout { crate::visualization::LayoutAlgorithm::Circular => { self.generate_circular_layout(nodes, &mut positions)?; } crate::visualization::LayoutAlgorithm::Grid => { self.generate_grid_layout(nodes, &mut positions)?; } crate::visualization::LayoutAlgorithm::Spring => { self.generate_spring_layout(nodes, &mut positions)?; } crate::visualization::LayoutAlgorithm::Hierarchical => { self.generate_hierarchical_layout(nodes, &mut positions)?; } crate::visualization::LayoutAlgorithm::Random => { self.generate_random_layout(nodes, &mut positions)?; } } Ok(positions) } fn generate_circular_layout(&self, nodes: &[NodeId], positions: &mut HashMap) -> GigabrainResult<()> { let node_count = nodes.len(); let center_x = 300.0; let center_y = 300.0; let radius = 200.0; for (i, &node_id) in nodes.iter().enumerate() { let angle = 2.0 * std::f64::consts::PI * i as f64 / node_count as f64; let x = center_x + radius * angle.cos(); let y = center_y + radius * angle.sin(); positions.insert(node_id, (x, y)); } Ok(()) } fn generate_grid_layout(&self, nodes: &[NodeId], positions: &mut HashMap) -> GigabrainResult<()> { let node_count = nodes.len(); let cols = (node_count as f64).sqrt().ceil() as usize; let spacing = 80.0; let start_x = 50.0; let start_y = 50.0; for (i, &node_id) in nodes.iter().enumerate() { let row = i / cols; let col = i % cols; let x = start_x + col as f64 * spacing; let y = start_y + row as f64 * spacing; positions.insert(node_id, (x, y)); } Ok(()) } fn generate_spring_layout(&self, nodes: &[NodeId], positions: &mut HashMap) -> GigabrainResult<()> { // Simple spring layout (basic implementation) // In a real implementation, this would use force-directed algorithms let node_count = nodes.len(); let area = 600.0 * 600.0; let _k = (area / node_count as f64).sqrt(); // Initialize random positions for (i, &node_id) in nodes.iter().enumerate() { let x = 50.0 + (i as f64 * 137.5) % 500.0; // Simple pseudo-random let y = 50.0 + (i as f64 * 73.3) % 500.0; positions.insert(node_id, (x, y)); } // Simple repulsion adjustment for _ in 0..10 { let mut forces: HashMap = HashMap::new(); // Calculate repulsive forces for &node1 in nodes { let mut fx = 0.0; let mut fy = 0.0; for &node2 in nodes { if node1 != node2 { let pos1 = positions[&node1]; let pos2 = positions[&node2]; let dx = pos1.0 - pos2.0; let dy = pos1.1 - pos2.1; let distance = (dx * dx + dy * dy).sqrt().max(1.0); let force = _k * _k / distance; fx += force * dx / distance; fy += force * dy / distance; } } forces.insert(node1, (fx, fy)); } // Apply forces for &node_id in nodes { if let Some(&(fx, fy)) = forces.get(&node_id) { let pos = positions.get_mut(&node_id).unwrap(); pos.0 += fx * 0.1; pos.1 += fy * 0.1; // Keep within bounds pos.0 = pos.0.max(30.0).min(570.0); pos.1 = pos.1.max(30.0).min(570.0); } } } Ok(()) } fn generate_hierarchical_layout(&self, nodes: &[NodeId], positions: &mut HashMap) -> GigabrainResult<()> { // Simple hierarchical layout - arrange nodes in levels let level_height = 100.0; let node_spacing = 80.0; let start_x = 50.0; let start_y = 50.0; // For simplicity, just arrange in rows let nodes_per_level = 5; for (i, &node_id) in nodes.iter().enumerate() { let level = i / nodes_per_level; let position_in_level = i % nodes_per_level; let x = start_x + position_in_level as f64 * node_spacing; let y = start_y + level as f64 * level_height; positions.insert(node_id, (x, y)); } Ok(()) } fn generate_random_layout(&self, nodes: &[NodeId], positions: &mut HashMap) -> GigabrainResult<()> { // Simple pseudo-random layout for (i, &node_id) in nodes.iter().enumerate() { let x = 50.0 + (i as f64 * 137.5) % 500.0; let y = 50.0 + (i as f64 * 73.3) % 400.0; positions.insert(node_id, (x, y)); } Ok(()) } async fn render_nodes(&self, graph: &Arc, nodes: &[NodeId], positions: &HashMap) -> GigabrainResult { let mut svg = String::new(); for &node_id in nodes { if let Some(node) = graph.get_node(node_id) { if let Some(&(x, y)) = positions.get(&node_id) { let (radius, color, stroke_color) = self.get_node_style(graph, &node)?; // Node circle svg.push_str(&format!( r#" "#, x, y, radius, color, stroke_color )); svg.push('\n'); // Node title (tooltip) let node_title = self.format_node_tooltip(graph, node_id, &node)?; svg.push_str(&format!(r#" {}"#, node_title)); svg.push('\n'); svg.push_str(" \n"); // Node label let label = self.format_node_label(graph, node_id, &node)?; let text_color = self.get_text_color(); svg.push_str(&format!( r#" {}"#, x, y + radius + 15.0, text_color, label )); svg.push('\n'); } } } Ok(svg) } async fn render_relationships(&self, graph: &Arc, nodes: &[NodeId], positions: &HashMap) -> GigabrainResult { let mut svg = String::new(); 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 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 nodes.contains(&rel.end_node) { if let (Some(&start_pos), Some(&end_pos)) = ( positions.get(&rel.start_node), positions.get(&rel.end_node) ) { let stroke_color = self.get_edge_color(); let stroke_width = self.get_edge_width(); // Draw edge line svg.push_str(&format!( r#" "#, start_pos.0, start_pos.1, end_pos.0, end_pos.1, stroke_color, stroke_width )); svg.push('\n'); // Edge title (tooltip) let edge_title = self.format_edge_tooltip(graph, &rel)?; svg.push_str(&format!(r#" {}"#, edge_title)); svg.push('\n'); svg.push_str(" \n"); // Edge label if self.should_show_edge_labels() { let mid_x = (start_pos.0 + end_pos.0) / 2.0; let mid_y = (start_pos.1 + end_pos.1) / 2.0; let label = self.format_edge_label(graph, &rel)?; let text_color = self.get_text_color(); svg.push_str(&format!( r#" {}"#, mid_x, mid_y - 5.0, text_color, label )); svg.push('\n'); } edge_count += 1; } } } } Ok(svg) } fn generate_styles(&self) -> String { let text_color = self.get_text_color(); let font_size = self.get_font_size(); format!(r#" "#, "#333", font_size, text_color, font_size - 2, text_color, text_color, text_color) } fn generate_legend(&self, graph: &Arc, nodes: &[NodeId]) -> String { let mut legend = String::new(); let legend_x = 10.0; let mut legend_y = 50.0; legend.push_str(&format!( r#" Legend:"#, legend_x, legend_y )); legend.push('\n'); legend_y += 20.0; legend.push_str(&format!( r#" "#, legend_x + 10.0, legend_y - 5.0, "#333" )); legend.push_str(&format!( r#" Node"#, legend_x + 25.0, legend_y )); legend.push('\n'); legend_y += 20.0; legend.push_str(&format!( r#" "#, legend_x, legend_y - 3.0, legend_x + 20.0, legend_y - 3.0, "#333" )); legend.push_str(&format!( r#" Relationship"#, legend_x + 25.0, legend_y )); legend.push('\n'); legend } fn calculate_dimensions(&self, positions: &HashMap) -> (u32, u32) { if positions.is_empty() { return (600, 400); } let mut min_x = f64::INFINITY; let mut max_x = f64::NEG_INFINITY; let mut min_y = f64::INFINITY; let mut max_y = f64::NEG_INFINITY; for &(x, y) in positions.values() { min_x = min_x.min(x); max_x = max_x.max(x); min_y = min_y.min(y); max_y = max_y.max(y); } let padding = 100.0; let width = (max_x - min_x + 2.0 * padding).max(600.0) as u32; let height = (max_y - min_y + 2.0 * padding).max(400.0) as u32; (width, height) } fn create_empty_svg(&self) -> String { format!(r#" Empty Graph - No Nodes to Visualize "#) } // Helper methods for styling fn get_node_style(&self, graph: &Arc, node: &crate::core::Node) -> GigabrainResult<(f64, &'static str, &'static str)> { let radius = match self.options.node_size { crate::visualization::NodeSize::Small => 12.0, crate::visualization::NodeSize::Medium => 16.0, crate::visualization::NodeSize::Large => 24.0, }; let (color, stroke) = match self.options.color_scheme { crate::visualization::ColorScheme::Dark => ("#4a90e2", "#ffffff"), crate::visualization::ColorScheme::Light => ("#87ceeb", "#333333"), crate::visualization::ColorScheme::Colorful => { if node.labels.len() > 1 { ("#ff6b6b", "#333333") } else if node.labels.len() == 1 { ("#4ecdc4", "#333333") } else { ("#95e1d3", "#333333") } } crate::visualization::ColorScheme::Monochrome => ("#d3d3d3", "#333333"), crate::visualization::ColorScheme::Default => ("#87ceeb", "#333333"), }; Ok((radius, color, stroke)) } fn get_edge_color(&self) -> &'static str { match self.options.color_scheme { crate::visualization::ColorScheme::Dark => "#cccccc", crate::visualization::ColorScheme::Light => "#666666", crate::visualization::ColorScheme::Colorful => "#ff7f0e", crate::visualization::ColorScheme::Monochrome => "#888888", crate::visualization::ColorScheme::Default => "#333333", } } fn get_edge_width(&self) -> u32 { 2 } fn get_background_color(&self) -> &'static str { match self.options.color_scheme { crate::visualization::ColorScheme::Dark => "#2d2d2d", crate::visualization::ColorScheme::Light => "#ffffff", crate::visualization::ColorScheme::Colorful => "#ffffff", crate::visualization::ColorScheme::Monochrome => "#ffffff", crate::visualization::ColorScheme::Default => "#ffffff", } } fn get_text_color(&self) -> &'static str { match self.options.color_scheme { crate::visualization::ColorScheme::Dark => "#ffffff", crate::visualization::ColorScheme::Light => "#000000", crate::visualization::ColorScheme::Colorful => "#333333", crate::visualization::ColorScheme::Monochrome => "#000000", crate::visualization::ColorScheme::Default => "#000000", } } fn get_font_size(&self) -> u32 { match self.options.font_size { crate::visualization::FontSize::Small => 10, crate::visualization::FontSize::Medium => 12, crate::visualization::FontSize::Large => 16, } } fn should_show_edge_labels(&self) -> bool { // Only show edge labels for smaller graphs to avoid clutter true } 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(); if let Some(first_label) = node.labels.iter() .filter_map(|&label_id| schema.get_label_name(label_id)) .next() { label = first_label.to_string(); } } Ok(label) } fn format_node_tooltip(&self, graph: &Arc, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult { let mut tooltip = format!("Node {}", 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() { tooltip.push_str(&format!(" ({})", labels.join(", "))); } } if self.options.include_properties && !node.properties.is_empty() { tooltip.push_str(" - Properties: "); let schema = graph.schema().read(); let props: Vec = node.properties.iter() .filter_map(|(key_id, value)| { schema.get_property_key_name(*key_id) .map(|name| format!("{}: {:?}", name, value)) }) .collect(); tooltip.push_str(&props.join(", ")); } Ok(tooltip) } 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()); Ok(rel_type_name.to_string()) } fn format_edge_tooltip(&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 tooltip = format!("Relationship: {} -> {} ({})", rel.start_node.0, rel.end_node.0, rel_type_name); if self.options.include_properties && !rel.properties.is_empty() { tooltip.push_str(" - Properties: "); let props: Vec = rel.properties.iter() .filter_map(|(key_id, value)| { schema.get_property_key_name(*key_id) .map(|name| format!("{}: {:?}", name, value)) }) .collect(); tooltip.push_str(&props.join(", ")); } Ok(tooltip) } }