Source code for epyr.baseline.interactive

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interactive region selection for baseline correction.

This module provides matplotlib-based interactive widgets for selecting
baseline regions in Jupyter notebooks and desktop environments.
"""

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import RectangleSelector, SpanSelector

from ..logging_config import get_logger

logger = get_logger(__name__)


def _get_widget_props_param():
    """Get the correct parameter name for matplotlib
    widget properties based on version."""
    # Matplotlib 3.5+ uses 'props' instead of 'rectprops'
    version = tuple(map(int, matplotlib.__version__.split(".")[:2]))
    if version >= (3, 5):
        return "props"
    else:
        return "rectprops"


[docs] class RegionSelector: """ Interactive region selector for baseline correction. This class provides matplotlib-based interactive region selection for both 1D and 2D EPR data. It handles matplotlib version compatibility and provides multiple methods to close selection windows. """
[docs] def __init__(self): self.regions = [] self.current_selector = None self.fig = None self.ax = None self.selection_done = False
def _on_select_1d(self, xmin, xmax): """Callback for 1D region selection.""" self.regions.append((xmin, xmax)) logger.info(f"Selected region: {xmin:.2f} - {xmax:.2f}") def _on_key_press(self, event): """Handle key press events.""" if event.key == "enter" or event.key == "escape": self.selection_done = True plt.close(self.fig) logger.info("✅ Region selection completed!")
[docs] def finish_selection(self): """Manually finish selection and close plot.""" self.selection_done = True if self.fig: plt.close(self.fig) logger.info("✅ Region selection completed!")
def _on_select_2d(self, eclick, erelease): """Callback for 2D region selection.""" x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata region = ((min(x1, x2), max(x1, x2)), (min(y1, y2), max(y1, y2))) self.regions.append(region) xr = region[0] yr = region[1] logger.info( f"Selected region:" f" x={xr[0]:.2f}-{xr[1]:.2f}," f" y={yr[0]:.2f}-{yr[1]:.2f}" )
[docs] def select_regions_1d( self, x, y, title="Select regions to EXCLUDE from baseline fitting" ): """ Interactive selection of 1D regions. Args: x: X-axis data y: Y-axis data title: Plot title with instructions Returns: list: Selected regions as [(x1, x2), ...] """ self.regions = [] self.fig, self.ax = plt.subplots(figsize=(12, 6)) self.ax.plot(x, y, "b-", alpha=0.7) self.ax.set_title( f"{title}\nClick and drag to select" " regions.\nPress ENTER or ESC when" " done, or run" " selector.finish_selection()" ) self.ax.grid(True, alpha=0.3) # Add keyboard event handling self.fig.canvas.mpl_connect("key_press_event", self._on_key_press) # Create span selector - handle matplotlib version compatibility props_param = _get_widget_props_param() selector_kwargs = { "useblit": True, props_param: dict(alpha=0.3, facecolor="red"), } self.current_selector = SpanSelector( self.ax, self._on_select_1d, "horizontal", **selector_kwargs ) plt.show() return self.regions
[docs] def select_regions_2d( self, x, y, z, title="Select regions to EXCLUDE from baseline fitting" ): """ Interactive selection of 2D regions. Args: x: X-axis coordinates (1D array or meshgrid) y: Y-axis coordinates (1D array or meshgrid) z: 2D data array title: Plot title with instructions Returns: list: Selected regions as [((x1,x2), (y1,y2)), ...] """ self.regions = [] self.fig, self.ax = plt.subplots(figsize=(10, 8)) # Handle coordinate arrays if isinstance(x, np.ndarray) and x.ndim == 1: X, Y = np.meshgrid(x, y) else: X, Y = x, y im = self.ax.pcolormesh(X, Y, z, shading="auto", cmap="viridis") self.fig.colorbar(im, ax=self.ax) self.ax.set_title( f"{title}\nClick and drag to select" " rectangular regions.\nPress ENTER" " or ESC when done, or run" " selector.finish_selection()" ) # Add keyboard event handling self.fig.canvas.mpl_connect("key_press_event", self._on_key_press) # Create rectangle selector - handle matplotlib version compatibility props_param = _get_widget_props_param() selector_kwargs = { "useblit": True, "button": [1], "minspanx": 5, "minspany": 5, props_param: dict(alpha=0.3, facecolor="red", edgecolor="red", linewidth=2), } self.current_selector = RectangleSelector( self.ax, self._on_select_2d, **selector_kwargs ) plt.show() return self.regions
# Global selector instance for Jupyter compatibility _current_selector = None
[docs] def interactive_select_regions_1d( x, y, title="Select regions to EXCLUDE from baseline fitting" ): """ Convenience function for interactive 1D region selection. Args: x: X-axis data y: Y-axis data title: Plot title Returns: list: Selected regions as [(x1, x2), ...] """ global _current_selector _current_selector = RegionSelector() return _current_selector.select_regions_1d(x, y, title)
[docs] def interactive_select_regions_2d( x, y, z, title="Select regions to EXCLUDE from baseline fitting" ): """ Convenience function for interactive 2D region selection. Args: x: X-axis coordinates y: Y-axis coordinates z: 2D data array title: Plot title Returns: list: Selected regions as [((x1,x2), (y1,y2)), ...] """ global _current_selector _current_selector = RegionSelector() return _current_selector.select_regions_2d(x, y, z, title)
[docs] def close_selector_window(): """ Utility function to close RegionSelector windows in Jupyter notebooks. Use this if the interactive region selector gets stuck or won't close. """ try: global _current_selector # noqa: F824 if _current_selector is not None: _current_selector.finish_selection() plt.close("all") logger.info("✅ All selector windows closed") except Exception as e: plt.close("all") logger.info(f"✅ Windows closed (with warning: {e})")
[docs] def jupyter_help(): """ Display help for using interactive region selection in Jupyter notebooks. """ help_text = """ 📋 JUPYTER NOTEBOOK - INTERACTIVE REGION SELECTION HELP ==================================================== When you run interactive baseline correction, a plot will appear. HOW TO SELECT REGIONS: 1. Click and drag on the plot to select regions to exclude from baseline fitting 2. You can select multiple regions HOW TO FINISH SELECTION: Method 1: Press ENTER or ESC key on the plot Method 2: In a new cell, run: from epyr.baseline.interactive import ( close_selector_window) close_selector_window() Method 3: In a new cell, run: plt.close('all') IF STUCK: - Run: plt.close('all') to force close all plots - Run in a new cell: from epyr.baseline.interactive import ( close_selector_window) close_selector_window() EXAMPLE: -------- import epyr x, y, params, filepath = epyr.eprload("data.dsc", plot_if_possible=False) # Option 1: Direct baseline correction with interactive selection corrected, baseline = epyr.baseline_polynomial_1d(x, y, params, interactive=True) # Option 2: Manual region selection first from epyr.baseline.interactive import interactive_select_regions_1d regions = interactive_select_regions_1d(x, y, "Select baseline regions") corrected, baseline = epyr.baseline_polynomial_1d(x, y, params, manual_regions=regions, region_mode='include') # If the plot won't close, run in a new cell: from epyr.baseline.interactive import close_selector_window close_selector_window() """ logger.info(help_text)
[docs] def is_interactive_available(): """ Check if interactive selection is available in the current environment. Returns: bool: True if interactive selection should work """ try: # Check if we're in a notebook from IPython import get_ipython ipython = get_ipython() if ipython is None: return True # Assume desktop matplotlib works # Check for notebook backends backend = matplotlib.get_backend().lower() if "inline" in backend: logger.warning( "Warning: Inline backend detected." " Interactive selection may not work." ) logger.warning(" Try: %matplotlib widget or %matplotlib notebook") return False elif "widget" in backend or "nbagg" in backend: return True else: return True # Assume it works except ImportError: return True # Not in Jupyter, assume desktop matplotlib works
[docs] def setup_interactive_backend(): """ Set up the best available interactive backend for the current environment. """ try: from IPython import get_ipython ipython = get_ipython() if ipython is not None: # We're in Jupyter current_backend = matplotlib.get_backend().lower() if "inline" in current_backend: logger.info("🔧 Setting up interactive backend...") try: ipython.magic("matplotlib widget") logger.info("✅ Switched to widget backend") except Exception: try: ipython.magic("matplotlib notebook") logger.info("✅ Switched to notebook backend") except Exception: logger.warning("⚠️ Could not switch to interactive backend") logger.warning(" Try running: %matplotlib widget") else: logger.info( "✅ Desktop matplotlib detected, interactive selection should work" ) except ImportError: logger.info("✅ Desktop environment, interactive selection should work")