this repo has no description
0
fork

Configure Feed

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

at main 598 lines 23 kB view raw
1use crate::{Graph, Result as GigabrainResult, GigabrainError}; 2use crate::{NodeId}; 3use crate::core::relationship::Direction; 4use crate::visualization::VisualizationOptions; 5use std::collections::{HashMap, HashSet}; 6use std::sync::Arc; 7 8/// SVG renderer for web-based graph visualization 9pub struct SvgRenderer<'a> { 10 options: &'a VisualizationOptions, 11} 12 13impl<'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}