Skip to content

Plot Services

Application Layer - Plot Services Package.

This package provides services for managing plot generation, configuration loading, and factory pattern implementation for chart creation across all use cases.

Modules

Architecture

The plot services follow the Strategy Pattern and Factory Pattern to provide flexible and extensible chart generation capabilities.

Package Overview

Application Layer - Plot Services.

Provides services for plot generation, configuration loading, and strategy creation with multi-layer caching support.

Classes:

Name Description
PlotService

Orchestrates plot generation pipeline with caching

PlotFactory

Factory for creating plot strategies

PlotConfigLoader

Loads and caches YAML configurations


PlotService

PlotService

PlotService()

Central service for plot generation with caching.

This service implements the Facade pattern, providing a simple interface for plot generation while managing complex operations behind the scenes: - Load YAML configuration - Create strategy via factory - Check multi-layer cache - Generate plot - Cache results

Attributes:

Name Type Description
config_loader PlotConfigLoader

Configuration loader instance.

factory PlotFactory

Plot factory instance.

cache_manager GraphCacheManager

Cache manager instance.

Examples:

>>> service = PlotService()
>>> fig = service.generate_plot(
...     "UC-2.1",
...     df,
...     filters={"uc-2-1-range-slider": [10, 50]}
... )

Initialize plot service with dependencies.

Source code in src/application/plot_services/plot_service.py
def __init__(self):
    """Initialize plot service with dependencies."""
    self.config_loader = PlotConfigLoader()
    self.factory = PlotFactory()
    self.cache_manager = GraphCacheManager()
    logger.info("PlotService initialized")

Functions

generate_plot
generate_plot(use_case_id: str, data: DataFrame, filters: Optional[Dict[str, Any]] = None, customizations: Optional[Any] = None, force_refresh: bool = False) -> go.Figure

Generate plot for given use case with caching.

Pipeline: 1. Load configuration from YAML 2. Generate cache keys 3. Check graph cache 4. If miss: Create strategy and generate plot 5. Cache result 6. Return figure

Parameters:

Name Type Description Default
use_case_id str

Use case identifier (e.g., "UC-2.1").

required
data DataFrame

Input data.

required
filters Optional[Dict[str, Any]]

Filters to apply (e.g., {"uc-2-1-range-slider": [10, 50]}).

None
customizations Optional[Any]

Additional customizations (future feature).

None
force_refresh bool

Force cache refresh.

False

Returns:

Type Description
Figure

Generated Plotly figure.

Raises:

Type Description
ValueError

If data validation fails.

FileNotFoundError

If configuration file not found.

Examples:

