diff --git a/src/sc_linac_physics/applications/microphonics/plots/base_plot.py b/src/sc_linac_physics/applications/microphonics/plots/base_plot.py index 2ac27807..1c0d0cc7 100644 --- a/src/sc_linac_physics/applications/microphonics/plots/base_plot.py +++ b/src/sc_linac_physics/applications/microphonics/plots/base_plot.py @@ -11,7 +11,7 @@ def autoRangeEnabled(self): pg.PlotWidget.autoRangeEnabled = autoRangeEnabled from typing import Tuple, Optional - +from sc_linac_physics.utils.plot_tooltip import PlotTooltip import numpy as np import pyqtgraph as pg from PyQt5.QtCore import Qt @@ -34,7 +34,6 @@ def __init__(self, parent=None, plot_type=None, config=None): self.plot_type = plot_type self.config = config or {} self.plot_curves = {} - self.tooltips = {} # Setup UI components common to all plots self.setup_ui() @@ -117,13 +116,14 @@ def _setup_plot_widget(self, plot_type, config): if "y_range" in config: widget.setYRange(*config["y_range"]) - # Connect signal for tooltips - widget.scene().sigMouseMoved.connect( - lambda ev: self._show_tooltip(plot_type, ev) - ) + self.tooltip = PlotTooltip(widget, self.get_formatter()) return widget + def get_formatter(self): + """Override in subclasses for custom tooltip formatting.""" + return None + def _get_cavity_pen(self, cavity_num): """ Create a pen for the specified cavity w/ appropriate styling @@ -153,56 +153,6 @@ def _get_cavity_pen(self, cavity_num): return pg.mkPen(qcolor, width=2, style=style) - def _show_tooltip(self, plot_type, ev): - """ - Show tooltip w/ data values on hover - - Args: - plot_type: Type of plot - ev: Mouse event - """ - try: - plot = self.plot_widget - view = plot.plotItem.vb - if plot.sceneBoundingRect().contains(ev): - mouse_point = view.mapSceneToView(ev) - x, y = mouse_point.x(), mouse_point.y() - - # Format tooltip based on plot type - tooltip = self._format_tooltip(plot_type, x, y) - if not tooltip: - return - - # Update/create tooltip label - if plot_type not in self.tooltips: - self.tooltips[plot_type] = pg.TextItem( - text=tooltip, - color=(255, 255, 255), - border="k", - fill=(0, 0, 0, 180), - ) - plot.addItem(self.tooltips[plot_type]) - - self.tooltips[plot_type].setText(tooltip) - self.tooltips[plot_type].setPos(x, y) - self.tooltips[plot_type].show() - except Exception as e: - print(f"Tooltip error: {str(e)}") - - def _format_tooltip(self, plot_type, x, y): - """ - Format tooltip text based on plot type - - Args: - plot_type: Type of plot - x: X coordinate - y: Y coordinate - - Returns: - str: Tooltip text - """ - return f"X: {x:.2f}, Y: {y:.2f}" - def _preprocess_data( self, cavity_channel_data: dict, channel_type: str = "DF" ) -> Tuple[Optional[np.ndarray], bool]: @@ -263,8 +213,8 @@ def clear_plot(self): self.plot_widget.clear() self.plot_curves = {} # Hide tooltip if it exists - if self.plot_type in self.tooltips: - self.tooltips[self.plot_type].hide() + if self.tooltip: + self.tooltip.hide() def update_plot(self, cavity_num, data): """ diff --git a/src/sc_linac_physics/applications/microphonics/plots/fft_plot.py b/src/sc_linac_physics/applications/microphonics/plots/fft_plot.py index 299db793..2abb655a 100644 --- a/src/sc_linac_physics/applications/microphonics/plots/fft_plot.py +++ b/src/sc_linac_physics/applications/microphonics/plots/fft_plot.py @@ -7,6 +7,7 @@ from sc_linac_physics.applications.microphonics.utils.data_processing import ( calculate_fft, ) +from sc_linac_physics.utils.plot_tooltip import PlotTooltip class FFTPlot(BasePlot): @@ -67,18 +68,9 @@ def set_plot_config(self, panel_wide_config): default_y_range = self.config.get("y_range", (0, 1.5)) self.plot_widget.setYRange(*default_y_range) - def _format_tooltip(self, plot_type, x, y): - """Format tooltip text for FFT plot - - Args: - plot_type: Type of plot (unused in this implementation) - x: X coordinate (frequency in Hz) - y: Y coordinate (amplitude) - - Returns: - str: Formatted tooltip text - """ - return f"Frequency: {x:.1f} Hz\nAmplitude: {y:.3f}" + def get_formatter(self): + """Tooltip formatter for FFT plot.""" + return PlotTooltip.make_formatter("Frequency (Hz)", "Amplitude") def update_plot(self, cavity_num, cavity_channel_data): """Update FFT plot w/ new data diff --git a/src/sc_linac_physics/applications/microphonics/plots/histogram_plot.py b/src/sc_linac_physics/applications/microphonics/plots/histogram_plot.py index baf84aac..c4800849 100644 --- a/src/sc_linac_physics/applications/microphonics/plots/histogram_plot.py +++ b/src/sc_linac_physics/applications/microphonics/plots/histogram_plot.py @@ -4,6 +4,7 @@ from sc_linac_physics.applications.microphonics.utils.data_processing import ( calculate_histogram, ) +from sc_linac_physics.utils.plot_tooltip import PlotTooltip class HistogramPlot(BasePlot): @@ -26,18 +27,9 @@ def __init__(self, parent=None): self.num_bins = 140 # Default number of bins super().__init__(parent, plot_type="histogram", config=config) - def _format_tooltip(self, plot_type, x, y): - """Format tooltip text specifically for histogram plot - - Args: - plot_type: Type of plot (unused in this implementation) - x: X coordinate (detuning in Hz) - y: Y coordinate (count) - - Returns: - str: Formatted tooltip text - """ - return f"Detuning: {x:.1f} Hz\nCount: {int(max(1, y))}" + def get_formatter(self): + """Tooltip formatter for histogram plot.""" + return PlotTooltip.make_formatter("Detuning (Hz)", "Count") def _update_data_range(self, df_data): """Update data range based on current data.""" diff --git a/src/sc_linac_physics/applications/microphonics/plots/spectrogram_plot.py b/src/sc_linac_physics/applications/microphonics/plots/spectrogram_plot.py index b798a08d..682022c7 100644 --- a/src/sc_linac_physics/applications/microphonics/plots/spectrogram_plot.py +++ b/src/sc_linac_physics/applications/microphonics/plots/spectrogram_plot.py @@ -156,10 +156,6 @@ def _refresh_grid_layout(self): if visible_cavities: self._add_colorbar(num_rows) - def _format_tooltip(self, plot_type, x, y): - """Format tooltip text specifically for spectrogram plot""" - return f"Time: {x:.3f} s\nFrequency: {y:.1f} Hz" - def update_plot(self, cavity_num, cavity_channel_data): df_data, is_valid = self._preprocess_data( cavity_channel_data, channel_type="DF" @@ -270,10 +266,6 @@ def set_plot_config(self, config): elif self.cavity_data_cache: self._refresh_grid_layout() - def _show_tooltip(self, plot_type, ev): - """Override tooltip behavior""" - pass - def clear_plot(self): """Clear all plot data""" self.cavity_data_cache.clear() diff --git a/src/sc_linac_physics/applications/microphonics/plots/time_series_plot.py b/src/sc_linac_physics/applications/microphonics/plots/time_series_plot.py index e7666f3a..a303b845 100644 --- a/src/sc_linac_physics/applications/microphonics/plots/time_series_plot.py +++ b/src/sc_linac_physics/applications/microphonics/plots/time_series_plot.py @@ -7,6 +7,7 @@ from sc_linac_physics.applications.microphonics.utils.constants import ( BASE_HARDWARE_SAMPLE_RATE, ) +from sc_linac_physics.utils.plot_tooltip import PlotTooltip class TimeSeriesPlot(BasePlot): @@ -39,9 +40,9 @@ def __init__(self, parent=None): vb.setLimits(xMin=0) # Prevent negative time vb.sigRangeChangedManually.connect(self._on_range_changed) - def _format_tooltip(self, plot_type, x, y): - """Override base tooltip formatting""" - return f"Time: {x:.3f} s\nDetuning: {y:.2f} Hz" + def get_formatter(self): + """Tooltip formatter for time series plot.""" + return PlotTooltip.make_formatter("Time (s)", "Detuning (Hz)") def _decimate_data(self, times, values, target_points): """Decimation that still preserves important features""" diff --git a/src/sc_linac_physics/utils/plot_tooltip.py b/src/sc_linac_physics/utils/plot_tooltip.py index 98bf57d4..82178047 100644 --- a/src/sc_linac_physics/utils/plot_tooltip.py +++ b/src/sc_linac_physics/utils/plot_tooltip.py @@ -36,7 +36,7 @@ def __init__( self._container = plot else: self._plot_item = plot - self._container = plot + self._container = plot.getViewBox() self.formatter = formatter or format_default self.enabled = True @@ -98,3 +98,13 @@ def cleanup(self): self._plot_item.removeItem(self._tooltip) except (RuntimeError, AttributeError): pass + + @staticmethod + def make_formatter( + x_label: str, + y_label: str, + x_fmt: str = ".2f", + y_fmt: str = ".2f", + ) -> Callable[[float, float], str]: + """Create a formatter from axis labels and optional format specs.""" + return lambda x, y: f"{x_label}: {x:{x_fmt}}\n{y_label}: {y:{y_fmt}}" diff --git a/tests/applications/microphonics/plots/test_time_series_plot.py b/tests/applications/microphonics/plots/test_time_series_plot.py index d8f7bff4..dab70591 100644 --- a/tests/applications/microphonics/plots/test_time_series_plot.py +++ b/tests/applications/microphonics/plots/test_time_series_plot.py @@ -428,11 +428,12 @@ def test_end_zoom_resets_flag(self, plot_widget): def test_format_tooltip(self, plot_widget): """Test tooltip formatting""" - tooltip = plot_widget._format_tooltip("time_series", x=5.123, y=123.456) + formatter = plot_widget.get_formatter() + tooltip = formatter(5.123, 123.456) - assert "Time:" in tooltip - assert "5.123" in tooltip - assert "Detuning:" in tooltip + assert "Time" in tooltip + assert "5.12" in tooltip + assert "Detuning" in tooltip assert "123.46" in tooltip # ===== Clear Plot Tests =====