this repo has no description
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}