this repo has no description
0
fork

Configure Feed

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

Implement comprehensive graph visualization capabilities

Added full-featured graph visualization system with multiple output formats:

- ASCII text visualization with hierarchical, circular, and grid layouts
- DOT format output for Graphviz rendering
- SVG generation with interactive elements and legends
- JSON format for web applications and D3.js integration

Features:
- Multiple layout algorithms (spring, hierarchical, circular, grid, random)
- Configurable color schemes (default, dark, light, colorful, monochrome)
- Adjustable node sizes and font sizes
- Property and label inclusion controls
- Node and relationship count limits for large graphs
- Comprehensive CLI integration with :visualize command

CLI Usage:
- :visualize - ASCII visualization with defaults
- :visualize --format svg -o graph.svg - Generate SVG file
- :visualize --layout circular - Circular layout
- :visualize --color dark --size large - Dark theme with large nodes

All formats support tooltips, legends, and metadata for enhanced usability.
Comprehensive test suite validates all visualization formats and options.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+2015 -1
+208 -1
src/cli/mod.rs
··· 2 2 use crate::cypher::{parse_cypher, QueryExecutor}; 3 3 use crate::cypher::executor::QueryResult; 4 4 use crate::algorithms::GraphAlgorithms; 5 + use crate::visualization::{GraphVisualizer, VisualizationOptions, VisualizationFormat, LayoutAlgorithm, ColorScheme, NodeSize, FontSize}; 5 6 use std::collections::HashMap; 6 7 use std::io::{self, Write}; 7 8 use std::sync::Arc; ··· 280 281 self.handle_import_command(&parts[1..]).await 281 282 } 282 283 } 284 + "visualize" | "viz" => { 285 + self.handle_visualize_command(&parts[1..]).await 286 + } 283 287 _ => CommandResult::Error(format!("Unknown command: :{}", parts[0])), 284 288 } 285 289 } ··· 344 348 help.push_str(" :history Show command history\n"); 345 349 help.push_str(" :clear Clear the screen\n"); 346 350 help.push_str(" :export <file> Export graph data\n"); 347 - help.push_str(" :import <file> Import graph data\n\n"); 351 + help.push_str(" :import <file> Import graph data\n"); 352 + help.push_str(" :visualize, :viz Generate graph visualization\n\n"); 348 353 help.push_str("Cypher Queries:\n"); 349 354 help.push_str(" MATCH (n) RETURN n Find all nodes\n"); 350 355 help.push_str(" CREATE (n:Person {name: 'Alice'}) Create a node\n"); ··· 645 650 format!("[{}]", items.join(", ")) 646 651 }, 647 652 } 653 + } 654 + 655 + /// Handle visualization command 656 + async fn handle_visualize_command(&self, args: &[&str]) -> CommandResult { 657 + // Parse arguments 658 + let mut format = VisualizationFormat::Ascii; 659 + let mut layout = LayoutAlgorithm::Spring; 660 + let mut color_scheme = ColorScheme::Default; 661 + let mut node_size = NodeSize::Medium; 662 + let mut font_size = FontSize::Medium; 663 + let mut max_nodes = Some(50); 664 + let mut max_relationships = Some(100); 665 + let mut output_file: Option<String> = None; 666 + let mut include_properties = true; 667 + let mut include_labels = true; 668 + 669 + // Parse command line arguments 670 + let mut i = 0; 671 + while i < args.len() { 672 + match args[i] { 673 + "--format" | "-f" => { 674 + if i + 1 < args.len() { 675 + match args[i + 1].parse::<VisualizationFormat>() { 676 + Ok(f) => format = f, 677 + Err(e) => return CommandResult::Error(e), 678 + } 679 + i += 1; 680 + } else { 681 + return CommandResult::Error("Missing format value".to_string()); 682 + } 683 + } 684 + "--layout" | "-l" => { 685 + if i + 1 < args.len() { 686 + layout = match args[i + 1].to_lowercase().as_str() { 687 + "spring" => LayoutAlgorithm::Spring, 688 + "hierarchical" | "hier" => LayoutAlgorithm::Hierarchical, 689 + "circular" | "circle" => LayoutAlgorithm::Circular, 690 + "grid" => LayoutAlgorithm::Grid, 691 + "random" => LayoutAlgorithm::Random, 692 + _ => return CommandResult::Error(format!("Unknown layout: {}", args[i + 1])), 693 + }; 694 + i += 1; 695 + } 696 + } 697 + "--color" | "-c" => { 698 + if i + 1 < args.len() { 699 + color_scheme = match args[i + 1].to_lowercase().as_str() { 700 + "default" => ColorScheme::Default, 701 + "dark" => ColorScheme::Dark, 702 + "light" => ColorScheme::Light, 703 + "colorful" => ColorScheme::Colorful, 704 + "mono" | "monochrome" => ColorScheme::Monochrome, 705 + _ => return CommandResult::Error(format!("Unknown color scheme: {}", args[i + 1])), 706 + }; 707 + i += 1; 708 + } 709 + } 710 + "--size" | "-s" => { 711 + if i + 1 < args.len() { 712 + node_size = match args[i + 1].to_lowercase().as_str() { 713 + "small" => NodeSize::Small, 714 + "medium" => NodeSize::Medium, 715 + "large" => NodeSize::Large, 716 + _ => return CommandResult::Error(format!("Unknown node size: {}", args[i + 1])), 717 + }; 718 + i += 1; 719 + } 720 + } 721 + "--font" => { 722 + if i + 1 < args.len() { 723 + font_size = match args[i + 1].to_lowercase().as_str() { 724 + "small" => FontSize::Small, 725 + "medium" => FontSize::Medium, 726 + "large" => FontSize::Large, 727 + _ => return CommandResult::Error(format!("Unknown font size: {}", args[i + 1])), 728 + }; 729 + i += 1; 730 + } 731 + } 732 + "--max-nodes" => { 733 + if i + 1 < args.len() { 734 + match args[i + 1].parse::<usize>() { 735 + Ok(n) => max_nodes = Some(n), 736 + Err(_) => return CommandResult::Error("Invalid max-nodes value".to_string()), 737 + } 738 + i += 1; 739 + } 740 + } 741 + "--max-rels" => { 742 + if i + 1 < args.len() { 743 + match args[i + 1].parse::<usize>() { 744 + Ok(n) => max_relationships = Some(n), 745 + Err(_) => return CommandResult::Error("Invalid max-rels value".to_string()), 746 + } 747 + i += 1; 748 + } 749 + } 750 + "--output" | "-o" => { 751 + if i + 1 < args.len() { 752 + output_file = Some(args[i + 1].to_string()); 753 + i += 1; 754 + } 755 + } 756 + "--no-properties" => { 757 + include_properties = false; 758 + } 759 + "--no-labels" => { 760 + include_labels = false; 761 + } 762 + "--help" | "-h" => { 763 + return CommandResult::Success(Some(self.get_visualize_help())); 764 + } 765 + _ => { 766 + if args[i].starts_with('-') { 767 + return CommandResult::Error(format!("Unknown option: {}", args[i])); 768 + } 769 + // Treat as output file if no --output was specified 770 + if output_file.is_none() { 771 + output_file = Some(args[i].to_string()); 772 + } 773 + } 774 + } 775 + i += 1; 776 + } 777 + 778 + // Create visualization options 779 + let options = VisualizationOptions { 780 + format, 781 + max_nodes, 782 + max_relationships, 783 + include_properties, 784 + include_labels, 785 + layout, 786 + color_scheme, 787 + node_size, 788 + font_size, 789 + }; 790 + 791 + // Create visualizer 792 + let visualizer = GraphVisualizer::with_options(self.graph.clone(), options); 793 + 794 + // Generate visualization 795 + match visualizer.visualize().await { 796 + Ok(output) => { 797 + if let Some(filename) = output_file { 798 + match visualizer.visualize_to_file(&filename).await { 799 + Ok(()) => { 800 + CommandResult::Success(Some(format!( 801 + "Visualization saved to: {} (format: {})", 802 + filename, format 803 + ))) 804 + } 805 + Err(e) => CommandResult::Error(format!("Failed to save visualization: {}", e)), 806 + } 807 + } else { 808 + // Display inline for ASCII format, provide info for others 809 + match format { 810 + VisualizationFormat::Ascii => { 811 + CommandResult::Success(Some(output)) 812 + } 813 + _ => { 814 + CommandResult::Success(Some(format!( 815 + "Visualization generated ({} format, {} chars). Use --output <file> to save.", 816 + format, output.len() 817 + ))) 818 + } 819 + } 820 + } 821 + } 822 + Err(e) => CommandResult::Error(format!("Visualization failed: {}", e)), 823 + } 824 + } 825 + 826 + /// Get visualization help text 827 + fn get_visualize_help(&self) -> String { 828 + let mut help = String::new(); 829 + help.push_str("Graph Visualization Command:\n\n"); 830 + help.push_str("Usage: :visualize [options] [output_file]\n\n"); 831 + help.push_str("Options:\n"); 832 + help.push_str(" --format, -f <format> Output format (ascii, dot, svg, json)\n"); 833 + help.push_str(" --layout, -l <layout> Layout algorithm (spring, hierarchical, circular, grid, random)\n"); 834 + help.push_str(" --color, -c <scheme> Color scheme (default, dark, light, colorful, monochrome)\n"); 835 + help.push_str(" --size, -s <size> Node size (small, medium, large)\n"); 836 + help.push_str(" --font <size> Font size (small, medium, large)\n"); 837 + help.push_str(" --max-nodes <n> Maximum nodes to display (default: 50)\n"); 838 + help.push_str(" --max-rels <n> Maximum relationships to display (default: 100)\n"); 839 + help.push_str(" --output, -o <file> Save to file instead of displaying\n"); 840 + help.push_str(" --no-properties Don't include node/relationship properties\n"); 841 + help.push_str(" --no-labels Don't include node labels\n"); 842 + help.push_str(" --help, -h Show this help\n\n"); 843 + help.push_str("Examples:\n"); 844 + help.push_str(" :visualize ASCII visualization with defaults\n"); 845 + help.push_str(" :visualize --format svg -o graph.svg Generate SVG file\n"); 846 + help.push_str(" :visualize --layout circular Circular ASCII layout\n"); 847 + help.push_str(" :visualize --color dark --size large Dark theme with large nodes\n"); 848 + help.push_str(" :visualize graph.dot --format dot Generate DOT file for Graphviz\n\n"); 849 + help.push_str("Formats:\n"); 850 + help.push_str(" ascii - Text-based visualization (displays inline)\n"); 851 + help.push_str(" dot - Graphviz DOT format (requires Graphviz to render)\n"); 852 + help.push_str(" svg - Scalable Vector Graphics (viewable in browsers)\n"); 853 + help.push_str(" json - JSON format for web applications\n"); 854 + help 648 855 } 649 856 650 857 /// Save command history
+1
src/lib.rs
··· 10 10 pub mod server; 11 11 pub mod observability; 12 12 pub mod cli; 13 + pub mod visualization; 13 14 14 15 pub use core::{Graph, PersistentGraph, Node, Relationship, Property}; 15 16 pub use error::{GigabrainError, Result};
+362
src/visualization/ascii.rs
··· 1 + use crate::{Graph, Result as GigabrainResult, GigabrainError}; 2 + use crate::{NodeId}; 3 + use crate::core::relationship::Direction; 4 + use crate::visualization::VisualizationOptions; 5 + use std::collections::{HashMap, HashSet}; 6 + use std::sync::Arc; 7 + 8 + /// ASCII art renderer for graph visualization 9 + pub struct AsciiRenderer<'a> { 10 + options: &'a VisualizationOptions, 11 + } 12 + 13 + impl<'a> AsciiRenderer<'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 + if limited_nodes.is_empty() { 27 + return Ok("Empty graph - no nodes to visualize".to_string()); 28 + } 29 + 30 + match self.options.layout { 31 + crate::visualization::LayoutAlgorithm::Hierarchical => { 32 + self.render_hierarchical(graph, &limited_nodes).await 33 + } 34 + crate::visualization::LayoutAlgorithm::Circular => { 35 + self.render_circular(graph, &limited_nodes).await 36 + } 37 + crate::visualization::LayoutAlgorithm::Grid => { 38 + self.render_grid(graph, &limited_nodes).await 39 + } 40 + _ => { 41 + // Default to simple layout for ASCII 42 + self.render_simple(graph, &limited_nodes).await 43 + } 44 + } 45 + } 46 + 47 + async fn render_simple(&self, graph: &Arc<Graph>, nodes: &[NodeId]) -> GigabrainResult<String> { 48 + let mut output = String::new(); 49 + output.push_str("┌─────────────────────────────────────────────────────────────────────────────┐\n"); 50 + output.push_str("│ Graph Visualization (ASCII) │\n"); 51 + output.push_str("└─────────────────────────────────────────────────────────────────────────────┘\n\n"); 52 + 53 + // Track processed relationships to avoid duplicates 54 + let mut processed_relationships = HashSet::new(); 55 + 56 + for (i, &node_id) in nodes.iter().enumerate() { 57 + if let Some(node) = graph.get_node(node_id) { 58 + // Node representation 59 + let node_repr = self.format_node(graph, node_id, &node)?; 60 + output.push_str(&format!("({}) {}\n", i + 1, node_repr)); 61 + 62 + // Show relationships 63 + let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None); 64 + for rel in relationships { 65 + let rel_key = (rel.start_node, rel.end_node, rel.rel_type); 66 + if processed_relationships.contains(&rel_key) { 67 + continue; 68 + } 69 + processed_relationships.insert(rel_key); 70 + 71 + // Find target node index 72 + if let Some(target_idx) = nodes.iter().position(|&n| n == rel.end_node) { 73 + let schema = graph.schema().read(); 74 + let rel_type_name = schema.get_relationship_type_name(rel.rel_type) 75 + .map(|s| s.to_string()) 76 + .unwrap_or_else(|| "UNKNOWN".to_string()); 77 + 78 + output.push_str(&format!(" │\n")); 79 + output.push_str(&format!(" └─[{}]─> ({})\n", rel_type_name, target_idx + 1)); 80 + } 81 + } 82 + 83 + if i < nodes.len() - 1 { 84 + output.push_str(" |\n"); 85 + } 86 + } 87 + } 88 + 89 + // Add legend 90 + output.push_str("\n"); 91 + output.push_str("Legend:\n"); 92 + output.push_str(" (n) Node n\n"); 93 + output.push_str(" [TYPE] Relationship type\n"); 94 + output.push_str(" └─[TYPE]─> Outgoing relationship\n"); 95 + 96 + Ok(output) 97 + } 98 + 99 + async fn render_hierarchical(&self, graph: &Arc<Graph>, nodes: &[NodeId]) -> GigabrainResult<String> { 100 + let mut output = String::new(); 101 + output.push_str("┌─────────────────────────────────────────────────────────────────────────────┐\n"); 102 + output.push_str("│ Graph Visualization (Hierarchical) │\n"); 103 + output.push_str("└─────────────────────────────────────────────────────────────────────────────┘\n\n"); 104 + 105 + // Find root nodes (nodes with no incoming relationships) 106 + let mut root_nodes = Vec::new(); 107 + let mut has_incoming = HashSet::new(); 108 + 109 + for &node_id in nodes { 110 + let incoming = graph.get_node_relationships(node_id, Direction::Incoming, None); 111 + if !incoming.is_empty() { 112 + has_incoming.insert(node_id); 113 + } 114 + } 115 + 116 + for &node_id in nodes { 117 + if !has_incoming.contains(&node_id) { 118 + root_nodes.push(node_id); 119 + } 120 + } 121 + 122 + if root_nodes.is_empty() { 123 + root_nodes.push(nodes[0]); // Fallback to first node 124 + } 125 + 126 + let mut visited = HashSet::new(); 127 + for root in root_nodes { 128 + self.render_hierarchy_recursive(graph, root, 0, &mut visited, &mut output)?; 129 + } 130 + 131 + Ok(output) 132 + } 133 + 134 + fn render_hierarchy_recursive( 135 + &self, 136 + graph: &Arc<Graph>, 137 + node_id: NodeId, 138 + depth: usize, 139 + visited: &mut HashSet<NodeId>, 140 + output: &mut String, 141 + ) -> GigabrainResult<()> { 142 + if visited.contains(&node_id) { 143 + return Ok(()); 144 + } 145 + visited.insert(node_id); 146 + 147 + let indent = " ".repeat(depth); 148 + let node = graph.get_node(node_id).ok_or_else(|| { 149 + GigabrainError::Storage("Node not found".to_string()) 150 + })?; 151 + 152 + let node_repr = self.format_node(graph, node_id, &node)?; 153 + output.push_str(&format!("{}├─ {}\n", indent, node_repr)); 154 + 155 + let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None); 156 + for (i, rel) in relationships.iter().enumerate() { 157 + let schema = graph.schema().read(); 158 + let rel_type_name = schema.get_relationship_type_name(rel.rel_type) 159 + .map(|s| s.to_string()) 160 + .unwrap_or_else(|| "UNKNOWN".to_string()); 161 + 162 + let is_last = i == relationships.len() - 1; 163 + let branch_char = if is_last { "└" } else { "├" }; 164 + 165 + output.push_str(&format!("{}│ {}─[{}]─┐\n", indent, branch_char, rel_type_name)); 166 + self.render_hierarchy_recursive(graph, rel.end_node, depth + 1, visited, output)?; 167 + } 168 + 169 + Ok(()) 170 + } 171 + 172 + async fn render_circular(&self, graph: &Arc<Graph>, nodes: &[NodeId]) -> GigabrainResult<String> { 173 + let mut output = String::new(); 174 + output.push_str("┌─────────────────────────────────────────────────────────────────────────────┐\n"); 175 + output.push_str("│ Graph Visualization (Circular) │\n"); 176 + output.push_str("└─────────────────────────────────────────────────────────────────────────────┘\n\n"); 177 + 178 + let node_count = nodes.len(); 179 + if node_count == 0 { 180 + return Ok(output + "No nodes to display"); 181 + } 182 + 183 + // Create a simple circular layout with ASCII 184 + let radius = std::cmp::max(3, node_count / 2); 185 + let mut positions = HashMap::new(); 186 + 187 + for (i, &node_id) in nodes.iter().enumerate() { 188 + let angle = 2.0 * std::f64::consts::PI * i as f64 / node_count as f64; 189 + let x = (radius as f64 * angle.cos()) as i32 + radius as i32; 190 + let y = (radius as f64 * angle.sin()) as i32 + radius as i32; 191 + positions.insert(node_id, (x, y)); 192 + } 193 + 194 + // Create grid for drawing 195 + let grid_size = (2 * radius + 5) as usize; 196 + let mut grid = vec![vec![' '; grid_size]; grid_size]; 197 + 198 + // Place nodes 199 + for (i, &node_id) in nodes.iter().enumerate() { 200 + if let Some(&(x, y)) = positions.get(&node_id) { 201 + let ux = x as usize; 202 + let uy = y as usize; 203 + if ux < grid_size && uy < grid_size { 204 + let node_char = char::from_digit((i + 1) as u32, 10).unwrap_or('*'); 205 + grid[uy][ux] = node_char; 206 + } 207 + } 208 + } 209 + 210 + // Draw the grid 211 + for row in &grid { 212 + output.push_str(&row.iter().collect::<String>()); 213 + output.push('\n'); 214 + } 215 + 216 + // Add node legend 217 + output.push_str("\nNodes:\n"); 218 + for (i, &node_id) in nodes.iter().enumerate() { 219 + if let Some(node) = graph.get_node(node_id) { 220 + let node_repr = self.format_node(graph, node_id, &node)?; 221 + output.push_str(&format!(" {}: {}\n", i + 1, node_repr)); 222 + } 223 + } 224 + 225 + Ok(output) 226 + } 227 + 228 + async fn render_grid(&self, graph: &Arc<Graph>, nodes: &[NodeId]) -> GigabrainResult<String> { 229 + let mut output = String::new(); 230 + output.push_str("┌─────────────────────────────────────────────────────────────────────────────┐\n"); 231 + output.push_str("│ Graph Visualization (Grid) │\n"); 232 + output.push_str("└─────────────────────────────────────────────────────────────────────────────┘\n\n"); 233 + 234 + let node_count = nodes.len(); 235 + if node_count == 0 { 236 + return Ok(output + "No nodes to display"); 237 + } 238 + 239 + // Calculate grid dimensions 240 + let cols = (node_count as f64).sqrt().ceil() as usize; 241 + let rows = (node_count + cols - 1) / cols; 242 + 243 + // Create grid layout 244 + let mut grid = vec![vec![String::new(); cols]; rows]; 245 + 246 + for (i, &node_id) in nodes.iter().enumerate() { 247 + let row = i / cols; 248 + let col = i % cols; 249 + 250 + if let Some(node) = graph.get_node(node_id) { 251 + let node_repr = self.format_node_short(graph, node_id, &node)?; 252 + grid[row][col] = format!("({}) {}", i + 1, node_repr); 253 + } 254 + } 255 + 256 + // Calculate column widths 257 + let mut col_widths = vec![0; cols]; 258 + for row in &grid { 259 + for (col, cell) in row.iter().enumerate() { 260 + col_widths[col] = col_widths[col].max(cell.len()); 261 + } 262 + } 263 + 264 + // Draw grid 265 + for row in &grid { 266 + output.push_str("│"); 267 + for (col, cell) in row.iter().enumerate() { 268 + let width = col_widths[col]; 269 + if width > 0 { 270 + output.push_str(&format!(" {:width$} │", cell, width = width)); 271 + } 272 + } 273 + output.push('\n'); 274 + } 275 + 276 + // Show relationships 277 + output.push_str("\nRelationships:\n"); 278 + let mut rel_count = 0; 279 + let max_rels = self.options.max_relationships.unwrap_or(20); 280 + 281 + for &node_id in nodes { 282 + if rel_count >= max_rels { 283 + break; 284 + } 285 + 286 + let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None); 287 + for rel in relationships { 288 + if rel_count >= max_rels { 289 + break; 290 + } 291 + 292 + if let (Some(start_idx), Some(end_idx)) = ( 293 + nodes.iter().position(|&n| n == rel.start_node), 294 + nodes.iter().position(|&n| n == rel.end_node) 295 + ) { 296 + let schema = graph.schema().read(); 297 + let rel_type_name = schema.get_relationship_type_name(rel.rel_type) 298 + .map(|s| s.to_string()) 299 + .unwrap_or_else(|| "UNKNOWN".to_string()); 300 + 301 + output.push_str(&format!(" ({}) --[{}]--> ({})\n", 302 + start_idx + 1, rel_type_name, end_idx + 1)); 303 + rel_count += 1; 304 + } 305 + } 306 + } 307 + 308 + if rel_count >= max_rels { 309 + output.push_str(&format!(" ... and more (showing first {})\n", max_rels)); 310 + } 311 + 312 + Ok(output) 313 + } 314 + 315 + fn format_node(&self, graph: &Arc<Graph>, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult<String> { 316 + let mut result = format!("Node({})", node_id.0); 317 + 318 + if self.options.include_labels && !node.labels.is_empty() { 319 + let schema = graph.schema().read(); 320 + let labels: Vec<String> = node.labels.iter() 321 + .filter_map(|&label_id| schema.get_label_name(label_id)) 322 + .map(|name| name.to_string()) 323 + .collect(); 324 + 325 + if !labels.is_empty() { 326 + result.push_str(&format!(":{}", labels.join(":"))); 327 + } 328 + } 329 + 330 + if self.options.include_properties && !node.properties.is_empty() { 331 + let schema = graph.schema().read(); 332 + let mut props = Vec::new(); 333 + 334 + for (key_id, value) in &node.properties { 335 + if let Some(key_name) = schema.get_property_key_name(*key_id) { 336 + props.push(format!("{}:{:?}", key_name, value)); 337 + } 338 + } 339 + 340 + if !props.is_empty() { 341 + result.push_str(&format!(" {{{}}}", props.join(", "))); 342 + } 343 + } 344 + 345 + Ok(result) 346 + } 347 + 348 + fn format_node_short(&self, graph: &Arc<Graph>, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult<String> { 349 + let mut result = format!("N{}", node_id.0); 350 + 351 + if self.options.include_labels && !node.labels.is_empty() { 352 + let schema = graph.schema().read(); 353 + if let Some(first_label) = node.labels.iter() 354 + .filter_map(|&label_id| schema.get_label_name(label_id)) 355 + .next() { 356 + result.push_str(&format!(":{}", first_label)); 357 + } 358 + } 359 + 360 + Ok(result) 361 + } 362 + }
+285
src/visualization/dot.rs
··· 1 + use crate::{Graph, Result as GigabrainResult, GigabrainError}; 2 + use crate::{NodeId}; 3 + use crate::core::relationship::Direction; 4 + use crate::visualization::VisualizationOptions; 5 + use std::collections::HashSet; 6 + use std::sync::Arc; 7 + 8 + /// DOT format renderer for Graphviz visualization 9 + pub struct DotRenderer<'a> { 10 + options: &'a VisualizationOptions, 11 + } 12 + 13 + impl<'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 + }
+342
src/visualization/mod.rs
··· 1 + use crate::{Graph, Result as GigabrainResult, GigabrainError}; 2 + use crate::{NodeId, RelationshipId}; 3 + use std::collections::HashMap; 4 + use std::sync::Arc; 5 + 6 + pub mod ascii; 7 + pub mod dot; 8 + pub mod svg; 9 + 10 + pub use ascii::*; 11 + pub use dot::*; 12 + pub use svg::*; 13 + 14 + /// Graph visualization formats 15 + #[derive(Debug, Clone, Copy, PartialEq)] 16 + pub enum VisualizationFormat { 17 + Ascii, 18 + Dot, 19 + Svg, 20 + Json, 21 + } 22 + 23 + impl std::fmt::Display for VisualizationFormat { 24 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 + match self { 26 + VisualizationFormat::Ascii => write!(f, "ascii"), 27 + VisualizationFormat::Dot => write!(f, "dot"), 28 + VisualizationFormat::Svg => write!(f, "svg"), 29 + VisualizationFormat::Json => write!(f, "json"), 30 + } 31 + } 32 + } 33 + 34 + impl std::str::FromStr for VisualizationFormat { 35 + type Err = String; 36 + 37 + fn from_str(s: &str) -> Result<Self, Self::Err> { 38 + match s.to_lowercase().as_str() { 39 + "ascii" => Ok(VisualizationFormat::Ascii), 40 + "dot" => Ok(VisualizationFormat::Dot), 41 + "svg" => Ok(VisualizationFormat::Svg), 42 + "json" => Ok(VisualizationFormat::Json), 43 + _ => Err(format!("Unknown visualization format: {}", s)), 44 + } 45 + } 46 + } 47 + 48 + /// Visualization options 49 + #[derive(Debug, Clone)] 50 + pub struct VisualizationOptions { 51 + pub format: VisualizationFormat, 52 + pub max_nodes: Option<usize>, 53 + pub max_relationships: Option<usize>, 54 + pub include_properties: bool, 55 + pub include_labels: bool, 56 + pub layout: LayoutAlgorithm, 57 + pub color_scheme: ColorScheme, 58 + pub node_size: NodeSize, 59 + pub font_size: FontSize, 60 + } 61 + 62 + impl Default for VisualizationOptions { 63 + fn default() -> Self { 64 + Self { 65 + format: VisualizationFormat::Ascii, 66 + max_nodes: Some(50), 67 + max_relationships: Some(100), 68 + include_properties: true, 69 + include_labels: true, 70 + layout: LayoutAlgorithm::Spring, 71 + color_scheme: ColorScheme::Default, 72 + node_size: NodeSize::Medium, 73 + font_size: FontSize::Medium, 74 + } 75 + } 76 + } 77 + 78 + /// Layout algorithms for graph visualization 79 + #[derive(Debug, Clone, Copy, PartialEq)] 80 + pub enum LayoutAlgorithm { 81 + Spring, 82 + Hierarchical, 83 + Circular, 84 + Grid, 85 + Random, 86 + } 87 + 88 + /// Color schemes for visualization 89 + #[derive(Debug, Clone, Copy, PartialEq)] 90 + pub enum ColorScheme { 91 + Default, 92 + Dark, 93 + Light, 94 + Colorful, 95 + Monochrome, 96 + } 97 + 98 + /// Node size options 99 + #[derive(Debug, Clone, Copy, PartialEq)] 100 + pub enum NodeSize { 101 + Small, 102 + Medium, 103 + Large, 104 + } 105 + 106 + /// Font size options 107 + #[derive(Debug, Clone, Copy, PartialEq)] 108 + pub enum FontSize { 109 + Small, 110 + Medium, 111 + Large, 112 + } 113 + 114 + /// Main visualization engine 115 + pub struct GraphVisualizer { 116 + graph: Arc<Graph>, 117 + options: VisualizationOptions, 118 + } 119 + 120 + impl GraphVisualizer { 121 + pub fn new(graph: Arc<Graph>) -> Self { 122 + Self { 123 + graph, 124 + options: VisualizationOptions::default(), 125 + } 126 + } 127 + 128 + pub fn with_options(graph: Arc<Graph>, options: VisualizationOptions) -> Self { 129 + Self { 130 + graph, 131 + options, 132 + } 133 + } 134 + 135 + /// Generate visualization in the specified format 136 + pub async fn visualize(&self) -> GigabrainResult<String> { 137 + match self.options.format { 138 + VisualizationFormat::Ascii => self.generate_ascii().await, 139 + VisualizationFormat::Dot => self.generate_dot().await, 140 + VisualizationFormat::Svg => self.generate_svg().await, 141 + VisualizationFormat::Json => self.generate_json().await, 142 + } 143 + } 144 + 145 + /// Generate visualization and save to file 146 + pub async fn visualize_to_file(&self, filename: &str) -> GigabrainResult<()> { 147 + let content = self.visualize().await?; 148 + std::fs::write(filename, content) 149 + .map_err(|e| GigabrainError::Storage(format!("Failed to write visualization file: {}", e)))?; 150 + Ok(()) 151 + } 152 + 153 + /// Generate ASCII art visualization 154 + async fn generate_ascii(&self) -> GigabrainResult<String> { 155 + let ascii_renderer = AsciiRenderer::new(&self.options); 156 + ascii_renderer.render(&self.graph).await 157 + } 158 + 159 + /// Generate DOT format for Graphviz 160 + async fn generate_dot(&self) -> GigabrainResult<String> { 161 + let dot_renderer = DotRenderer::new(&self.options); 162 + dot_renderer.render(&self.graph).await 163 + } 164 + 165 + /// Generate SVG visualization 166 + async fn generate_svg(&self) -> GigabrainResult<String> { 167 + let svg_renderer = SvgRenderer::new(&self.options); 168 + svg_renderer.render(&self.graph).await 169 + } 170 + 171 + /// Generate JSON representation for web visualization 172 + async fn generate_json(&self) -> GigabrainResult<String> { 173 + let nodes = self.graph.get_all_nodes(); 174 + let limited_nodes: Vec<NodeId> = if let Some(max) = self.options.max_nodes { 175 + nodes.into_iter().take(max).collect() 176 + } else { 177 + nodes 178 + }; 179 + 180 + let mut json_nodes = Vec::new(); 181 + let mut json_edges = Vec::new(); 182 + let mut edge_count = 0; 183 + let max_edges = self.options.max_relationships.unwrap_or(usize::MAX); 184 + 185 + // Collect nodes 186 + for &node_id in &limited_nodes { 187 + if let Some(node) = self.graph.get_node(node_id) { 188 + let mut node_obj = serde_json::Map::new(); 189 + node_obj.insert("id".to_string(), serde_json::Value::String(format!("n{}", node_id.0))); 190 + node_obj.insert("label".to_string(), serde_json::Value::String(format!("Node {}", node_id.0))); 191 + 192 + if self.options.include_labels { 193 + let labels: Vec<String> = node.labels.iter().map(|id| { 194 + let schema = self.graph.schema().read(); 195 + schema.get_label_name(*id).map(|s| s.to_string()).unwrap_or_else(|| "Unknown".to_string()) 196 + }).collect(); 197 + node_obj.insert("labels".to_string(), serde_json::Value::Array( 198 + labels.iter().map(|l| serde_json::Value::String(l.clone())).collect() 199 + )); 200 + } 201 + 202 + if self.options.include_properties && !node.properties.is_empty() { 203 + let mut props = serde_json::Map::new(); 204 + for (key_id, value) in &node.properties { 205 + let schema = self.graph.schema().read(); 206 + if let Some(key_name) = schema.get_property_key_name(*key_id) { 207 + props.insert(key_name.to_string(), serde_json::Value::String(format!("{:?}", value))); 208 + } 209 + } 210 + node_obj.insert("properties".to_string(), serde_json::Value::Object(props)); 211 + } 212 + 213 + json_nodes.push(serde_json::Value::Object(node_obj)); 214 + } 215 + } 216 + 217 + // Collect relationships 218 + for &node_id in &limited_nodes { 219 + if edge_count >= max_edges { 220 + break; 221 + } 222 + 223 + let relationships = self.graph.get_node_relationships( 224 + node_id, 225 + crate::core::relationship::Direction::Outgoing, 226 + None 227 + ); 228 + 229 + for rel in relationships { 230 + if edge_count >= max_edges { 231 + break; 232 + } 233 + 234 + // Only include relationships between nodes in our limited set 235 + if limited_nodes.contains(&rel.end_node) { 236 + let mut edge_obj = serde_json::Map::new(); 237 + edge_obj.insert("source".to_string(), serde_json::Value::String(format!("n{}", rel.start_node.0))); 238 + edge_obj.insert("target".to_string(), serde_json::Value::String(format!("n{}", rel.end_node.0))); 239 + 240 + let schema = self.graph.schema().read(); 241 + if let Some(type_name) = schema.get_relationship_type_name(rel.rel_type) { 242 + edge_obj.insert("label".to_string(), serde_json::Value::String(type_name.to_string())); 243 + } 244 + 245 + if self.options.include_properties && !rel.properties.is_empty() { 246 + let mut props = serde_json::Map::new(); 247 + for (key_id, value) in &rel.properties { 248 + if let Some(key_name) = schema.get_property_key_name(*key_id) { 249 + props.insert(key_name.to_string(), serde_json::Value::String(format!("{:?}", value))); 250 + } 251 + } 252 + edge_obj.insert("properties".to_string(), serde_json::Value::Object(props)); 253 + } 254 + 255 + json_edges.push(serde_json::Value::Object(edge_obj)); 256 + edge_count += 1; 257 + } 258 + } 259 + } 260 + 261 + let result = serde_json::json!({ 262 + "nodes": json_nodes, 263 + "edges": json_edges, 264 + "metadata": { 265 + "total_nodes": limited_nodes.len(), 266 + "total_edges": edge_count, 267 + "layout": format!("{:?}", self.options.layout), 268 + "options": { 269 + "include_properties": self.options.include_properties, 270 + "include_labels": self.options.include_labels, 271 + "max_nodes": self.options.max_nodes, 272 + "max_relationships": self.options.max_relationships 273 + } 274 + } 275 + }); 276 + 277 + serde_json::to_string_pretty(&result) 278 + .map_err(|e| GigabrainError::Storage(format!("JSON serialization failed: {}", e))) 279 + } 280 + 281 + /// Update visualization options 282 + pub fn set_options(&mut self, options: VisualizationOptions) { 283 + self.options = options; 284 + } 285 + 286 + /// Get current options 287 + pub fn get_options(&self) -> &VisualizationOptions { 288 + &self.options 289 + } 290 + } 291 + 292 + /// Graph statistics for visualization purposes 293 + #[derive(Debug)] 294 + pub struct VisualizationStats { 295 + pub node_count: usize, 296 + pub edge_count: usize, 297 + pub avg_degree: f64, 298 + pub max_degree: usize, 299 + pub components: usize, 300 + pub diameter: Option<usize>, 301 + } 302 + 303 + impl GraphVisualizer { 304 + /// Get visualization statistics 305 + pub fn get_stats(&self) -> VisualizationStats { 306 + let nodes = self.graph.get_all_nodes(); 307 + let node_count = nodes.len(); 308 + let mut edge_count = 0; 309 + let mut total_degree = 0; 310 + let mut max_degree = 0; 311 + 312 + for &node_id in &nodes { 313 + let relationships = self.graph.get_node_relationships( 314 + node_id, 315 + crate::core::relationship::Direction::Both, 316 + None 317 + ); 318 + let degree = relationships.len(); 319 + total_degree += degree; 320 + edge_count += relationships.len(); 321 + max_degree = max_degree.max(degree); 322 + } 323 + 324 + // Relationships are counted twice (once for each direction) 325 + edge_count /= 2; 326 + 327 + let avg_degree = if node_count > 0 { 328 + total_degree as f64 / node_count as f64 329 + } else { 330 + 0.0 331 + }; 332 + 333 + VisualizationStats { 334 + node_count, 335 + edge_count, 336 + avg_degree, 337 + max_degree, 338 + components: 1, // Placeholder - would need proper component analysis 339 + diameter: None, // Placeholder - would need shortest path analysis 340 + } 341 + } 342 + }
+598
src/visualization/svg.rs
··· 1 + use crate::{Graph, Result as GigabrainResult, GigabrainError}; 2 + use crate::{NodeId}; 3 + use crate::core::relationship::Direction; 4 + use crate::visualization::VisualizationOptions; 5 + use std::collections::{HashMap, HashSet}; 6 + use std::sync::Arc; 7 + 8 + /// SVG renderer for web-based graph visualization 9 + pub struct SvgRenderer<'a> { 10 + options: &'a VisualizationOptions, 11 + } 12 + 13 + impl<'a> SvgRenderer<'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 + if limited_nodes.is_empty() { 27 + return Ok(self.create_empty_svg()); 28 + } 29 + 30 + // Generate layout 31 + let positions = self.generate_layout(&limited_nodes)?; 32 + 33 + // Calculate SVG dimensions 34 + let (width, height) = self.calculate_dimensions(&positions); 35 + 36 + let mut svg = String::new(); 37 + 38 + // SVG header 39 + svg.push_str(&format!( 40 + r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">"#, 41 + width, height 42 + )); 43 + svg.push('\n'); 44 + 45 + // Add styles 46 + svg.push_str(&self.generate_styles()); 47 + 48 + // Add background 49 + let bg_color = self.get_background_color(); 50 + svg.push_str(&format!( 51 + r#" <rect width="100%" height="100%" fill="{}"/>"#, 52 + bg_color 53 + )); 54 + svg.push('\n'); 55 + 56 + // Create group for the graph 57 + svg.push_str(" <g id=\"graph\">\n"); 58 + 59 + // Render relationships first (so they appear behind nodes) 60 + svg.push_str(&self.render_relationships(graph, &limited_nodes, &positions).await?); 61 + 62 + // Render nodes 63 + svg.push_str(&self.render_nodes(graph, &limited_nodes, &positions).await?); 64 + 65 + svg.push_str(" </g>\n"); 66 + 67 + // Add title 68 + svg.push_str(&format!( 69 + r#" <text x="10" y="20" class="title">Graph Visualization ({} nodes)</text>"#, 70 + limited_nodes.len() 71 + )); 72 + svg.push('\n'); 73 + 74 + // Add legend 75 + svg.push_str(&self.generate_legend(graph, &limited_nodes)); 76 + 77 + svg.push_str("</svg>\n"); 78 + 79 + Ok(svg) 80 + } 81 + 82 + fn generate_layout(&self, nodes: &[NodeId]) -> GigabrainResult<HashMap<NodeId, (f64, f64)>> { 83 + let mut positions = HashMap::new(); 84 + let node_count = nodes.len(); 85 + 86 + match self.options.layout { 87 + crate::visualization::LayoutAlgorithm::Circular => { 88 + self.generate_circular_layout(nodes, &mut positions)?; 89 + } 90 + crate::visualization::LayoutAlgorithm::Grid => { 91 + self.generate_grid_layout(nodes, &mut positions)?; 92 + } 93 + crate::visualization::LayoutAlgorithm::Spring => { 94 + self.generate_spring_layout(nodes, &mut positions)?; 95 + } 96 + crate::visualization::LayoutAlgorithm::Hierarchical => { 97 + self.generate_hierarchical_layout(nodes, &mut positions)?; 98 + } 99 + crate::visualization::LayoutAlgorithm::Random => { 100 + self.generate_random_layout(nodes, &mut positions)?; 101 + } 102 + } 103 + 104 + Ok(positions) 105 + } 106 + 107 + fn generate_circular_layout(&self, nodes: &[NodeId], positions: &mut HashMap<NodeId, (f64, f64)>) -> GigabrainResult<()> { 108 + let node_count = nodes.len(); 109 + let center_x = 300.0; 110 + let center_y = 300.0; 111 + let radius = 200.0; 112 + 113 + for (i, &node_id) in nodes.iter().enumerate() { 114 + let angle = 2.0 * std::f64::consts::PI * i as f64 / node_count as f64; 115 + let x = center_x + radius * angle.cos(); 116 + let y = center_y + radius * angle.sin(); 117 + positions.insert(node_id, (x, y)); 118 + } 119 + 120 + Ok(()) 121 + } 122 + 123 + fn generate_grid_layout(&self, nodes: &[NodeId], positions: &mut HashMap<NodeId, (f64, f64)>) -> GigabrainResult<()> { 124 + let node_count = nodes.len(); 125 + let cols = (node_count as f64).sqrt().ceil() as usize; 126 + let spacing = 80.0; 127 + let start_x = 50.0; 128 + let start_y = 50.0; 129 + 130 + for (i, &node_id) in nodes.iter().enumerate() { 131 + let row = i / cols; 132 + let col = i % cols; 133 + let x = start_x + col as f64 * spacing; 134 + let y = start_y + row as f64 * spacing; 135 + positions.insert(node_id, (x, y)); 136 + } 137 + 138 + Ok(()) 139 + } 140 + 141 + fn generate_spring_layout(&self, nodes: &[NodeId], positions: &mut HashMap<NodeId, (f64, f64)>) -> GigabrainResult<()> { 142 + // Simple spring layout (basic implementation) 143 + // In a real implementation, this would use force-directed algorithms 144 + let node_count = nodes.len(); 145 + let area = 600.0 * 600.0; 146 + let _k = (area / node_count as f64).sqrt(); 147 + 148 + // Initialize random positions 149 + for (i, &node_id) in nodes.iter().enumerate() { 150 + let x = 50.0 + (i as f64 * 137.5) % 500.0; // Simple pseudo-random 151 + let y = 50.0 + (i as f64 * 73.3) % 500.0; 152 + positions.insert(node_id, (x, y)); 153 + } 154 + 155 + // Simple repulsion adjustment 156 + for _ in 0..10 { 157 + let mut forces: HashMap<NodeId, (f64, f64)> = HashMap::new(); 158 + 159 + // Calculate repulsive forces 160 + for &node1 in nodes { 161 + let mut fx = 0.0; 162 + let mut fy = 0.0; 163 + 164 + for &node2 in nodes { 165 + if node1 != node2 { 166 + let pos1 = positions[&node1]; 167 + let pos2 = positions[&node2]; 168 + let dx = pos1.0 - pos2.0; 169 + let dy = pos1.1 - pos2.1; 170 + let distance = (dx * dx + dy * dy).sqrt().max(1.0); 171 + let force = _k * _k / distance; 172 + fx += force * dx / distance; 173 + fy += force * dy / distance; 174 + } 175 + } 176 + 177 + forces.insert(node1, (fx, fy)); 178 + } 179 + 180 + // Apply forces 181 + for &node_id in nodes { 182 + if let Some(&(fx, fy)) = forces.get(&node_id) { 183 + let pos = positions.get_mut(&node_id).unwrap(); 184 + pos.0 += fx * 0.1; 185 + pos.1 += fy * 0.1; 186 + 187 + // Keep within bounds 188 + pos.0 = pos.0.max(30.0).min(570.0); 189 + pos.1 = pos.1.max(30.0).min(570.0); 190 + } 191 + } 192 + } 193 + 194 + Ok(()) 195 + } 196 + 197 + fn generate_hierarchical_layout(&self, nodes: &[NodeId], positions: &mut HashMap<NodeId, (f64, f64)>) -> GigabrainResult<()> { 198 + // Simple hierarchical layout - arrange nodes in levels 199 + let level_height = 100.0; 200 + let node_spacing = 80.0; 201 + let start_x = 50.0; 202 + let start_y = 50.0; 203 + 204 + // For simplicity, just arrange in rows 205 + let nodes_per_level = 5; 206 + 207 + for (i, &node_id) in nodes.iter().enumerate() { 208 + let level = i / nodes_per_level; 209 + let position_in_level = i % nodes_per_level; 210 + 211 + let x = start_x + position_in_level as f64 * node_spacing; 212 + let y = start_y + level as f64 * level_height; 213 + 214 + positions.insert(node_id, (x, y)); 215 + } 216 + 217 + Ok(()) 218 + } 219 + 220 + fn generate_random_layout(&self, nodes: &[NodeId], positions: &mut HashMap<NodeId, (f64, f64)>) -> GigabrainResult<()> { 221 + // Simple pseudo-random layout 222 + for (i, &node_id) in nodes.iter().enumerate() { 223 + let x = 50.0 + (i as f64 * 137.5) % 500.0; 224 + let y = 50.0 + (i as f64 * 73.3) % 400.0; 225 + positions.insert(node_id, (x, y)); 226 + } 227 + 228 + Ok(()) 229 + } 230 + 231 + async fn render_nodes(&self, graph: &Arc<Graph>, nodes: &[NodeId], positions: &HashMap<NodeId, (f64, f64)>) -> GigabrainResult<String> { 232 + let mut svg = String::new(); 233 + 234 + for &node_id in nodes { 235 + if let Some(node) = graph.get_node(node_id) { 236 + if let Some(&(x, y)) = positions.get(&node_id) { 237 + let (radius, color, stroke_color) = self.get_node_style(graph, &node)?; 238 + 239 + // Node circle 240 + svg.push_str(&format!( 241 + r#" <circle cx="{}" cy="{}" r="{}" fill="{}" stroke="{}" stroke-width="2" class="node">"#, 242 + x, y, radius, color, stroke_color 243 + )); 244 + svg.push('\n'); 245 + 246 + // Node title (tooltip) 247 + let node_title = self.format_node_tooltip(graph, node_id, &node)?; 248 + svg.push_str(&format!(r#" <title>{}</title>"#, node_title)); 249 + svg.push('\n'); 250 + svg.push_str(" </circle>\n"); 251 + 252 + // Node label 253 + let label = self.format_node_label(graph, node_id, &node)?; 254 + let text_color = self.get_text_color(); 255 + svg.push_str(&format!( 256 + r#" <text x="{}" y="{}" text-anchor="middle" dominant-baseline="central" fill="{}" class="node-label">{}</text>"#, 257 + x, y + radius + 15.0, text_color, label 258 + )); 259 + svg.push('\n'); 260 + } 261 + } 262 + } 263 + 264 + Ok(svg) 265 + } 266 + 267 + async fn render_relationships(&self, graph: &Arc<Graph>, nodes: &[NodeId], positions: &HashMap<NodeId, (f64, f64)>) -> GigabrainResult<String> { 268 + let mut svg = String::new(); 269 + let mut processed_edges = HashSet::new(); 270 + let mut edge_count = 0; 271 + let max_edges = self.options.max_relationships.unwrap_or(usize::MAX); 272 + 273 + for &node_id in nodes { 274 + if edge_count >= max_edges { 275 + break; 276 + } 277 + 278 + let relationships = graph.get_node_relationships(node_id, Direction::Outgoing, None); 279 + for rel in relationships { 280 + if edge_count >= max_edges { 281 + break; 282 + } 283 + 284 + let edge_key = (rel.start_node, rel.end_node, rel.rel_type); 285 + if processed_edges.contains(&edge_key) { 286 + continue; 287 + } 288 + processed_edges.insert(edge_key); 289 + 290 + // Only include edges between nodes in our limited set 291 + if nodes.contains(&rel.end_node) { 292 + if let (Some(&start_pos), Some(&end_pos)) = ( 293 + positions.get(&rel.start_node), 294 + positions.get(&rel.end_node) 295 + ) { 296 + let stroke_color = self.get_edge_color(); 297 + let stroke_width = self.get_edge_width(); 298 + 299 + // Draw edge line 300 + svg.push_str(&format!( 301 + r#" <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}" marker-end="url(#arrowhead)" class="edge">"#, 302 + start_pos.0, start_pos.1, end_pos.0, end_pos.1, stroke_color, stroke_width 303 + )); 304 + svg.push('\n'); 305 + 306 + // Edge title (tooltip) 307 + let edge_title = self.format_edge_tooltip(graph, &rel)?; 308 + svg.push_str(&format!(r#" <title>{}</title>"#, edge_title)); 309 + svg.push('\n'); 310 + svg.push_str(" </line>\n"); 311 + 312 + // Edge label 313 + if self.should_show_edge_labels() { 314 + let mid_x = (start_pos.0 + end_pos.0) / 2.0; 315 + let mid_y = (start_pos.1 + end_pos.1) / 2.0; 316 + let label = self.format_edge_label(graph, &rel)?; 317 + let text_color = self.get_text_color(); 318 + 319 + svg.push_str(&format!( 320 + r#" <text x="{}" y="{}" text-anchor="middle" fill="{}" class="edge-label">{}</text>"#, 321 + mid_x, mid_y - 5.0, text_color, label 322 + )); 323 + svg.push('\n'); 324 + } 325 + 326 + edge_count += 1; 327 + } 328 + } 329 + } 330 + } 331 + 332 + Ok(svg) 333 + } 334 + 335 + fn generate_styles(&self) -> String { 336 + let text_color = self.get_text_color(); 337 + let font_size = self.get_font_size(); 338 + 339 + format!(r#" <defs> 340 + <marker id="arrowhead" markerWidth="10" markerHeight="7" 341 + refX="9" refY="3.5" orient="auto"> 342 + <polygon points="0 0, 10 3.5, 0 7" fill="{}" /> 343 + </marker> 344 + <style> 345 + .node {{ cursor: pointer; }} 346 + .node:hover {{ opacity: 0.8; }} 347 + .edge {{ cursor: pointer; }} 348 + .edge:hover {{ stroke-width: 3; }} 349 + .node-label {{ 350 + font-family: Arial, sans-serif; 351 + font-size: {}px; 352 + fill: {}; 353 + pointer-events: none; 354 + }} 355 + .edge-label {{ 356 + font-family: Arial, sans-serif; 357 + font-size: {}px; 358 + fill: {}; 359 + pointer-events: none; 360 + }} 361 + .title {{ 362 + font-family: Arial, sans-serif; 363 + font-size: 16px; 364 + font-weight: bold; 365 + fill: {}; 366 + }} 367 + .legend {{ 368 + font-family: Arial, sans-serif; 369 + font-size: 12px; 370 + fill: {}; 371 + }} 372 + </style> 373 + </defs> 374 + "#, "#333", font_size, text_color, font_size - 2, text_color, text_color, text_color) 375 + } 376 + 377 + fn generate_legend(&self, graph: &Arc<Graph>, nodes: &[NodeId]) -> String { 378 + let mut legend = String::new(); 379 + let legend_x = 10.0; 380 + let mut legend_y = 50.0; 381 + 382 + legend.push_str(&format!( 383 + r#" <text x="{}" y="{}" class="legend">Legend:</text>"#, 384 + legend_x, legend_y 385 + )); 386 + legend.push('\n'); 387 + 388 + legend_y += 20.0; 389 + legend.push_str(&format!( 390 + r#" <circle cx="{}" cy="{}" r="8" fill="lightblue" stroke="{}" stroke-width="1"/>"#, 391 + legend_x + 10.0, legend_y - 5.0, "#333" 392 + )); 393 + legend.push_str(&format!( 394 + r#" <text x="{}" y="{}" class="legend">Node</text>"#, 395 + legend_x + 25.0, legend_y 396 + )); 397 + legend.push('\n'); 398 + 399 + legend_y += 20.0; 400 + legend.push_str(&format!( 401 + r#" <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="2" marker-end="url(#arrowhead)"/>"#, 402 + legend_x, legend_y - 3.0, legend_x + 20.0, legend_y - 3.0, "#333" 403 + )); 404 + legend.push_str(&format!( 405 + r#" <text x="{}" y="{}" class="legend">Relationship</text>"#, 406 + legend_x + 25.0, legend_y 407 + )); 408 + legend.push('\n'); 409 + 410 + legend 411 + } 412 + 413 + fn calculate_dimensions(&self, positions: &HashMap<NodeId, (f64, f64)>) -> (u32, u32) { 414 + if positions.is_empty() { 415 + return (600, 400); 416 + } 417 + 418 + let mut min_x = f64::INFINITY; 419 + let mut max_x = f64::NEG_INFINITY; 420 + let mut min_y = f64::INFINITY; 421 + let mut max_y = f64::NEG_INFINITY; 422 + 423 + for &(x, y) in positions.values() { 424 + min_x = min_x.min(x); 425 + max_x = max_x.max(x); 426 + min_y = min_y.min(y); 427 + max_y = max_y.max(y); 428 + } 429 + 430 + let padding = 100.0; 431 + let width = (max_x - min_x + 2.0 * padding).max(600.0) as u32; 432 + let height = (max_y - min_y + 2.0 * padding).max(400.0) as u32; 433 + 434 + (width, height) 435 + } 436 + 437 + fn create_empty_svg(&self) -> String { 438 + format!(r#"<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg"> 439 + <rect width="100%" height="100%" fill="white"/> 440 + <text x="200" y="100" text-anchor="middle" dominant-baseline="central" 441 + font-family="Arial, sans-serif" font-size="16" fill="black"> 442 + Empty Graph - No Nodes to Visualize 443 + </text> 444 + </svg>"#) 445 + } 446 + 447 + // Helper methods for styling 448 + 449 + fn get_node_style(&self, graph: &Arc<Graph>, node: &crate::core::Node) -> GigabrainResult<(f64, &'static str, &'static str)> { 450 + let radius = match self.options.node_size { 451 + crate::visualization::NodeSize::Small => 12.0, 452 + crate::visualization::NodeSize::Medium => 16.0, 453 + crate::visualization::NodeSize::Large => 24.0, 454 + }; 455 + 456 + let (color, stroke) = match self.options.color_scheme { 457 + crate::visualization::ColorScheme::Dark => ("#4a90e2", "#ffffff"), 458 + crate::visualization::ColorScheme::Light => ("#87ceeb", "#333333"), 459 + crate::visualization::ColorScheme::Colorful => { 460 + if node.labels.len() > 1 { 461 + ("#ff6b6b", "#333333") 462 + } else if node.labels.len() == 1 { 463 + ("#4ecdc4", "#333333") 464 + } else { 465 + ("#95e1d3", "#333333") 466 + } 467 + } 468 + crate::visualization::ColorScheme::Monochrome => ("#d3d3d3", "#333333"), 469 + crate::visualization::ColorScheme::Default => ("#87ceeb", "#333333"), 470 + }; 471 + 472 + Ok((radius, color, stroke)) 473 + } 474 + 475 + fn get_edge_color(&self) -> &'static str { 476 + match self.options.color_scheme { 477 + crate::visualization::ColorScheme::Dark => "#cccccc", 478 + crate::visualization::ColorScheme::Light => "#666666", 479 + crate::visualization::ColorScheme::Colorful => "#ff7f0e", 480 + crate::visualization::ColorScheme::Monochrome => "#888888", 481 + crate::visualization::ColorScheme::Default => "#333333", 482 + } 483 + } 484 + 485 + fn get_edge_width(&self) -> u32 { 486 + 2 487 + } 488 + 489 + fn get_background_color(&self) -> &'static str { 490 + match self.options.color_scheme { 491 + crate::visualization::ColorScheme::Dark => "#2d2d2d", 492 + crate::visualization::ColorScheme::Light => "#ffffff", 493 + crate::visualization::ColorScheme::Colorful => "#ffffff", 494 + crate::visualization::ColorScheme::Monochrome => "#ffffff", 495 + crate::visualization::ColorScheme::Default => "#ffffff", 496 + } 497 + } 498 + 499 + fn get_text_color(&self) -> &'static str { 500 + match self.options.color_scheme { 501 + crate::visualization::ColorScheme::Dark => "#ffffff", 502 + crate::visualization::ColorScheme::Light => "#000000", 503 + crate::visualization::ColorScheme::Colorful => "#333333", 504 + crate::visualization::ColorScheme::Monochrome => "#000000", 505 + crate::visualization::ColorScheme::Default => "#000000", 506 + } 507 + } 508 + 509 + fn get_font_size(&self) -> u32 { 510 + match self.options.font_size { 511 + crate::visualization::FontSize::Small => 10, 512 + crate::visualization::FontSize::Medium => 12, 513 + crate::visualization::FontSize::Large => 16, 514 + } 515 + } 516 + 517 + fn should_show_edge_labels(&self) -> bool { 518 + // Only show edge labels for smaller graphs to avoid clutter 519 + true 520 + } 521 + 522 + fn format_node_label(&self, graph: &Arc<Graph>, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult<String> { 523 + let mut label = format!("N{}", node_id.0); 524 + 525 + if self.options.include_labels && !node.labels.is_empty() { 526 + let schema = graph.schema().read(); 527 + if let Some(first_label) = node.labels.iter() 528 + .filter_map(|&label_id| schema.get_label_name(label_id)) 529 + .next() { 530 + label = first_label.to_string(); 531 + } 532 + } 533 + 534 + Ok(label) 535 + } 536 + 537 + fn format_node_tooltip(&self, graph: &Arc<Graph>, node_id: NodeId, node: &crate::core::Node) -> GigabrainResult<String> { 538 + let mut tooltip = format!("Node {}", node_id.0); 539 + 540 + if self.options.include_labels && !node.labels.is_empty() { 541 + let schema = graph.schema().read(); 542 + let labels: Vec<String> = node.labels.iter() 543 + .filter_map(|&label_id| schema.get_label_name(label_id)) 544 + .map(|name| name.to_string()) 545 + .collect(); 546 + 547 + if !labels.is_empty() { 548 + tooltip.push_str(&format!(" ({})", labels.join(", "))); 549 + } 550 + } 551 + 552 + if self.options.include_properties && !node.properties.is_empty() { 553 + tooltip.push_str(" - Properties: "); 554 + let schema = graph.schema().read(); 555 + let props: Vec<String> = node.properties.iter() 556 + .filter_map(|(key_id, value)| { 557 + schema.get_property_key_name(*key_id) 558 + .map(|name| format!("{}: {:?}", name, value)) 559 + }) 560 + .collect(); 561 + tooltip.push_str(&props.join(", ")); 562 + } 563 + 564 + Ok(tooltip) 565 + } 566 + 567 + fn format_edge_label(&self, graph: &Arc<Graph>, rel: &crate::core::Relationship) -> GigabrainResult<String> { 568 + let schema = graph.schema().read(); 569 + let rel_type_name = schema.get_relationship_type_name(rel.rel_type) 570 + .map(|s| s.to_string()) 571 + .unwrap_or_else(|| "UNKNOWN".to_string()); 572 + 573 + Ok(rel_type_name.to_string()) 574 + } 575 + 576 + fn format_edge_tooltip(&self, graph: &Arc<Graph>, rel: &crate::core::Relationship) -> GigabrainResult<String> { 577 + let schema = graph.schema().read(); 578 + let rel_type_name = schema.get_relationship_type_name(rel.rel_type) 579 + .map(|s| s.to_string()) 580 + .unwrap_or_else(|| "UNKNOWN".to_string()); 581 + 582 + let mut tooltip = format!("Relationship: {} -> {} ({})", 583 + rel.start_node.0, rel.end_node.0, rel_type_name); 584 + 585 + if self.options.include_properties && !rel.properties.is_empty() { 586 + tooltip.push_str(" - Properties: "); 587 + let props: Vec<String> = rel.properties.iter() 588 + .filter_map(|(key_id, value)| { 589 + schema.get_property_key_name(*key_id) 590 + .map(|name| format!("{}: {:?}", name, value)) 591 + }) 592 + .collect(); 593 + tooltip.push_str(&props.join(", ")); 594 + } 595 + 596 + Ok(tooltip) 597 + } 598 + }
+219
tests/visualization_tests.rs
··· 1 + use gigabrain::Graph; 2 + use gigabrain::visualization::{GraphVisualizer, VisualizationOptions, VisualizationFormat, LayoutAlgorithm}; 3 + use std::sync::Arc; 4 + 5 + #[tokio::test] 6 + async fn test_ascii_visualization() { 7 + let graph = Arc::new(Graph::new()); 8 + 9 + // Create some test data 10 + let alice = graph.create_node(); 11 + let bob = graph.create_node(); 12 + 13 + // Add labels and properties 14 + { 15 + let mut schema = graph.schema().write(); 16 + let person_label = schema.get_or_create_label("Person"); 17 + let name_prop = schema.get_or_create_property_key("name"); 18 + let knows_rel = schema.get_or_create_relationship_type("KNOWS"); 19 + drop(schema); 20 + 21 + // Add properties and labels to nodes 22 + graph.update_node(alice, |node| { 23 + node.add_label(person_label); 24 + node.properties.insert(name_prop, gigabrain::core::PropertyValue::String("Alice".to_string())); 25 + }).unwrap(); 26 + 27 + graph.update_node(bob, |node| { 28 + node.add_label(person_label); 29 + node.properties.insert(name_prop, gigabrain::core::PropertyValue::String("Bob".to_string())); 30 + }).unwrap(); 31 + 32 + // Create relationship 33 + let _rel = graph.create_relationship(alice, bob, knows_rel).unwrap(); 34 + } 35 + 36 + // Test ASCII visualization 37 + let options = VisualizationOptions { 38 + format: VisualizationFormat::Ascii, 39 + max_nodes: Some(10), 40 + max_relationships: Some(10), 41 + include_properties: true, 42 + include_labels: true, 43 + layout: LayoutAlgorithm::Hierarchical, 44 + color_scheme: gigabrain::visualization::ColorScheme::Default, 45 + node_size: gigabrain::visualization::NodeSize::Medium, 46 + font_size: gigabrain::visualization::FontSize::Medium, 47 + }; 48 + 49 + let visualizer = GraphVisualizer::with_options(graph.clone(), options); 50 + let result = visualizer.visualize().await; 51 + 52 + assert!(result.is_ok()); 53 + let output = result.unwrap(); 54 + assert!(output.contains("Graph Visualization")); 55 + assert!(output.contains("Alice")); 56 + assert!(output.contains("Bob")); 57 + assert!(output.contains("KNOWS")); 58 + } 59 + 60 + #[tokio::test] 61 + async fn test_json_visualization() { 62 + let graph = Arc::new(Graph::new()); 63 + 64 + // Create test data 65 + let node1 = graph.create_node(); 66 + let node2 = graph.create_node(); 67 + 68 + { 69 + let mut schema = graph.schema().write(); 70 + let test_rel = schema.get_or_create_relationship_type("TEST"); 71 + drop(schema); 72 + 73 + let _rel = graph.create_relationship(node1, node2, test_rel).unwrap(); 74 + } 75 + 76 + // Test JSON visualization 77 + let options = VisualizationOptions { 78 + format: VisualizationFormat::Json, 79 + max_nodes: Some(5), 80 + max_relationships: Some(5), 81 + include_properties: false, 82 + include_labels: false, 83 + layout: LayoutAlgorithm::Spring, 84 + color_scheme: gigabrain::visualization::ColorScheme::Default, 85 + node_size: gigabrain::visualization::NodeSize::Medium, 86 + font_size: gigabrain::visualization::FontSize::Medium, 87 + }; 88 + 89 + let visualizer = GraphVisualizer::with_options(graph.clone(), options); 90 + let result = visualizer.visualize().await; 91 + 92 + assert!(result.is_ok()); 93 + let output = result.unwrap(); 94 + 95 + // Parse as JSON to verify it's valid 96 + let json: serde_json::Value = serde_json::from_str(&output).unwrap(); 97 + assert!(json["nodes"].is_array()); 98 + assert!(json["edges"].is_array()); 99 + assert!(json["metadata"].is_object()); 100 + 101 + let nodes = json["nodes"].as_array().unwrap(); 102 + let edges = json["edges"].as_array().unwrap(); 103 + 104 + assert_eq!(nodes.len(), 2); 105 + assert_eq!(edges.len(), 1); 106 + } 107 + 108 + #[tokio::test] 109 + async fn test_dot_visualization() { 110 + let graph = Arc::new(Graph::new()); 111 + 112 + // Create simple test data 113 + let node1 = graph.create_node(); 114 + let node2 = graph.create_node(); 115 + 116 + { 117 + let mut schema = graph.schema().write(); 118 + let connects_rel = schema.get_or_create_relationship_type("CONNECTS"); 119 + drop(schema); 120 + 121 + let _rel = graph.create_relationship(node1, node2, connects_rel).unwrap(); 122 + } 123 + 124 + // Test DOT visualization 125 + let options = VisualizationOptions { 126 + format: VisualizationFormat::Dot, 127 + max_nodes: Some(5), 128 + max_relationships: Some(5), 129 + include_properties: true, 130 + include_labels: true, 131 + layout: LayoutAlgorithm::Spring, 132 + color_scheme: gigabrain::visualization::ColorScheme::Light, 133 + node_size: gigabrain::visualization::NodeSize::Large, 134 + font_size: gigabrain::visualization::FontSize::Large, 135 + }; 136 + 137 + let visualizer = GraphVisualizer::with_options(graph.clone(), options); 138 + let result = visualizer.visualize().await; 139 + 140 + assert!(result.is_ok()); 141 + let output = result.unwrap(); 142 + 143 + // Check DOT format basics 144 + assert!(output.starts_with("digraph G")); 145 + assert!(output.contains("layout=fdp")); 146 + assert!(output.contains("CONNECTS")); 147 + assert!(output.ends_with("}\n")); 148 + } 149 + 150 + #[tokio::test] 151 + async fn test_svg_visualization() { 152 + let graph = Arc::new(Graph::new()); 153 + 154 + // Create test data 155 + let _node1 = graph.create_node(); 156 + 157 + // Test SVG visualization 158 + let options = VisualizationOptions { 159 + format: VisualizationFormat::Svg, 160 + max_nodes: Some(1), 161 + max_relationships: Some(0), 162 + include_properties: false, 163 + include_labels: false, 164 + layout: LayoutAlgorithm::Circular, 165 + color_scheme: gigabrain::visualization::ColorScheme::Colorful, 166 + node_size: gigabrain::visualization::NodeSize::Small, 167 + font_size: gigabrain::visualization::FontSize::Small, 168 + }; 169 + 170 + let visualizer = GraphVisualizer::with_options(graph.clone(), options); 171 + let result = visualizer.visualize().await; 172 + 173 + assert!(result.is_ok()); 174 + let output = result.unwrap(); 175 + 176 + // Check SVG format basics 177 + assert!(output.starts_with("<svg")); 178 + assert!(output.contains("xmlns=\"http://www.w3.org/2000/svg\"")); 179 + assert!(output.contains("Graph Visualization")); 180 + assert!(output.ends_with("</svg>\n")); 181 + } 182 + 183 + #[tokio::test] 184 + async fn test_empty_graph_visualization() { 185 + let graph = Arc::new(Graph::new()); 186 + 187 + let options = VisualizationOptions::default(); 188 + let visualizer = GraphVisualizer::with_options(graph.clone(), options); 189 + let result = visualizer.visualize().await; 190 + 191 + assert!(result.is_ok()); 192 + let output = result.unwrap(); 193 + assert!(output.contains("Empty graph")); 194 + } 195 + 196 + #[test] 197 + fn test_visualization_stats() { 198 + let graph = Arc::new(Graph::new()); 199 + 200 + // Create test data 201 + let node1 = graph.create_node(); 202 + let node2 = graph.create_node(); 203 + 204 + { 205 + let mut schema = graph.schema().write(); 206 + let test_rel = schema.get_or_create_relationship_type("TEST"); 207 + drop(schema); 208 + 209 + let _rel = graph.create_relationship(node1, node2, test_rel).unwrap(); 210 + } 211 + 212 + let visualizer = GraphVisualizer::new(graph); 213 + let stats = visualizer.get_stats(); 214 + 215 + assert_eq!(stats.node_count, 2); 216 + assert_eq!(stats.edge_count, 1); 217 + assert!(stats.avg_degree > 0.0); 218 + assert!(stats.max_degree > 0); 219 + }