import tkinter as tk import numpy as np import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure from matplotlib.axes import Axes from matplotlib.patches import Rectangle from typing import Optional class ProfileWindow(tk.Toplevel): """ A Toplevel window to display a 1D data profile plot. This window shows a plot of a single row or column from a matrix, providing interactive features like zoom and value inspection. """ def __init__(self, master: tk.Tk, data: np.ndarray, title: str): """ Initializes the profile window. Args: master: The parent tk.Tk window. data: The 1D NumPy array containing the profile data. title: The title for the window and the plot. """ super().__init__(master) self.title(title) self.geometry("800x600") # --- Internal state --- self._data = data self._original_xlim: Optional[tuple[float, float]] = None self._original_ylim: Optional[tuple[float, float]] = None # Zoom interaction variables self._zoom_start_x: Optional[float] = None self._zoom_rect: Optional[Rectangle] = None # --- Matplotlib components --- self._fig: Figure self._ax: Axes self._canvas: FigureCanvasTkAgg self._create_widgets() self._plot_data(title) self._connect_events() self._center_window() def _create_widgets(self) -> None: """Creates and arranges the main widgets of the window.""" # --- Toolbar --- toolbar_frame = tk.Frame(self) toolbar_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) dezoom_button = tk.Button( toolbar_frame, text="Reset Zoom", command=self._reset_zoom ) dezoom_button.pack(side=tk.LEFT) # --- Matplotlib Canvas --- self._fig = Figure(figsize=(6, 4), dpi=100) self._ax = self._fig.add_subplot(111) self._canvas = FigureCanvasTkAgg(self._fig, master=self) canvas_widget = self._canvas.get_tk_widget() canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) # --- Status Bar --- self._status_text = tk.StringVar() status_bar = tk.Label( self, textvariable=self._status_text, bd=1, relief=tk.SUNKEN, anchor=tk.W ) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _plot_data(self, title: str) -> None: """Plots the initial data on the axes.""" self._ax.plot(self._data) self._ax.set_title(title) self._ax.set_xlabel("Index") self._ax.set_ylabel("Value") self._ax.grid(True) self._original_xlim = self._ax.get_xlim() self._original_ylim = self._ax.get_ylim() self._canvas.draw() def _connect_events(self) -> None: """Connects mouse events to their callback methods.""" self._canvas.mpl_connect('button_press_event', self._on_mouse_press) self._canvas.mpl_connect('button_release_event', self._on_mouse_release) self._canvas.mpl_connect('motion_notify_event', self._on_mouse_move) def _center_window(self) -> None: """Centers the window on the screen.""" self.update_idletasks() screen_width = self.winfo_screenwidth() screen_height = self.winfo_screenheight() window_width = self.winfo_width() window_height = self.winfo_height() pos_x = (screen_width // 2) - (window_width // 2) pos_y = (screen_height // 2) - (window_height // 2) self.geometry(f"+{pos_x}+{pos_y}") def _on_mouse_press(self, event) -> None: """Handles mouse press events on the canvas.""" if not event.inaxes: return # Left click: Display value in status bar if event.button == 1 and event.xdata is not None: try: index = int(round(event.xdata)) if 0 <= index < len(self._data): value = self._data[index] self._status_text.set(f"Index: {index}, Value: {value:.3f}") else: self._status_text.set("") except (ValueError, IndexError): self._status_text.set("") # Right click: Start zoom selection elif event.button == 3: self._zoom_start_x = event.xdata y_min, y_max = self._ax.get_ylim() self._zoom_rect = Rectangle( (event.xdata, y_min), 0, y_max - y_min, linewidth=1, edgecolor='r', facecolor='r', alpha=0.2 ) self._ax.add_patch(self._zoom_rect) self._canvas.draw() def _on_mouse_release(self, event) -> None: """Handles mouse release events (for zoom).""" if event.button == 3 and self._zoom_start_x is not None and event.xdata is not None: x_start, x_end = sorted((self._zoom_start_x, event.xdata)) # Prevent zooming to a zero-width area if abs(x_end - x_start) > 1e-6: self._ax.set_xlim(x_start, x_end) # Clean up if self._zoom_rect: self._zoom_rect.remove() self._zoom_rect = None self._zoom_start_x = None self._canvas.draw() def _on_mouse_move(self, event) -> None: """Handles mouse movement during zoom selection.""" if self._zoom_rect and event.inaxes and event.xdata is not None: x_end = event.xdata width = x_end - self._zoom_start_x self._zoom_rect.set_width(width) self._zoom_rect.set_x(min(self._zoom_start_x, x_end)) self._canvas.draw_idle() def _reset_zoom(self) -> None: """Resets the plot axes to the original view.""" if self._original_xlim and self._original_ylim: self._ax.set_xlim(self._original_xlim) self._ax.set_ylim(self._original_ylim) self._canvas.draw()