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()