LCOV - code coverage report
Current view: top level - src/pairinteraction_gui/plotwidget - canvas.py (source / functions) Coverage Total Hit
Test: coverage.info Lines: 45.8 % 48 22
Test Date: 2026-11-02 09:44:19 Functions: 50.0 % 4 2

            Line data    Source code
       1              : # SPDX-FileCopyrightText: 2025 PairInteraction Developers
       2              : # SPDX-License-Identifier: LGPL-3.0-or-later
       3            1 : from __future__ import annotations
       4              : 
       5            1 : from typing import TYPE_CHECKING
       6              : 
       7            1 : import numpy as np
       8            1 : from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
       9            1 : from matplotlib.figure import Figure
      10            1 : from PySide6.QtCore import Qt, QTimer
      11              : 
      12              : if TYPE_CHECKING:
      13              :     from PySide6.QtGui import QWheelEvent
      14              :     from PySide6.QtWidgets import QWidget
      15              : 
      16              : 
      17            1 : class MatplotlibCanvas(FigureCanvasQTAgg):
      18              :     """Canvas for matplotlib figures."""
      19              : 
      20            1 :     def __init__(self, parent: QWidget | None = None) -> None:
      21              :         """Initialize the canvas with a figure."""
      22              :         # Create the figure directly instead of via pyplot, so it is not registered in pyplot's global
      23              :         # figure manager, which would keep this canvas (and thus the whole widget) alive forever.
      24            1 :         self.fig = Figure()
      25            1 :         self.ax = self.fig.add_subplot()
      26            1 :         super().__init__(self.fig)
      27              : 
      28            1 :         self.setup_zoom()
      29              : 
      30            1 :     def setup_zoom(self) -> None:
      31              :         """Set up mouse wheel zoom functionality."""
      32              :         # Wheel event accumulation variables
      33            1 :         self.wheel_accumulation: float = 0
      34            1 :         self.last_wheel_pos: list[list[float]] = []
      35            1 :         self.wheel_timer = QTimer(self)
      36            1 :         self.wheel_timer.setSingleShot(True)
      37            1 :         self.wheel_timer.timeout.connect(self.apply_accumulated_zoom)
      38            1 :         self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
      39            1 :         self.setMouseTracking(True)
      40              : 
      41            1 :     def wheelEvent(self, event: QWheelEvent) -> None:
      42              :         """Handle mouse wheel events for zooming."""
      43            0 :         self.wheel_accumulation += event.angleDelta().y() / 120
      44            0 :         self.last_wheel_pos.append([event.position().x(), event.position().y()])
      45            0 :         self.wheel_timer.start(100)  # Apply zoom after 100ms of inactivity
      46              : 
      47            1 :     def apply_accumulated_zoom(self) -> None:
      48              :         """Apply the accumulated zoom from wheel events."""
      49            0 :         if not self.wheel_accumulation:
      50            0 :             return
      51              : 
      52            0 :         x_min, x_max = self.ax.get_xlim()
      53            0 :         y_min, y_max = self.ax.get_ylim()
      54              : 
      55            0 :         scale_factor = 1 - 0.1 * self.wheel_accumulation
      56              : 
      57              :         # Get the mouse position in data coordinates
      58            0 :         wheel_pos_mean = np.mean(self.last_wheel_pos, axis=0)
      59            0 :         x_data, y_data = self.ax.transData.inverted().transform(wheel_pos_mean)
      60            0 :         y_data = -(y_data - (y_max + y_min) / 2) + (y_max + y_min) / 2  # y_data is mirrored bottom / top
      61              : 
      62            0 :         self.wheel_accumulation = 0
      63            0 :         self.last_wheel_pos = []
      64              : 
      65            0 :         if x_data > x_max or y_data > y_max or (x_data < x_min and y_data < y_min):
      66            0 :             return
      67              : 
      68            0 :         if x_min <= x_data <= x_max:
      69            0 :             x_min_new = x_data - (x_data - x_min) * scale_factor
      70            0 :             x_max_new = x_data + (x_max - x_data) * scale_factor
      71            0 :             self.ax.set_xbound(x_min_new, x_max_new)
      72            0 :             self.ax.set_autoscalex_on(False)
      73              : 
      74            0 :         if y_min <= y_data <= y_max:
      75            0 :             y_min_new = y_data - (y_data - y_min) * scale_factor
      76            0 :             y_max_new = y_data + (y_max - y_data) * scale_factor
      77            0 :             self.ax.set_ybound(y_min_new, y_max_new)
      78            0 :             self.ax.set_autoscaley_on(False)
      79              : 
      80              :         # Redraw the canvas
      81            0 :         self.draw_idle()
        

Generated by: LCOV version 2.0-1