diff --git a/doc/release/next_whats_new/scroll_to_zoom.rst b/doc/release/next_whats_new/scroll_to_zoom.rst new file mode 100644 index 000000000000..bafa312c32a5 --- /dev/null +++ b/doc/release/next_whats_new/scroll_to_zoom.rst @@ -0,0 +1,8 @@ +Zooming using mouse wheel +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Ctrl+MouseWheel`` can be used to zoom in the plot windows. + +The zoom focusses on the mouse pointer, and keeps the aspect ratio of the axes. + +Zooming is currently only supported on rectilinear Axes. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8107471955fe..7560db80d2c1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2574,6 +2574,51 @@ def button_press_handler(event, canvas=None, toolbar=None): toolbar.forward() +def scroll_handler(event, canvas=None, toolbar=None): + ax = event.inaxes + if ax is None: + return + if ax.name != "rectilinear": + # zooming is currently only supported on rectilinear axes + return + + if toolbar is None: + toolbar = (canvas or event.canvas).toolbar + + if toolbar is None: + # technically we do not need a toolbar, but until wheel zoom was + # introduced, any interactive modification was only possible through + # the toolbar tools. For now, we keep the restriction that a toolbar + # is required for interactive navigation. + return + + if event.key == "control": # zoom towards the mouse position + toolbar.push_current() + + xmin, xmax = ax.get_xlim() + ymin, ymax = ax.get_ylim() + (xmin, ymin), (xmax, ymax) = ax.transScale.transform( + [(xmin, ymin), (xmax, ymax)]) + + # mouse position in scaled (e.g., log) data coordinates + x, y = ax.transScale.transform((event.xdata, event.ydata)) + + scale_factor = 0.85 ** event.step + new_xmin = x - (x - xmin) * scale_factor + new_xmax = x + (xmax - x) * scale_factor + new_ymin = y - (y - ymin) * scale_factor + new_ymax = y + (ymax - y) * scale_factor + + inv_scale = ax.transScale.inverted() + (new_xmin, new_ymin), (new_xmax, new_ymax) = inv_scale.transform( + [(new_xmin, new_ymin), (new_xmax, new_ymax)]) + + ax.set_xlim(new_xmin, new_xmax) + ax.set_ylim(new_ymin, new_ymax) + + ax.figure.canvas.draw_idle() + + class NonGuiException(Exception): """Raised when trying show a figure in a non-GUI backend.""" pass @@ -2653,11 +2698,14 @@ def __init__(self, canvas, num): self.key_press_handler_id = None self.button_press_handler_id = None + self.scroll_handler_id = None if rcParams['toolbar'] != 'toolmanager': self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', key_press_handler) self.button_press_handler_id = self.canvas.mpl_connect( 'button_press_event', button_press_handler) + self.scroll_handler_id = self.canvas.mpl_connect( + 'scroll_event', scroll_handler) self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index c65d39415472..7a2b28262249 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -407,6 +407,11 @@ def button_press_handler( canvas: FigureCanvasBase | None = ..., toolbar: NavigationToolbar2 | None = ..., ) -> None: ... +def scroll_handler( + event: MouseEvent, + canvas: FigureCanvasBase | None = ..., + toolbar: NavigationToolbar2 | None = ..., +) -> None: ... class NonGuiException(Exception): ... @@ -415,6 +420,7 @@ class FigureManagerBase: num: int | str key_press_handler_id: int | None button_press_handler_id: int | None + scroll_handler_id: int | None toolmanager: ToolManager | None toolbar: NavigationToolbar2 | ToolContainerBase | None def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ...