diff --git a/semantica/visualization/analytics_visualizer.py b/semantica/visualization/analytics_visualizer.py index 75e1865d..f5dac384 100644 --- a/semantica/visualization/analytics_visualizer.py +++ b/semantica/visualization/analytics_visualizer.py @@ -34,10 +34,19 @@ License: MIT from pathlib import Path from typing import Any, Dict, List, Optional, Union -import numpy as np -import plotly.express as px -import plotly.graph_objects as go -from plotly.subplots import make_subplots +try: + import numpy as np +except ImportError: + np = None + +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None from ..utils.exceptions import ProcessingError from ..utils.logging import get_logger @@ -68,6 +77,23 @@ class AnalyticsVisualizer: except (KeyError, AttributeError): self.color_scheme = ColorScheme.DEFAULT + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for analytics visualization. " + "Install with: pip install plotly" + ) + if np is None: + raise ProcessingError( + "NumPy is required for analytics visualization. " + "Install with: pip install numpy" + ) + + def visualize_centrality(self, *args, **kwargs): + """Alias for visualize_centrality_rankings.""" + return self.visualize_centrality_rankings(*args, **kwargs) + def visualize_centrality_rankings( self, centrality: Dict[str, Any], @@ -91,6 +117,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="AnalyticsVisualizer", @@ -188,6 +215,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing community structure") # Use KG visualizer for community visualization @@ -217,6 +245,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing connectivity analysis") # Extract metrics @@ -291,6 +320,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing degree distribution") # Calculate degrees @@ -360,6 +390,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing graph metrics dashboard") # Extract key metrics @@ -498,6 +529,7 @@ class AnalyticsVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing centrality comparison") # Extract top nodes for each centrality type diff --git a/semantica/visualization/embedding_visualizer.py b/semantica/visualization/embedding_visualizer.py index c0162172..24782e1e 100644 --- a/semantica/visualization/embedding_visualizer.py +++ b/semantica/visualization/embedding_visualizer.py @@ -35,10 +35,16 @@ from typing import Any, Dict, List, Optional, Tuple, Union import matplotlib.pyplot as plt import numpy as np -import plotly.express as px -import plotly.graph_objects as go -import seaborn as sns -from plotly.subplots import make_subplots + +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None + from sklearn.decomposition import PCA from sklearn.manifold import TSNE @@ -85,6 +91,14 @@ class EmbeddingVisualizer: self.color_scheme = ColorScheme.DEFAULT self.point_size = config.get("point_size", 5) + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for embedding visualization. " + "Install with: pip install plotly" + ) + def visualize_2d_projection( self, embeddings: np.ndarray, @@ -92,17 +106,30 @@ class EmbeddingVisualizer: method: str = "umap", output: str = "interactive", file_path: Optional[Union[str, Path]] = None, + color_by: Optional[List[Any]] = None, + size_by: Optional[List[float]] = None, + hover_data: Optional[List[Dict[str, Any]]] = None, **options, ) -> Optional[Any]: """ Visualize embeddings in 2D using dimensionality reduction. + Implements the 5-step visualization process: + 1. Problem setting: Dimensionality reduction choice + 2. Data analysis: Logs embedding statistics + 3. Layout: 2D Projection (UMAP/t-SNE/PCA) + 4. Styling: Configurable color and size mapping + 5. Interaction: Rich hover data + Args: embeddings: Embedding matrix (n_samples, n_features) - labels: Optional labels for coloring points + labels: Optional labels for points (used as default color_by if provided) method: Reduction method ("umap", "tsne", "pca") output: Output type ("interactive", "html", "png", "svg") file_path: Output file path + color_by: List of values to map to color (overrides labels) + size_by: List of values to map to point size + hover_data: List of dictionaries containing metadata for each point **options: Additional options: - n_components: Number of components (default: 2) - perplexity: Perplexity for t-SNE @@ -111,6 +138,7 @@ class EmbeddingVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="EmbeddingVisualizer", @@ -119,6 +147,10 @@ class EmbeddingVisualizer: try: self.logger.info(f"Visualizing 2D projection using {method}") + + # Step 2: Data Analysis + n_samples, n_features = embeddings.shape + self.logger.info(f"Embedding Analysis: {n_samples} samples, {n_features} dimensions") if embeddings.shape[1] <= 2: # Already 2D or less, use directly @@ -138,7 +170,14 @@ class EmbeddingVisualizer: tracking_id, message="Generating visualization..." ) result = self._visualize_2d_plotly( - projected, labels, output, file_path, **options + projected, + labels, + output, + file_path, + color_by=color_by, + size_by=size_by, + hover_data=hover_data, + **options ) self.progress_tracker.stop_tracking( @@ -176,6 +215,7 @@ class EmbeddingVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="EmbeddingVisualizer", @@ -238,6 +278,7 @@ class EmbeddingVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="EmbeddingVisualizer", @@ -340,6 +381,7 @@ class EmbeddingVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="EmbeddingVisualizer", @@ -444,6 +486,7 @@ class EmbeddingVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="EmbeddingVisualizer", diff --git a/semantica/visualization/kg_visualizer.py b/semantica/visualization/kg_visualizer.py index 911c2414..231440f3 100644 --- a/semantica/visualization/kg_visualizer.py +++ b/semantica/visualization/kg_visualizer.py @@ -33,12 +33,23 @@ License: MIT from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -import matplotlib.patches as mpatches -import matplotlib.pyplot as plt import numpy as np -import plotly.express as px -import plotly.graph_objects as go -from plotly.subplots import make_subplots + +try: + import matplotlib.patches as mpatches + import matplotlib.pyplot as plt +except ImportError: + mpatches = None + plt = None + +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None from ..utils.exceptions import ProcessingError from ..utils.logging import get_logger @@ -96,25 +107,47 @@ class KGVisualizer: self.hierarchical_layout = HierarchicalLayout(**config) self.circular_layout = CircularLayout(**config) + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for KG visualization. " + "Install with: pip install plotly" + ) + def visualize_network( self, graph: Dict[str, Any], output: str = "interactive", file_path: Optional[Union[str, Path]] = None, + node_color_by: str = "type", + node_size_by: Optional[str] = None, + hover_data: Optional[List[str]] = None, **options, ) -> Optional[Any]: """ Visualize knowledge graph as interactive network. + Implements the 5-step visualization process: + 1. Problem setting: implicit in graph selection + 2. Data analysis: logs graph statistics + 3. Layout: configurable via options + 4. Styling: configurable node color/size mappings + 5. Interaction: rich hover data and zoom capabilities + Args: graph: Knowledge graph dictionary with entities and relationships output: Output type ("interactive", "html", "png", "svg") file_path: Output file path (required for non-interactive) + node_color_by: Property to map to node color (default: "type") + node_size_by: Property to map to node size (default: fixed) + hover_data: List of properties to show in hover tooltip **options: Additional visualization options Returns: Plotly figure (if interactive) or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="KGVisualizer", @@ -137,18 +170,34 @@ class KGVisualizer: ) raise ProcessingError("No entities found in graph") + # Step 2: Data Analysis - Understand data structure + nodes = self._extract_nodes(entities) + edges = self._extract_edges(relationships, entities) + + num_nodes = len(nodes) + num_edges = len(edges) + entity_types = set(n.get("type", "unknown") for n in nodes) + + self.logger.info(f"Graph Structure Analysis: {num_nodes} nodes, {num_edges} edges") + self.logger.info(f"Entity Types: {', '.join(sorted(entity_types))}") + # Build node and edge lists self.progress_tracker.update_tracking( tracking_id, message="Building node and edge lists..." ) - nodes = self._extract_nodes(entities) - edges = self._extract_edges(relationships, entities) self.progress_tracker.update_tracking( tracking_id, message="Generating visualization..." ) result = self._visualize_network_plotly( - nodes, edges, output, file_path, **options + nodes, + edges, + output, + file_path, + node_color_by=node_color_by, + node_size_by=node_size_by, + hover_data=hover_data, + **options ) self.progress_tracker.stop_tracking( @@ -184,6 +233,7 @@ class KGVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing knowledge graph communities") entities = graph.get("entities", []) @@ -242,6 +292,7 @@ class KGVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info( f"Visualizing knowledge graph with {centrality_type} centrality" ) @@ -295,6 +346,7 @@ class KGVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing entity type distribution") entities = graph.get("entities", []) @@ -339,6 +391,7 @@ class KGVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing relationship matrix") entities = graph.get("entities", []) @@ -442,6 +495,9 @@ class KGVisualizer: edges: List[Dict[str, Any]], output: str, file_path: Optional[Path], + node_color_by: str = "type", + node_size_by: Optional[str] = None, + hover_data: Optional[List[str]] = None, **options, ) -> Optional[Any]: """Create Plotly network visualization.""" @@ -458,11 +514,91 @@ class KGVisualizer: else: pos = self.force_layout.compute_layout(node_ids, edge_tuples, **options) - # Get entity type colors - entity_types = list(set(n.get("type", "entity") for n in nodes)) - type_colors = ColorPalette.get_entity_type_colors( - entity_types, self.color_scheme - ) + # Step 4: Styling - Node Colors + # Priority 1: Explicit color set in node (e.g. from visualize_communities) + # Priority 2: Mapped property via node_color_by + + node_colors = [] + if any("color" in n for n in nodes): + node_colors = [n.get("color", "#888") for n in nodes if n["id"] in pos] + else: + if node_color_by == "type": + entity_types = list(set(n.get("type", "entity") for n in nodes)) + type_colors = ColorPalette.get_entity_type_colors( + entity_types, self.color_scheme + ) + node_colors = [ + type_colors.get(n.get("type", "entity"), "#888") + for n in nodes + if n["id"] in pos + ] + else: + # Custom property mapping + values = [] + for n in nodes: + if n["id"] not in pos: continue + val = n.get(node_color_by) or n.get("metadata", {}).get(node_color_by, "Unknown") + values.append(str(val)) + + unique_vals = sorted(list(set(values))) + colors = ColorPalette.get_colors(self.color_scheme, len(unique_vals)) + val_map = dict(zip(unique_vals, colors)) + + node_colors = [] + for n in nodes: + if n["id"] not in pos: continue + val = str(n.get(node_color_by) or n.get("metadata", {}).get(node_color_by, "Unknown")) + node_colors.append(val_map.get(val, "#888")) + + # Step 4: Styling - Node Sizes + # Priority 1: Explicit size set in node (e.g. from visualize_centrality) + # Priority 2: Mapped property via node_size_by + + node_sizes = [] + if any("size" in n for n in nodes) and not node_size_by: + node_sizes = [n.get("size", self.node_size) for n in nodes if n["id"] in pos] + elif node_size_by: + raw_sizes = [] + valid_indices = [] + for i, n in enumerate(nodes): + if n["id"] not in pos: continue + val = n.get(node_size_by) or n.get("metadata", {}).get(node_size_by, 0) + try: + s = float(val) + except (ValueError, TypeError): + s = 0 + raw_sizes.append(s) + valid_indices.append(i) + + # Normalize to range [10, 50] + if raw_sizes and max(raw_sizes) > min(raw_sizes): + min_s, max_s = min(raw_sizes), max(raw_sizes) + node_sizes = [10 + 40 * ((s - min_s) / (max_s - min_s)) for s in raw_sizes] + else: + node_sizes = [self.node_size] * len(raw_sizes) + else: + node_sizes = [self.node_size for n in nodes if n["id"] in pos] + + # Step 5: Interaction - Rich Hover + node_text = [] + for n in nodes: + if n["id"] not in pos: continue + + # Basic info + text = f"{n['label']}
Type: {n.get('type', 'entity')}" + + # Additional hover data + if hover_data: + for field in hover_data: + val = n.get(field) or n.get("metadata", {}).get(field, "N/A") + text += f"
{field}: {val}" + + # Add dynamic styling info if relevant + if node_size_by: + val = n.get(node_size_by) or n.get("metadata", {}).get(node_size_by, "N/A") + text += f"
{node_size_by}: {val}" + + node_text.append(text) # Prepare edge traces edge_x = [] @@ -485,13 +621,6 @@ class KGVisualizer: # Prepare node traces node_x = [pos[n["id"]][0] for n in nodes if n["id"] in pos] node_y = [pos[n["id"]][1] for n in nodes if n["id"] in pos] - node_text = [n["label"] for n in nodes if n["id"] in pos] - node_colors = [ - type_colors.get(n.get("type", "entity"), "#888") - for n in nodes - if n["id"] in pos - ] - node_sizes = [n.get("size", self.node_size) for n in nodes if n["id"] in pos] node_trace = go.Scatter( x=node_x, @@ -499,9 +628,12 @@ class KGVisualizer: mode="markers+text", hoverinfo="text", text=node_text, - textposition="middle center", + textposition="top center", marker=dict( - size=node_sizes, color=node_colors, line=dict(width=2, color="white") + size=node_sizes, + color=node_colors, + line=dict(width=2, color="white"), + opacity=0.9 ), ) diff --git a/semantica/visualization/ontology_visualizer.py b/semantica/visualization/ontology_visualizer.py index bf4cc446..9cad4ea5 100644 --- a/semantica/visualization/ontology_visualizer.py +++ b/semantica/visualization/ontology_visualizer.py @@ -37,10 +37,17 @@ from typing import Any, Dict, List, Optional, Union import matplotlib.patches as mpatches import matplotlib.pyplot as plt -import plotly.express as px -import plotly.graph_objects as go + +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None + from matplotlib.patches import FancyBboxPatch -from plotly.subplots import make_subplots try: import graphviz @@ -90,26 +97,60 @@ class OntologyVisualizer: self.color_scheme = ColorScheme.DEFAULT self.node_size = config.get("node_size", 15) + def _check_dependencies(self, require_graphviz: bool = False): + """Check if dependencies are available.""" + if require_graphviz: + if graphviz is None: + raise ProcessingError( + "Graphviz is required for DOT export. " + "Install with: pip install graphviz" + ) + else: + if px is None or go is None: + raise ProcessingError( + "Plotly is required for ontology visualization. " + "Install with: pip install plotly" + ) + def visualize_hierarchy( self, ontology: Dict[str, Any], output: str = "interactive", file_path: Optional[Union[str, Path]] = None, + node_color_by: str = "level", + node_size_by: str = "instances", + hover_data: Optional[List[str]] = None, **options, ) -> Optional[Any]: """ Visualize class hierarchy as tree. + Implements the 5-step visualization process: + 1. Problem setting: Implicit in ontology selection + 2. Data analysis: Logs ontology statistics + 3. Layout: Hierarchical tree layout + 4. Styling: Configurable node color (e.g. by level) and size (e.g. by instances) + 5. Interaction: Rich hover data + Args: ontology: Ontology dictionary with classes, or SemanticNetwork object, or ontology generator result output: Output type ("interactive", "html", "png", "svg", "dot") file_path: Output file path + node_color_by: Property to map to node color (default: "level") + node_size_by: Property to map to node size (default: "instances") + hover_data: List of properties to show in hover tooltip **options: Additional options Returns: Visualization figure or None """ + # Check dependencies + if output == "dot": + self._check_dependencies(require_graphviz=True) + else: + self._check_dependencies() + tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="OntologyVisualizer", @@ -150,6 +191,15 @@ class OntologyVisualizer: "No classes found in ontology. Please provide classes or a semantic network." ) + # Step 2: Data Analysis + num_classes = len(classes) + max_depth = 0 + for cls in classes: + depth = self._calculate_class_depth(cls, classes) + max_depth = max(max_depth, depth) + + self.logger.info(f"Ontology Analysis: {num_classes} classes, max depth {max_depth}") + # If output is dot and graphviz is available, use it if output == "dot" and graphviz is not None and file_path: self.progress_tracker.update_tracking( @@ -175,7 +225,14 @@ class OntologyVisualizer: tracking_id, message="Generating visualization..." ) result = self._visualize_hierarchy_plotly( - hierarchy, classes, output, file_path, **options + hierarchy, + classes, + output, + file_path, + node_color_by=node_color_by, + node_size_by=node_size_by, + hover_data=hover_data, + **options ) self.progress_tracker.stop_tracking( @@ -209,6 +266,7 @@ class OntologyVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing ontology properties") # Handle different input formats @@ -258,6 +316,7 @@ class OntologyVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing ontology structure") classes = ontology.get("classes", []) @@ -342,6 +401,7 @@ class OntologyVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing class-property matrix") classes = ontology.get("classes", []) @@ -411,6 +471,7 @@ class OntologyVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing ontology metrics") classes = ontology.get("classes", []) @@ -603,6 +664,7 @@ class OntologyVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing semantic model") # Handle OntologyGenerator result @@ -651,6 +713,9 @@ class OntologyVisualizer: classes: List[Dict[str, Any]], output: str, file_path: Optional[Path], + node_color_by: str = "level", + node_size_by: str = "instances", + hover_data: Optional[List[str]] = None, **options, ) -> Optional[Any]: """Create Plotly hierarchy visualization.""" @@ -676,7 +741,16 @@ class OntologyVisualizer: edges = [] def add_node_and_children(cls_name, level=0, x_offset=0): - nodes.append({"name": cls_name, "level": level, "x": x_offset, "y": -level}) + # Find class data + cls_data = all_class_names.get(cls_name, {}) + + nodes.append({ + "name": cls_name, + "level": level, + "x": x_offset, + "y": -level, + "data": cls_data + }) children = hierarchy.get(cls_name, []) child_width = 1.0 / max(len(children), 1) @@ -692,6 +766,63 @@ class OntologyVisualizer: root_x = (i + 0.5) * root_width add_node_and_children(root, 0, root_x) + # Step 4: Styling - Node Colors + # Default to coloring by level + node_colors = [] + if node_color_by == "level": + node_colors = [n["level"] for n in nodes] + else: + # Map custom property + values = [] + for n in nodes: + val = str(n["data"].get(node_color_by, "Unknown")) + values.append(val) + + unique_vals = sorted(list(set(values))) + colors = ColorPalette.get_colors(self.color_scheme, len(unique_vals)) + val_map = dict(zip(unique_vals, colors)) + node_colors = [val_map.get(str(n["data"].get(node_color_by, "Unknown")), "#888") for n in nodes] + + # Step 4: Styling - Node Sizes + # Default to sizing by instances (if available) or fixed size + node_sizes = [] + if node_size_by: + raw_sizes = [] + for n in nodes: + val = n["data"].get(node_size_by, 0) + try: + s = float(val) + except (ValueError, TypeError): + s = 0 + raw_sizes.append(s) + + if raw_sizes and max(raw_sizes) > min(raw_sizes): + min_s, max_s = min(raw_sizes), max(raw_sizes) + # Scale between 10 and 40 + node_sizes = [10 + 30 * ((s - min_s) / (max_s - min_s)) for s in raw_sizes] + else: + node_sizes = [self.node_size] * len(nodes) + else: + node_sizes = [self.node_size] * len(nodes) + + # Step 5: Interaction - Rich Hover + node_text = [] + for n in nodes: + cls_data = n["data"] + text = f"{n['name']}
Level: {n['level']}" + + # Add instances if available + if "instances" in cls_data: + text += f"
Instances: {cls_data['instances']}" + + # Additional hover data + if hover_data: + for field in hover_data: + val = cls_data.get(field, "N/A") + text += f"
{field}: {val}" + + node_text.append(text) + # Create visualization edge_x = [] edge_y = [] @@ -714,18 +845,21 @@ class OntologyVisualizer: node_x = [n["x"] for n in nodes] node_y = [n["y"] for n in nodes] - node_text = [n["name"] for n in nodes] node_trace = go.Scatter( x=node_x, y=node_y, mode="markers+text", - text=node_text, - textposition="middle center", + text=[n["name"] for n in nodes], # Keep label on node simple + hovertext=node_text, # Rich hover text + hoverinfo="text", + textposition="top center", marker=dict( - size=self.node_size * 10, - color="lightblue", - line=dict(width=2, color="darkblue"), + size=node_sizes, + color=node_colors, + colorscale="Viridis" if node_color_by == "level" else None, + line=dict(width=2, color="white"), + showscale=True if node_color_by == "level" else False ), ) diff --git a/semantica/visualization/quality_visualizer.py b/semantica/visualization/quality_visualizer.py index 20ca4a7c..bc8aa6cc 100644 --- a/semantica/visualization/quality_visualizer.py +++ b/semantica/visualization/quality_visualizer.py @@ -33,9 +33,19 @@ License: MIT from pathlib import Path from typing import Any, Dict, List, Optional, Union -import plotly.express as px -import plotly.graph_objects as go -from plotly.subplots import make_subplots +try: + import numpy as np +except ImportError: + np = None + +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None from ..utils.exceptions import ProcessingError from ..utils.logging import get_logger @@ -66,6 +76,14 @@ class QualityVisualizer: except (KeyError, AttributeError): self.color_scheme = ColorScheme.DEFAULT + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for quality visualization. " + "Install with: pip install plotly" + ) + def visualize_dashboard( self, quality_report: Any, @@ -85,6 +103,7 @@ class QualityVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="QualityVisualizer", @@ -266,6 +285,7 @@ class QualityVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing quality score distribution") fig = go.Figure( @@ -315,6 +335,7 @@ class QualityVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing quality issues") # Extract issues @@ -405,6 +426,7 @@ class QualityVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing completeness metrics") # Extract metrics @@ -468,6 +490,13 @@ class QualityVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() + if np is None: + raise ProcessingError( + "NumPy is required for consistency heatmap visualization. " + "Install with: pip install numpy" + ) + self.logger.info("Visualizing consistency heatmap") # Extract consistency matrix @@ -477,8 +506,6 @@ class QualityVisualizer: if not matrix: raise ProcessingError("No consistency matrix found") - import numpy as np - matrix = np.array(matrix) fig = go.Figure( diff --git a/semantica/visualization/semantic_network_visualizer.py b/semantica/visualization/semantic_network_visualizer.py index e28946c5..7a6dcb89 100644 --- a/semantica/visualization/semantic_network_visualizer.py +++ b/semantica/visualization/semantic_network_visualizer.py @@ -33,8 +33,12 @@ License: MIT from pathlib import Path from typing import Any, Dict, List, Optional, Union -import plotly.express as px -import plotly.graph_objects as go +try: + import plotly.express as px + import plotly.graph_objects as go +except ImportError: + px = None + go = None from ..utils.exceptions import ProcessingError from ..utils.logging import get_logger @@ -62,6 +66,14 @@ class SemanticNetworkVisualizer: except (KeyError, AttributeError): self.color_scheme = ColorScheme.DEFAULT + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for semantic network visualization. " + "Install with: pip install plotly" + ) + def visualize_network( self, semantic_network: Any, @@ -87,6 +99,7 @@ class SemanticNetworkVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="SemanticNetworkVisualizer", @@ -274,6 +287,7 @@ class SemanticNetworkVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing semantic network node types") # Extract nodes @@ -325,6 +339,7 @@ class SemanticNetworkVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing semantic network edge types") # Extract edges diff --git a/semantica/visualization/temporal_visualizer.py b/semantica/visualization/temporal_visualizer.py index 7c75c7ee..8584fd46 100644 --- a/semantica/visualization/temporal_visualizer.py +++ b/semantica/visualization/temporal_visualizer.py @@ -33,9 +33,14 @@ License: MIT from pathlib import Path from typing import Any, Dict, List, Optional, Union -import plotly.express as px -import plotly.graph_objects as go -from plotly.subplots import make_subplots +try: + import plotly.express as px + import plotly.graph_objects as go + from plotly.subplots import make_subplots +except ImportError: + px = None + go = None + make_subplots = None from ..utils.exceptions import ProcessingError from ..utils.logging import get_logger @@ -66,6 +71,14 @@ class TemporalVisualizer: except (KeyError, AttributeError): self.color_scheme = ColorScheme.DEFAULT + def _check_dependencies(self): + """Check if dependencies are available.""" + if px is None or go is None: + raise ProcessingError( + "Plotly is required for temporal visualization. " + "Install with: pip install plotly" + ) + def visualize_timeline( self, temporal_data: Dict[str, Any], @@ -85,6 +98,7 @@ class TemporalVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() tracking_id = self.progress_tracker.start_tracking( module="visualization", submodule="TemporalVisualizer", @@ -208,6 +222,7 @@ class TemporalVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing temporal patterns") if not patterns: @@ -279,6 +294,7 @@ class TemporalVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing snapshot comparison") timestamps = sorted(snapshots.keys()) @@ -371,6 +387,7 @@ class TemporalVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing version history") # Build tree structure @@ -438,6 +455,7 @@ class TemporalVisualizer: Returns: Visualization figure or None """ + self._check_dependencies() self.logger.info("Visualizing metrics evolution") fig = go.Figure() diff --git a/tests/visualization/reproduce_notebooks.py b/tests/visualization/reproduce_notebooks.py new file mode 100644 index 00000000..4437244b --- /dev/null +++ b/tests/visualization/reproduce_notebooks.py @@ -0,0 +1,291 @@ + +import os +import sys +import unittest +import numpy as np +from datetime import datetime +import logging + +# Add project root to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) + +from semantica.visualization import ( + KGVisualizer, + OntologyVisualizer, + EmbeddingVisualizer, + SemanticNetworkVisualizer, + QualityVisualizer, + AnalyticsVisualizer, + TemporalVisualizer +) +from semantica.kg import GraphBuilder, GraphAnalyzer, TemporalVersionManager +from semantica.ontology import OntologyGenerator +from semantica.embeddings import EmbeddingGenerator + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("reproduce_notebooks") + +def run_introduction_notebook(): + logger.info("Running Introduction Notebook steps...") + + # Step 1: Knowledge Graph Visualization + logger.info("Step 1: Knowledge Graph Visualization") + kg_visualizer = KGVisualizer() + builder = GraphBuilder() + + entities = [ + {"id": "e1", "type": "Organization", "name": "Apple Inc.", "properties": {}}, + {"id": "e2", "type": "Person", "name": "Tim Cook", "properties": {}} + ] + + relationships = [ + {"source": "e2", "target": "e1", "type": "CEO_of", "properties": {}} + ] + + kg = builder.build([{"entities": entities, "relationships": relationships}]) + viz = kg_visualizer.visualize_network(kg, output="interactive") + assert viz is not None, "KG visualization failed" + logger.info("KG Visualization successful") + + # Step 2: Ontology Visualization + logger.info("Step 2: Ontology Visualization") + ontology_visualizer = OntologyVisualizer() + generator = OntologyGenerator(min_occurrences=1) + + ontology = generator.generate_ontology({"entities": entities, "relationships": relationships}) + viz = ontology_visualizer.visualize_hierarchy(ontology, output="interactive") + # Note: verify if None is expected if ontology is simple or empty, but here it should be fine + if viz is None: + logger.warning("Ontology visualization returned None (might be due to empty hierarchy)") + else: + logger.info("Ontology Visualization successful") + + # Step 3: Embedding Visualization + logger.info("Step 3: Embedding Visualization") + embedding_visualizer = EmbeddingVisualizer() + # Mocking EmbeddingGenerator to avoid heavy model loading if possible, + # but let's try to use the real one if it falls back gracefully. + # If it fails, we will catch and use random embeddings. + try: + emb_generator = EmbeddingGenerator() + texts = ["Apple Inc.", "Microsoft Corporation", "Amazon"] + embeddings = emb_generator.generate_embeddings(texts, data_type="text") + except Exception as e: + logger.warning(f"Embedding generation failed: {e}. Using random embeddings.") + embeddings = np.random.rand(3, 384) + + labels = ["Apple", "Microsoft", "Amazon"] + + # Need at least n_neighbors + 1 samples for UMAP usually, but with 3 samples it might warn. + # Let's use PCA or just catch potential UMAP errors if samples are too few. + try: + viz = embedding_visualizer.visualize_2d_projection(embeddings, labels, method="umap") + if viz is None: + # Fallback to pca if umap fails silently or returns None + viz = embedding_visualizer.visualize_2d_projection(embeddings, labels, method="pca") + except Exception as e: + logger.warning(f"UMAP visualization failed: {e}. Trying PCA.") + viz = embedding_visualizer.visualize_2d_projection(embeddings, labels, method="pca") + + assert viz is not None, "Embedding visualization failed" + logger.info("Embedding Visualization successful") + + # Step 4: Semantic Network Visualization + logger.info("Step 4: Semantic Network Visualization") + semantic_network = { + "nodes": [ + {"id": "n1", "label": "Node 1", "type": "Entity"}, + {"id": "n2", "label": "Node 2", "type": "Entity"} + ], + "edges": [ + {"source": "n1", "target": "n2", "label": "related_to"} + ] + } + + sem_viz = SemanticNetworkVisualizer() + viz1 = sem_viz.visualize_network(semantic_network, output="interactive") + viz2 = sem_viz.visualize_node_types(semantic_network, output="interactive") + viz3 = sem_viz.visualize_edge_types(semantic_network, output="interactive") + + assert viz1 is not None, "Semantic Network visualization failed" + assert viz2 is not None, "Node Types visualization failed" + assert viz3 is not None, "Edge Types visualization failed" + logger.info("Semantic Network Visualization successful") + + # Step 5: Advanced Embedding Visualization + logger.info("Step 5: Advanced Embedding Visualization") + text_emb = np.random.rand(50, 128) + image_emb = np.random.rand(50, 128) + audio_emb = np.random.rand(50, 128) + + emb_viz = EmbeddingVisualizer() + viz1 = emb_viz.visualize_multimodal_comparison(text_emb, image_emb, audio_emb, output="interactive") + viz2 = emb_viz.visualize_quality_metrics(text_emb, output="interactive") + + assert viz1 is not None, "Multimodal comparison failed" + assert viz2 is not None, "Quality metrics visualization failed" + logger.info("Advanced Embedding Visualization successful") + + +def run_advanced_notebook(): + logger.info("Running Advanced Notebook steps...") + + # Step 1: Create Sample Knowledge Graph + logger.info("Step 1: Create Sample Knowledge Graph") + builder = GraphBuilder() + + entities = [ + {"id": "e1", "type": "Person", "name": "Alice", "properties": {"age": 30}}, + {"id": "e2", "type": "Person", "name": "Bob", "properties": {"age": 35}}, + {"id": "e3", "type": "Organization", "name": "Tech Corp", "properties": {"founded": 2010}}, + {"id": "e4", "type": "Location", "name": "San Francisco", "properties": {"country": "USA"}}, + ] + + relationships = [ + {"source": "e1", "target": "e2", "type": "knows", "properties": {"since": 2020}}, + {"source": "e1", "target": "e3", "type": "works_for", "properties": {"role": "Engineer"}}, + {"source": "e3", "target": "e4", "type": "located_in", "properties": {}}, + ] + + knowledge_graph = builder.build([{"entities": entities, "relationships": relationships}]) + + # Step 2: Knowledge Graph Visualization + logger.info("Step 2: Knowledge Graph Visualization") + kg_visualizer = KGVisualizer(layout="force", color_scheme="vibrant") + viz = kg_visualizer.visualize_network(knowledge_graph, output="interactive") + assert viz is not None, "KG visualization failed" + logger.info("KG Visualization successful") + + # Step 3: Generate Embeddings and Visualize + logger.info("Step 3: Generate Embeddings and Visualize") + # Use random embeddings to ensure stability + embeddings = np.random.rand(len(entities), 128) + labels = [entity.get("type", "Unknown") for entity in entities] + + embedding_visualizer = EmbeddingVisualizer() + # t-SNE requires more samples typically, use PCA if it fails + try: + viz = embedding_visualizer.visualize_2d_projection(embeddings, labels, method="tsne", output="interactive", file_path=None) + except Exception as e: + logger.warning(f"t-SNE failed (likely too few samples): {e}. Using PCA.") + viz = embedding_visualizer.visualize_2d_projection(embeddings, labels, method="pca", output="interactive", file_path=None) + + assert viz is not None, "Embedding visualization failed" + logger.info("Embedding Visualization successful") + + # Step 4: Quality Metrics Visualization + logger.info("Step 4: Quality Metrics Visualization") + quality_visualizer = QualityVisualizer() + quality_report = { + "overall_score": 0.85, + "consistency_score": 0.90, + "completeness_score": 0.80 + } + viz = quality_visualizer.visualize_dashboard(quality_report, output="interactive") + assert viz is not None, "Quality dashboard visualization failed" + logger.info("Quality Visualization successful") + + # Step 5: Graph Analytics Visualization + logger.info("Step 5: Graph Analytics Visualization") + # Mocking GraphAnalyzer results + centrality_scores = {"e1": 0.5, "e2": 0.3, "e3": 0.8, "e4": 0.4} + # Wrap in expected format + centrality_data = {"centrality": centrality_scores} + + community_dict = {"e1": 0, "e2": 0, "e3": 1, "e4": 1} + # Wrap in expected format + communities_data = {"node_assignments": community_dict} + + analytics_visualizer = AnalyticsVisualizer() + viz1 = analytics_visualizer.visualize_centrality_rankings(centrality_data, title="Node Centrality Scores") + viz2 = analytics_visualizer.visualize_community_structure( + knowledge_graph, + communities_data, + title="Community Detection" + ) + + assert viz1 is not None, "Centrality visualization failed" + assert viz2 is not None, "Communities visualization failed" + logger.info("Analytics Visualization successful") + + # Step 6: Temporal Data Visualization + logger.info("Step 6: Temporal Data Visualization") + temporal_kg = { + "entities": entities, + "relationships": relationships, + "timestamps": { + "e1": [2020, 2021, 2022], + "e2": [2020, 2021], + "e3": [2010, 2015, 2020, 2022], + } + } + + # Generate events from timestamps + events = [] + for entity_id, times in temporal_kg["timestamps"].items(): + for t in times: + events.append({ + "timestamp": t, + "type": "update", + "entity": entity_id, + "label": f"Update {entity_id}" + }) + temporal_kg["events"] = events + + entity_history = { + "e1": [ + {"timestamp": 2020, "properties": {"age": 28}}, + {"timestamp": 2021, "properties": {"age": 29}}, + {"timestamp": 2022, "properties": {"age": 30}}, + ] + } + + temporal_visualizer = TemporalVisualizer() + viz1 = temporal_visualizer.visualize_timeline(temporal_kg, output="interactive") + + timestamps = [str(item["timestamp"]) for item in entity_history["e1"]] + age_values = [item["properties"]["age"] for item in entity_history["e1"]] + metrics_history = {"age": age_values} + viz2 = temporal_visualizer.visualize_metrics_evolution(metrics_history, timestamps, output="interactive") + + assert viz1 is not None, "Timeline visualization failed" + assert viz2 is not None, "Metrics evolution visualization failed" + + # Version Manager part + try: + version_manager = TemporalVersionManager() + v1 = version_manager.create_version(temporal_kg, timestamp="2020-01-01", version_label="v2020") + temporal_kg_v2 = { + "entities": temporal_kg.get("entities", []), + "relationships": temporal_kg.get("relationships", []) + [ + {"source": "e1", "target": "e2", "type": "collaborated_with", "valid_from": "2023-01-01"} + ] + } + v2 = version_manager.create_version(temporal_kg_v2, timestamp="2023-01-01", version_label="v2023") + snapshots = {v1["timestamp"]: v1, v2["timestamp"]: v2} + + viz3 = temporal_visualizer.visualize_snapshot_comparison(snapshots, output="interactive") + + version_history = [ + {"version": v1.get("label"), "timestamp": v1.get("timestamp")}, + {"version": v2.get("label"), "timestamp": v2.get("timestamp")} + ] + viz4 = temporal_visualizer.visualize_version_history(version_history, output="interactive") + + assert viz3 is not None, "Snapshot comparison failed" + assert viz4 is not None, "Version history visualization failed" + except Exception as e: + logger.warning(f"Temporal Version Manager part failed: {e}") + + logger.info("Temporal Visualization successful") + +if __name__ == "__main__": + try: + run_introduction_notebook() + print("-" * 50) + run_advanced_notebook() + print("ALL NOTEBOOK REPRODUCTIONS SUCCESSFUL") + except Exception as e: + logger.error(f"Reproduction failed: {e}") + sys.exit(1) diff --git a/tests/visualization/test_optional_dependencies.py b/tests/visualization/test_optional_dependencies.py new file mode 100644 index 00000000..2f4f93f6 --- /dev/null +++ b/tests/visualization/test_optional_dependencies.py @@ -0,0 +1,149 @@ +import unittest +from unittest.mock import MagicMock, patch +import sys +import numpy as np + +# Helper to mock modules +def mock_module(name): + m = MagicMock() + sys.modules[name] = m + return m + +class TestOptionalDependencies(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Mock heavy/problematic dependencies globally to prevent environment crashes + # We use a dict to save original modules if they exist, but for this test file + # we generally want to run in a controlled "clean" environment. + cls.modules_to_patch = [ + 'sklearn', 'sklearn.decomposition', 'sklearn.manifold', + 'scipy', 'scipy.optimize', + 'matplotlib', 'matplotlib.pyplot', 'matplotlib.patches', + 'plotly', 'plotly.express', 'plotly.graph_objects', 'plotly.subplots', + 'networkx', 'seaborn' + ] + + cls.original_modules = {} + for mod in cls.modules_to_patch: + if mod in sys.modules: + cls.original_modules[mod] = sys.modules[mod] + sys.modules[mod] = MagicMock() + + @classmethod + def tearDownClass(cls): + # Restore original modules + for mod in cls.modules_to_patch: + if mod in cls.original_modules: + sys.modules[mod] = cls.original_modules[mod] + else: + del sys.modules[mod] + + def setUp(self): + # Clear cached visualization modules to ensure fresh imports + self.viz_modules = [ + 'semantica.visualization.embedding_visualizer', + 'semantica.visualization.ontology_visualizer', + 'semantica.visualization.kg_visualizer', + 'semantica.visualization.utils.export_formats' + ] + for mod in self.viz_modules: + if mod in sys.modules: + del sys.modules[mod] + + def test_embedding_visualizer_without_umap(self): + """Test EmbeddingVisualizer behavior when umap is missing.""" + # Ensure umap is missing + with patch.dict(sys.modules, {'umap': None}): + from semantica.visualization.embedding_visualizer import EmbeddingVisualizer + + # Setup PCA mock to verify fallback + mock_pca_class = sys.modules['sklearn.decomposition'].PCA + mock_pca_instance = mock_pca_class.return_value + # Configure fit_transform to return correct shape (n_samples, 2) + mock_pca_instance.fit_transform.return_value = np.zeros((4, 2)) + + viz = EmbeddingVisualizer() + # Use numpy array! + embeddings = np.array([[0, 1, 2], [1, 0, 3], [0, 0, 0], [1, 1, 1]]) + + # Should fallback to PCA when method="umap" is used but umap is None + # The code logs a warning and uses PCA + viz.visualize_2d_projection(embeddings, method="umap") + + # Verify PCA was called + mock_pca_class.assert_called() + + def test_ontology_visualizer_without_graphviz(self): + """Test OntologyVisualizer behavior when graphviz is missing.""" + # Ensure graphviz is missing + with patch.dict(sys.modules, {'graphviz': None}): + from semantica.visualization.ontology_visualizer import OntologyVisualizer, ProcessingError + + viz = OntologyVisualizer() + ontology = { + "classes": [ + {"name": "A", "label": "A"}, + {"name": "B", "label": "B", "parent": "A"} + ] + } + + with self.assertRaises(ProcessingError) as cm: + viz.visualize_hierarchy(ontology, output="dot", file_path="test.dot") + + self.assertIn("Graphviz is required for DOT export", str(cm.exception)) + + def test_analytics_visualizer_without_plotly(self): + """Test AnalyticsVisualizer behavior when plotly is missing.""" + with patch.dict(sys.modules, {'plotly': None, 'plotly.express': None, 'plotly.graph_objects': None}): + from semantica.visualization.analytics_visualizer import AnalyticsVisualizer, ProcessingError + + # Need to ensure numpy is available for init (it's imported at top level) + # But we are testing plotly missing. + + viz = AnalyticsVisualizer() + + with self.assertRaises(ProcessingError) as cm: + viz.visualize_centrality_rankings({"node1": 1.0}) + + self.assertIn("Plotly is required", str(cm.exception)) + + def test_quality_visualizer_without_plotly(self): + """Test QualityVisualizer behavior when plotly is missing.""" + with patch.dict(sys.modules, {'plotly': None, 'plotly.express': None, 'plotly.graph_objects': None}): + from semantica.visualization.quality_visualizer import QualityVisualizer, ProcessingError + + viz = QualityVisualizer() + + with self.assertRaises(ProcessingError) as cm: + viz.visualize_dashboard({}) + + self.assertIn("Plotly is required", str(cm.exception)) + + def test_semantic_network_visualizer_without_plotly(self): + """Test SemanticNetworkVisualizer behavior when plotly is missing.""" + with patch.dict(sys.modules, {'plotly': None, 'plotly.express': None, 'plotly.graph_objects': None}): + from semantica.visualization.semantic_network_visualizer import SemanticNetworkVisualizer, ProcessingError + + viz = SemanticNetworkVisualizer() + + with self.assertRaises(ProcessingError) as cm: + viz.visualize_network({}) + + self.assertIn("Plotly is required", str(cm.exception)) + + def test_temporal_visualizer_without_plotly(self): + """Test TemporalVisualizer behavior when plotly is missing.""" + with patch.dict(sys.modules, {'plotly': None, 'plotly.express': None, 'plotly.graph_objects': None}): + from semantica.visualization.temporal_visualizer import TemporalVisualizer, ProcessingError + + viz = TemporalVisualizer() + + with self.assertRaises(ProcessingError) as cm: + viz.visualize_timeline({"events": []}) + + self.assertIn("Plotly is required", str(cm.exception)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/visualization/test_visualization_comprehensive.py b/tests/visualization/test_visualization_comprehensive.py new file mode 100644 index 00000000..c40027ad --- /dev/null +++ b/tests/visualization/test_visualization_comprehensive.py @@ -0,0 +1,252 @@ +import unittest +from unittest.mock import MagicMock, patch +import sys +import numpy as np +from pathlib import Path + +# Mock heavy libraries before importing visualization modules +sys.modules['matplotlib'] = MagicMock() +sys.modules['matplotlib.pyplot'] = MagicMock() +sys.modules['matplotlib.colors'] = MagicMock() +sys.modules['matplotlib.patches'] = MagicMock() +sys.modules['plotly'] = MagicMock() +sys.modules['plotly.express'] = MagicMock() +sys.modules['plotly.graph_objects'] = MagicMock() +sys.modules['plotly.subplots'] = MagicMock() +sys.modules['seaborn'] = MagicMock() +sys.modules['umap'] = MagicMock() +sys.modules['sklearn'] = MagicMock() +sys.modules['sklearn.decomposition'] = MagicMock() +sys.modules['sklearn.manifold'] = MagicMock() +sys.modules['networkx'] = MagicMock() +sys.modules['graphviz'] = MagicMock() + +# Import visualizers +from semantica.visualization.kg_visualizer import KGVisualizer +from semantica.visualization.ontology_visualizer import OntologyVisualizer +from semantica.visualization.embedding_visualizer import EmbeddingVisualizer +from semantica.visualization.semantic_network_visualizer import SemanticNetworkVisualizer +from semantica.visualization.quality_visualizer import QualityVisualizer +from semantica.visualization.analytics_visualizer import AnalyticsVisualizer +from semantica.visualization.temporal_visualizer import TemporalVisualizer +from semantica.visualization.utils.color_schemes import ColorScheme + +class TestVisualizationComprehensive(unittest.TestCase): + + def setUp(self): + self.mock_logger = MagicMock() + self.mock_tracker = MagicMock() + + # Patch dependencies for all visualizers + self.patchers = [ + patch('semantica.visualization.kg_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.kg_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.ontology_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.ontology_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.embedding_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.embedding_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.semantic_network_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.semantic_network_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.quality_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.quality_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.analytics_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.analytics_visualizer.get_progress_tracker', return_value=self.mock_tracker), + patch('semantica.visualization.temporal_visualizer.get_logger', return_value=self.mock_logger), + patch('semantica.visualization.temporal_visualizer.get_progress_tracker', return_value=self.mock_tracker), + # Mock Layouts + patch('semantica.visualization.kg_visualizer.ForceDirectedLayout', MagicMock()), + patch('semantica.visualization.kg_visualizer.HierarchicalLayout', MagicMock()), + patch('semantica.visualization.kg_visualizer.CircularLayout', MagicMock()), + patch('semantica.visualization.ontology_visualizer.HierarchicalLayout', MagicMock()), + patch('semantica.visualization.semantic_network_visualizer.ForceDirectedLayout', MagicMock()), + ] + + for p in self.patchers: + p.start() + + # Reset plotly mocks + import plotly.graph_objects as go + import plotly.express as px + go.Figure.reset_mock() + px.bar.reset_mock() + px.scatter.reset_mock() + + def tearDown(self): + for p in self.patchers: + p.stop() + + # --- KGVisualizer Tests --- + def test_kg_visualizer(self): + viz = KGVisualizer() + graph = { + "entities": [{"id": "e1", "label": "E1", "type": "T1"}, {"id": "e2", "label": "E2", "type": "T2"}], + "relationships": [{"source": "e1", "target": "e2", "type": "R1"}] + } + + # Test visualize_network + viz.visualize_network(graph) + + # Test visualize_communities + communities = {"node_assignments": {"e1": 0, "e2": 1}, "num_communities": 2} + viz.visualize_communities(graph, communities) + + # Test visualize_centrality + centrality = {"centrality": {"e1": 0.5, "e2": 0.3}} + viz.visualize_centrality(graph, centrality) + + # Test visualize_entity_types + viz.visualize_entity_types(graph) + + # Test visualize_relationship_matrix + viz.visualize_relationship_matrix(graph) + + # --- OntologyVisualizer Tests --- + def test_ontology_visualizer(self): + viz = OntologyVisualizer() + ontology = { + "classes": [ + {"name": "C1", "label": "Class 1", "parent": None}, + {"name": "C2", "label": "Class 2", "parent": "C1"} + ], + "properties": [ + {"name": "P1", "label": "Prop 1", "domain": "C1", "range": "C2"} + ] + } + + # Test visualize_hierarchy + viz.visualize_hierarchy(ontology) + + # Test visualize_properties + viz.visualize_properties(ontology) + + # Test visualize_structure + viz.visualize_structure(ontology) + + # Test visualize_class_property_matrix + viz.visualize_class_property_matrix(ontology) + + # Test visualize_metrics + viz.visualize_metrics(ontology) + + # Test visualize_semantic_model (mocking extract classes) + semantic_model = {"nodes": [{"id": "n1", "type": "T1"}], "edges": []} + viz.visualize_semantic_model(semantic_model) + + # --- SemanticNetworkVisualizer Tests --- + def test_semantic_network_visualizer(self): + viz = SemanticNetworkVisualizer() + semantic_network = { + "nodes": [{"id": "n1", "label": "N1", "type": "T1"}], + "edges": [{"source": "n1", "target": "n1", "label": "R1"}] + } + + # Test visualize_network + with patch('semantica.visualization.kg_visualizer.KGVisualizer') as MockKG: + viz.visualize_network(semantic_network) + MockKG.return_value.visualize_network.assert_called() + + # Test visualize_node_types + viz.visualize_node_types(semantic_network) + + # Test visualize_edge_types + viz.visualize_edge_types(semantic_network) + + # --- QualityVisualizer Tests --- + def test_quality_visualizer(self): + viz = QualityVisualizer() + + # Test visualize_dashboard + report = {"overall_score": 0.8, "consistency_score": 0.9, "completeness_score": 0.7} + viz.visualize_dashboard(report) + + # Test visualize_score_distribution + scores = [0.1, 0.5, 0.9] + viz.visualize_score_distribution(scores) + + # Test visualize_issues + report_issues = {"issues": [{"type": "error", "severity": "high"}]} + viz.visualize_issues(report_issues) + + # Test visualize_completeness_metrics + metrics = {"entity_completeness": 0.8} + viz.visualize_completeness_metrics(metrics) + + # Test visualize_consistency_heatmap + consistency = {"consistency_matrix": [[1.0]], "labels": ["C1"]} + viz.visualize_consistency_heatmap(consistency) + + # --- AnalyticsVisualizer Tests --- + def test_analytics_visualizer(self): + viz = AnalyticsVisualizer() + graph = {"entities": [], "relationships": []} + + # Test visualize_centrality_rankings + centrality = {"rankings": [{"node": "n1", "score": 0.9}]} + viz.visualize_centrality_rankings(centrality) + + # Test visualize_community_structure + communities = {"node_assignments": {}} + with patch('semantica.visualization.kg_visualizer.KGVisualizer') as MockKG: + viz.visualize_community_structure(graph, communities) + + # Test visualize_connectivity + connectivity = {"is_connected": True, "num_components": 1, "component_sizes": [10]} + viz.visualize_connectivity(connectivity) + + # Test visualize_degree_distribution + viz.visualize_degree_distribution(graph) + + # Test visualize_metrics_dashboard + metrics = {"num_nodes": 10, "num_edges": 20, "density": 0.1} + viz.visualize_metrics_dashboard(metrics) + + # Test visualize_centrality_comparison + results = {"degree": {"rankings": [{"node": "n1", "score": 0.9}]}} + viz.visualize_centrality_comparison(results) + + # --- TemporalVisualizer Tests --- + def test_temporal_visualizer(self): + viz = TemporalVisualizer() + + # Test visualize_timeline + temporal_data = {"events": [{"timestamp": "2023-01-01", "type": "create", "label": "E1"}], "timestamps": ["2023-01-01"]} + viz.visualize_timeline(temporal_data) + + # Test visualize_temporal_patterns + patterns = [{"pattern_type": "trend", "start_time": "2023", "end_time": "2024", "entities": ["e1"]}] + viz.visualize_temporal_patterns(patterns) + + # Test visualize_snapshot_comparison + snapshots = {"2023": {"entities": ["e1"], "relationships": []}} + viz.visualize_snapshot_comparison(snapshots) + + # Test visualize_version_history + history = [{"version": "v1", "date": "2023-01-01"}] + viz.visualize_version_history(history) + + # Test visualize_metrics_evolution + metrics_history = {"nodes": [10, 20]} + timestamps = ["2023", "2024"] + viz.visualize_metrics_evolution(metrics_history, timestamps) + + # --- EmbeddingVisualizer Tests --- + def test_embedding_visualizer(self): + viz = EmbeddingVisualizer() + embeddings = np.random.rand(10, 10) + + # Test visualize_2d_projection (mock UMAP/PCA) + with patch('semantica.visualization.embedding_visualizer.umap.UMAP') as MockUMAP: + MockUMAP.return_value.fit_transform.return_value = np.random.rand(10, 2) + viz.visualize_2d_projection(embeddings) + + # Test visualize_similarity_heatmap + viz.visualize_similarity_heatmap(embeddings[:5]) # smaller for heatmap + + # Test visualize_clustering + clusters = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + with patch('semantica.visualization.embedding_visualizer.umap.UMAP') as MockUMAP: + MockUMAP.return_value.fit_transform.return_value = np.random.rand(10, 2) + viz.visualize_clustering(embeddings, clusters) + +if __name__ == '__main__': + unittest.main()