"""Unified SVD component labels and automatic flip direction computation. This module provides a single source of truth for SVD component labels, deriving them from SVD_THEMES in explorer.py. It also computes flip directions automatically based on party centroids. """ import logging from typing import Dict, List, Optional, Tuple _logger = logging.getLogger(__name__) # Canonical party sets for orientation # Right-wing parties that should appear on the right side of axes RIGHT_PARTIES = { "PVV", "VVD", "FVD", "BBB", "JA21", "Nieuw Sociaal Contract", "SGP", "CDA", "ChristenUnie", } # Left-wing parties that should appear on the left side of axes LEFT_PARTIES = { "SP", "PvdA", "GL", "GroenLinks", "GroenLinks-PvdA", "DENK", "PvdD", "Volt", } # Cache for SVD_THEMES to avoid repeated imports _svd_themes_cache: Optional[Dict[int, Dict[str, str]]] = None def _get_svd_themes() -> Dict[int, Dict[str, str]]: """Import SVD_THEMES from explorer.py. Returns: Dict mapping component number to theme dict with keys: - label: Short label for the component - explanation: Detailed explanation - positive_pole: Description of positive pole - negative_pole: Description of negative pole - flip: Whether to flip the axis """ global _svd_themes_cache if _svd_themes_cache is not None: return _svd_themes_cache try: # Import SVD_THEMES from explorer at runtime to avoid circular imports # explorer.py now exports SVD_THEMES at module level import explorer _svd_themes_cache = explorer.SVD_THEMES return _svd_themes_cache except ImportError as e: _logger.warning("Could not import explorer.SVD_THEMES: %s", e) return {} except Exception as e: _logger.exception("Failed to load SVD_THEMES from explorer.py: %s", e) return {} def get_svd_label(component: int) -> str: """Get short label for SVD component. Args: component: SVD component number (1-indexed) Returns: Short label string (e.g., 'EU-integratie–Nationalisme') Raises: ValueError: If component < 1 """ if component < 1: raise ValueError(f"Component must be >= 1, got {component}") themes = _get_svd_themes() if component in themes: return themes[component].get("label", f"As {component}") # Fallback labels for components 1-3 (most commonly used) fallback_labels = { 1: "EU-integratie–Nationalisme", 2: "Populistisch–Institutioneel", 3: "Verzorgingsstaat–Marktwerking", } return fallback_labels.get(component, f"As {component}") def get_svd_theme(component: int) -> Dict[str, str]: """Get full theme dict for SVD component. Args: component: SVD component number (1-indexed) Returns: Dict with keys: label, explanation, positive_pole, negative_pole, flip """ if component < 1: raise ValueError(f"Component must be >= 1, got {component}") themes = _get_svd_themes() if component in themes: return themes[component] # Return minimal fallback return { "label": get_svd_label(component), "explanation": "", "positive_pole": "", "negative_pole": "", "flip": False, } def compute_flip_direction( component: int, party_scores: Dict[str, List[float]] ) -> bool: """Compute flip direction so right parties appear on the right side. Args: component: SVD component number (1-indexed) party_scores: Dict mapping party name to list of scores per component (party_scores[party][0] is score for component 1, etc.) Returns: True if axis should be flipped so right parties are on right. False otherwise. """ if component < 1: return False idx = component - 1 # Convert to 0-indexed right_scores = [] left_scores = [] for party, scores in party_scores.items(): if len(scores) <= idx: continue score = scores[idx] if party in RIGHT_PARTIES: right_scores.append(score) elif party in LEFT_PARTIES: left_scores.append(score) if not right_scores or not left_scores: return False # Default: no flip if insufficient data right_mean = sum(right_scores) / len(right_scores) left_mean = sum(left_scores) / len(left_scores) # Flip if right parties have lower mean (they're on the left) return right_mean < left_mean def get_fallback_labels() -> Tuple[str, str]: """Get fallback labels for x and y axes (components 1 and 2). Returns: Tuple of (x_label, y_label) """ return (get_svd_label(1), get_svd_label(2))