add toolbar-free mouse panning and zooming to the plot
Build Debian Packages / build (bookworm, debian12) (push) Successful in 11m52s
Build Debian Packages / build (trixie, debian13) (push) Successful in 12m3s

Connect matplotlib event handlers on the plot canvas so the display
can be navigated without engaging the toolbar modes: left button
drag pans the view, the mouse wheel zooms both axes around the
pointer, and right button drag zooms single axes (horizontal motion
zooms x, vertical motion zooms y, like the right button in the
toolbar pan mode). Linear (including inverted ppm) and logarithmic
axes are handled, the latter in log space. While a toolbar pan/zoom
mode is active the handlers stand back so the toolbar behavior is
unchanged. Documented in doc/manual.rst.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
markusro committed 2026-10-19 23:16:43 +02:00
1 parent 1bed381639
commit 7164531351
2 files changed
+104

No files matched your search

+102
View File
@@ -3306,6 +3306,16 @@ class MonitorWidgets:
self.display_table.attach( self.matplot_toolbar, 0, 1, 1, 2, gtk.AttachOptions.FILL | gtk.AttachOptions.EXPAND, 0, 0, 0 )
self.matplot_toolbar.show( )
# mouse pan (left button drag), single-axis zoom (middle button
# drag) and wheel zoom, active whenever no toolbar pan/zoom
# mode is on
self._pan_start = None
self._zoom_drag = None
self.matplot_canvas.mpl_connect( "scroll_event", self.plot_wheel_zoom )
self.matplot_canvas.mpl_connect( "button_press_event", self.plot_button_press )
self.matplot_canvas.mpl_connect( "motion_notify_event", self.plot_button_motion )
self.matplot_canvas.mpl_connect( "button_release_event", self.plot_button_release )
# select display source
self.display_source_combobox = self.xml_gui.get_object( "display_source_combobox" )
self.display_source_treestore = gtk.TreeStore( gobject.TYPE_STRING )
@@ -3802,6 +3812,98 @@ class MonitorWidgets:
return True
@staticmethod
def _zoomed_limits( limits, center, factor, logarithmic ):
"""Return the limits zoomed by factor around center.
On logarithmic axes the scaling happens in log space.
"""
if logarithmic:
limits = numpy.log10( limits )
center = numpy.log10( center )
new_limits = [ center + ( limit - center ) * factor for limit in limits ]
if logarithmic:
new_limits = [ 10 ** limit for limit in new_limits ]
return tuple( new_limits )
@staticmethod
def _panned_limits( limits, grabbed, current, logarithmic ):
"""Return the limits shifted so that current moves to grabbed.
On logarithmic axes the shift happens in log space.
"""
if logarithmic:
shift = 10 ** ( numpy.log10( grabbed ) - numpy.log10( current ) )
return tuple( limit * shift for limit in limits )
shift = grabbed - current
return tuple( limit + shift for limit in limits )
def plot_wheel_zoom( self, event ):
"""Zoom the plot around the pointer position with the mouse wheel."""
if event.inaxes is not self.matplot_axes or event.xdata is None or event.ydata is None:
return
factor = 1.0 / 1.5 if event.step > 0 else 1.5
axes = self.matplot_axes
axes.set_xlim( self._zoomed_limits( axes.get_xlim( ), event.xdata, factor,
axes.get_xscale( ) == "log" ) )
axes.set_ylim( self._zoomed_limits( axes.get_ylim( ), event.ydata, factor,
axes.get_yscale( ) == "log" ) )
self.matplot_canvas.draw_idle( )
def plot_button_press( self, event ):
"""Start panning the plot with a left button drag.
A right button drag starts zooming single axes, like the right
button in the toolbar pan mode: horizontal motion zooms the x
axis, vertical motion the y axis. Both are inactive while a
toolbar pan/zoom mode is on, so the toolbar behavior stays
unchanged.
"""
if event.inaxes is not self.matplot_axes or event.xdata is None or event.ydata is None:
return
if event.button == 3 and not self.matplot_toolbar.mode:
self._zoom_drag = ( event.x, event.y, event.xdata, event.ydata )
elif event.button == 1 and not self.matplot_toolbar.mode:
self._pan_start = ( event.xdata, event.ydata )
def plot_button_motion( self, event ):
"""Pan the plot so the grabbed data point follows the pointer.
With the right button held, zoom single axes around the
pressed position instead: horizontal motion zooms the x axis,
vertical motion the y axis, dragging right or up zooms in.
"""
if self._zoom_drag is not None and event.x is not None and event.y is not None:
x0, y0, xdata0, ydata0 = self._zoom_drag
axes = self.matplot_axes
dx = event.x - x0
dy = event.y - y0
if dx:
factor = 10 ** ( -dx / axes.bbox.width )
axes.set_xlim( self._zoomed_limits( axes.get_xlim( ), xdata0, factor,
axes.get_xscale( ) == "log" ) )
if dy:
factor = 10 ** ( -dy / axes.bbox.height )
axes.set_ylim( self._zoomed_limits( axes.get_ylim( ), ydata0, factor,
axes.get_yscale( ) == "log" ) )
self.matplot_canvas.draw_idle( )
return
if self._pan_start is None or event.xdata is None or event.ydata is None:
return
x0, y0 = self._pan_start
axes = self.matplot_axes
axes.set_xlim( self._panned_limits( axes.get_xlim( ), x0, event.xdata,
axes.get_xscale( ) == "log" ) )
axes.set_ylim( self._panned_limits( axes.get_ylim( ), y0, event.ydata,
axes.get_yscale( ) == "log" ) )
self._pan_start = ( event.xdata, event.ydata )
self.matplot_canvas.draw_idle( )
def plot_button_release( self, event ):
"""Stop panning or single-axis zooming the plot."""
self._pan_start = None
self._zoom_drag = None
def toggle_cursor(self, widget):
"""Toggle matplotlib crosshair cursor"""
if widget.get_active():