164 lines
6.0 KiB
Python
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() |