>>> service = PlotService()
>>> fig = service.generate_plot("UC-2.1", df)
>>> fig.layout.height
500
Source code in src/application/plot_services/plot_service.py
def generate_plot(
    self,
    use_case_id: str,
    data: pd.DataFrame,
    filters: Optional[Dict[str, Any]] = None,
    customizations: Optional[Any] = None,
    force_refresh: bool = False,
) -> go.Figure:
    """
    Generate plot for given use case with caching.

    Pipeline:
    1. Load configuration from YAML
    2. Generate cache keys
    3. Check graph cache
    4. If miss: Create strategy and generate plot
    5. Cache result
    6. Return figure

    Parameters
    ----------
    use_case_id : str
        Use case identifier (e.g., "UC-2.1").
    data : pd.DataFrame
        Input data.
    filters : Optional[Dict[str, Any]], default=None
        Filters to apply (e.g., {"uc-2-1-range-slider": [10, 50]}).
    customizations : Optional[Any], default=None
        Additional customizations (future feature).
    force_refresh : bool, default=False
        Force cache refresh.

    Returns
    -------
    go.Figure
        Generated Plotly figure.

    Raises
    ------
    ValueError
        If data validation fails.
    FileNotFoundError
        If configuration file not found.

    Examples
    --------
    >>> service = PlotService()
    >>> fig = service.generate_plot("UC-2.1", df)
    >>> fig.layout.height
    500
    """
    start_time = time.time()
    logger.info(f"Generating plot for {use_case_id}")

    # 1. Load configuration (force reload if force_refresh is True)
    config = self.config_loader.load_config(use_case_id, force_reload=force_refresh)
    perf_config = config.get("performance", {})
    cache_config = perf_config.get("cache", {})

    # 2. Generate cache keys
    data_hash = self._generate_data_hash(data)
    filters_hash = self._generate_filters_hash(filters) if filters else "no_filters"

    # Generate cache key (needed for both checking and storing)
    graph_cache_key = self._get_cache_key(config, "graph", data_hash, filters_hash)

    # 3. Check if caching is enabled
    if cache_config.get("enabled", True) and not force_refresh:
        # Check graph cache (fastest)
        cached_figure = self.cache_manager.get_cached_graph(graph_cache_key)
        if cached_figure:
            cache_time = time.time() - start_time
            logger.info(
                f"Cache HIT (graph) for {use_case_id}: " f"{cache_time:.3f}s"
            )
            return cached_figure

        logger.debug(f"Cache MISS (graph) for {use_case_id}")

    # 4. Create strategy via factory
    strategy = self.factory.create_strategy(config)

    # 5. Generate plot (includes validation, processing, filtering)
    try:
        figure = strategy.generate_plot(
            data, filters=filters, customizations=customizations
        )

        # 6. Cache the figure
        if cache_config.get("enabled", True):
            ttl = self._get_cache_ttl(cache_config, "graph")
            # Note: ttl is not used by current GraphCacheManager
            # (it uses global TTL), but we pass metadata
            metadata = {"use_case_id": use_case_id, "filters": filters, "ttl": ttl}
            self.cache_manager.cache_graph(
                graph_cache_key, figure, metadata=metadata
            )
            logger.debug(f"Cached graph for {use_case_id} (TTL: {ttl}s)")

        total_time = time.time() - start_time
        logger.info(f"Plot generated for {use_case_id}: {total_time:.3f}s")

        return figure

    except Exception as e:
        logger.error(f"Error generating plot for {use_case_id}: {e}", exc_info=True)
        raise
clear_cache
clear_cache(use_case_id: Optional[str] = None) -> None

Clear cache for specific use case or all.

Parameters:

Name Type Description Default
use_case_id Optional[str]

Use case ID to clear. If None, clears all.

None
Source code in src/application/plot_services/plot_service.py
def clear_cache(self, use_case_id: Optional[str] = None) -> None:
    """
    Clear cache for specific use case or all.

    Parameters
    ----------
    use_case_id : Optional[str], default=None
        Use case ID to clear. If None, clears all.
    """
    if use_case_id:
        # Clear specific use case (would need pattern matching)
        logger.info(f"Clearing cache for {use_case_id}")
    else:
        self.cache_manager.clear()
        logger.info("Cleared all plot caches")

PlotFactory

PlotFactory

PlotFactory()

Factory for creating plot strategies.

Maps strategy names (from YAML config) to concrete strategy classes.

Attributes:

Name Type Description
_strategy_registry Dict[str, type]

Registry mapping strategy names to classes

Initialize plot factory with strategy registry.

Source code in src/application/plot_services/plot_factory.py
def __init__(self):
    """Initialize plot factory with strategy registry."""
    self._strategy_registry: Dict[str, type] = {
        "BarChartStrategy": BarChartStrategy,
        "BoxScatterStrategy": BoxScatterStrategy,
        "ChordStrategy": ChordStrategy,
        "CorrelogramStrategy": CorrelogramStrategy,
        "DensityPlotStrategy": DensityPlotStrategy,
        "DotPlotStrategy": DotPlotStrategy,
        "FacetedHeatmapStrategy": FacetedHeatmapStrategy,
        "FrozensetStrategy": FrozensetStrategy,
        "HeatmapScoredStrategy": HeatmapScoredStrategy,
        "HeatmapStrategy": HeatmapStrategy,
        "HierarchicalClusteringStrategy": HierarchicalClusteringStrategy,
        "NetworkStrategy": NetworkStrategy,
        "PCAStrategy": PCAStrategy,
        "RadarChartStrategy": RadarChartStrategy,
        "SankeyStrategy": SankeyStrategy,
        "StackedBarChartStrategy": StackedBarChartStrategy,
        "SunburstStrategy": SunburstStrategy,
        "TreemapStrategy": TreemapStrategy,
        "UpSetStrategy": UpSetStrategy,
    }
    logger.info(
        f"PlotFactory initialized with "
        f"{len(self._strategy_registry)} strategies"
    )

Functions

create_strategy
create_strategy(config: Dict[str, Any]) -> BasePlotStrategy

Create strategy instance from configuration.

Parameters:

Name Type Description Default
config Dict[str, Any]

Complete configuration dictionary (must contain visualization.strategy: str)

required

Returns:

Type Description
BasePlotStrategy

Instantiated strategy

Raises:

Type Description
ValueError

If strategy name not found in registry

KeyError

If configuration missing required keys

Source code in src/application/plot_services/plot_factory.py
def create_strategy(self, config: Dict[str, Any]) -> BasePlotStrategy:
    """
    Create strategy instance from configuration.

    Parameters
    ----------
    config : Dict[str, Any]
        Complete configuration dictionary (must contain
        visualization.strategy: str)

    Returns
    -------
    BasePlotStrategy
        Instantiated strategy

    Raises
    ------
    ValueError
        If strategy name not found in registry
    KeyError
        If configuration missing required keys
    """
    # Get strategy name from config
    viz_config = config.get("visualization", {})
    strategy_name = viz_config.get("strategy")

    if not strategy_name:
        raise ValueError("Configuration missing 'visualization.strategy' key")

    # Lookup strategy class
    strategy_class = self._strategy_registry.get(strategy_name)

    if not strategy_class:
        available = list(self._strategy_registry.keys())
        raise ValueError(
            f"Unknown strategy: '{strategy_name}'. "
            f"Available strategies: {available}"
        )

    # Instantiate and return
    strategy = strategy_class(config)

    use_case_id = config.get("metadata", {}).get("use_case_id", "unknown")
    logger.info(f"Created {strategy_name} for {use_case_id}")

    return strategy
register_strategy
register_strategy(name: str, strategy_class: type) -> None

Register new strategy class.

Allows dynamic registration of strategies at runtime.

Parameters:

Name Type Description Default
name str

Strategy name (used in YAML config)

required
strategy_class type

Strategy class (must inherit from BasePlotStrategy)

required
Source code in src/application/plot_services/plot_factory.py
def register_strategy(self, name: str, strategy_class: type) -> None:
    """
    Register new strategy class.

    Allows dynamic registration of strategies at runtime.

    Parameters
    ----------
    name : str
        Strategy name (used in YAML config)
    strategy_class : type
        Strategy class (must inherit from BasePlotStrategy)
    """
    if not issubclass(strategy_class, BasePlotStrategy):
        raise TypeError(f"{strategy_class} must inherit from BasePlotStrategy")

    self._strategy_registry[name] = strategy_class
    logger.info(f"Registered strategy: {name}")
get_available_strategies
get_available_strategies() -> list

Get list of available strategy names.

Returns:

Type Description
list

List of registered strategy names.

Source code in src/application/plot_services/plot_factory.py
def get_available_strategies(self) -> list:
    """
    Get list of available strategy names.

    Returns
    -------
    list
        List of registered strategy names.
    """
    return list(self._strategy_registry.keys())

PlotConfigLoader

PlotConfigLoader

PlotConfigLoader(config_dir: str = 'src/infrastructure/plot_configs')

Configuration loader for plot use cases.

Loads YAML configuration files and caches them in memory for performance.

Attributes:

Name Type Description
config_dir Path

Base directory for configurations

_cache Dict[str, Dict[str, Any]]

Configuration cache

Initialize configuration loader.

Parameters:

Name Type Description Default
config_dir str

Base directory for plot configurations.

