SXXXXXXX_Radalyze/radalyze/gui/profile_window.py
2025-06-13 14:13:29 +02:00

164 lines
6.0 KiB
Python

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()