"src/infrastructure/plot_configs"
Source code in src/application/plot_services/plot_config_loader.py
def __init__(self, config_dir: str = "src/infrastructure/plot_configs"):
    """
    Initialize configuration loader.

    Parameters
    ----------
    config_dir : str, default="src/infrastructure/plot_configs"
        Base directory for plot configurations.
    """
    self.config_dir = Path(config_dir)
    self._cache: Dict[str, Dict[str, Any]] = {}
    logger.info(f"PlotConfigLoader initialized: {self.config_dir}")

Functions

load_config
load_config(use_case_id: str, force_reload: bool = False) -> Dict[str, Any]

Load configuration for given use case.

Configuration file naming convention: - UC-2.1 -> module2/uc_2_1_config.yaml - UC-3.4 -> module3/uc_3_4_config.yaml

Parameters:

Name Type Description Default
use_case_id str

Use case identifier (e.g., "UC-2.1")

required
force_reload bool

Force reload from file (bypass cache)

False

Returns:

Type Description
Dict[str, Any]

Configuration dictionary

Raises:

Type Description
FileNotFoundError

If configuration file not found

ValueError

If YAML is invalid

Source code in src/application/plot_services/plot_config_loader.py
def load_config(
    self, use_case_id: str, force_reload: bool = False
) -> Dict[str, Any]:
    """
    Load configuration for given use case.

    Configuration file naming convention:
    - UC-2.1 -> module2/uc_2_1_config.yaml
    - UC-3.4 -> module3/uc_3_4_config.yaml

    Parameters
    ----------
    use_case_id : str
        Use case identifier (e.g., "UC-2.1")
    force_reload : bool, default=False
        Force reload from file (bypass cache)

    Returns
    -------
    Dict[str, Any]
        Configuration dictionary

    Raises
    ------
    FileNotFoundError
        If configuration file not found
    ValueError
        If YAML is invalid
    """
    # Check cache
    if use_case_id in self._cache and not force_reload:
        logger.debug(f"Cache HIT for config: {use_case_id}")
        return self._cache[use_case_id]

    logger.debug(f"Cache MISS for config: {use_case_id}")

    # Parse use case ID to get module and file name
    # UC-2.1 -> module2, uc_2_1_config.yaml
    # UC-3.2 -> module3, uc_3_2_config.yaml

    # Remove 'UC-' prefix and split by '.'
    if not use_case_id.startswith("UC-"):
        raise ValueError(
            f"Invalid use case ID format: {use_case_id}. "
            f"Expected format: UC-X.Y"
        )

    # Extract module and subcase numbers
    # "UC-2.1" -> "2.1" -> ["2", "1"]
    id_part = use_case_id.replace("UC-", "")
    parts = id_part.split(".")

    if len(parts) != 2:
        raise ValueError(
            f"Invalid use case ID format: {use_case_id}. "
            f"Expected format: UC-X.Y"
        )

    module_num = parts[0]  # "2"
    sub_case = parts[1]  # "1"

    # Build file path
    # module2/uc_2_1_config.yaml
    module_dir = f"module{module_num}"
    filename = f"uc_{module_num}_{sub_case}_config.yaml"
    config_path = self.config_dir / module_dir / filename

    logger.debug(f"Looking for config: {use_case_id} -> {config_path}")

    # Load YAML
    if not config_path.exists():
        raise FileNotFoundError(f"Configuration file not found: {config_path}")

    try:
        with open(config_path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)

        # Cache and return
        self._cache[use_case_id] = config
        logger.info(f"Loaded configuration for {use_case_id}")

        return config

    except yaml.YAMLError as e:
        raise ValueError(f"Invalid YAML in {config_path}: {e}")
clear_cache
clear_cache() -> None

Clear configuration cache.

Source code in src/application/plot_services/plot_config_loader.py
def clear_cache(self) -> None:
    """Clear configuration cache."""
    self._cache.clear()
    logger.info("Configuration cache cleared")
get_cached_keys
get_cached_keys() -> list

Get list of cached use case IDs.

Returns:

Type Description
list

List of cached use case IDs.

Source code in src/application/plot_services/plot_config_loader.py
def get_cached_keys(self) -> list:
    """
    Get list of cached use case IDs.

    Returns
    -------
    list
        List of cached use case IDs.
    """
    return list(self._cache.keys())