Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
factory for adding zoom callback to matplotlib graphs
import matplotlib.pyplot as plt
def zoom_factory(ax,base_scale = 2.):
def zoom_fun(event):
# get the current x and y limits
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
# set the range
cur_xrange = (cur_xlim[1] - cur_xlim[0])*.5
cur_yrange = (cur_ylim[1] - cur_ylim[0])*.5
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if event.button == 'up':
# deal with zoom in
scale_factor = 1/base_scale
elif event.button == 'down':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print event.button
# set new limits
ax.set_xlim([xdata - cur_xrange*scale_factor,
xdata + cur_xrange*scale_factor])
ax.set_ylim([ydata - cur_yrange*scale_factor,
ydata + cur_yrange*scale_factor])
ax.figure.canvas.draw_idle() # force re-draw the next time the GUI refreshes
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('scroll_event',zoom_fun)
#return the function
return zoom_fun
@jdreaver

This comment has been minimized.

Copy link

@jdreaver jdreaver commented Nov 19, 2012

Very nice!

One modification I made for my PyQt4 GUI: instead of using ax as a parameter, I pass in the parent canvas and say ax = canvas.ax on the first line. Then, instead of plt.draw(), I use canvas.draw(). However, your version works better for subplot zooming.

@tacaswell

This comment has been minimized.

Copy link
Owner Author

@tacaswell tacaswell commented May 24, 2013

@jdreaver I didn't see your comment till recently. I am glad it was helpful! If you think your changes would be helpful for other people you should fork the gist.

I also tweaked it a bit to not call plt.draw().

@giadang

This comment has been minimized.

Copy link

@giadang giadang commented Jan 17, 2015

IMHO, I think we don't have to calculate the cur_xrange and cur_yrange. Instead we can calculate the relative distance from the cursor position to the frame like this:

        # Get distance from the cursor to the edge of the figure frame
        x_left = xdata - cur_xlim[0]
        x_right = cur_xlim[1] - xdata
        y_top = ydata - cur_ylim[0]
        y_bottom = cur_ylim[1] - ydata

then set the new xlim, ylim like this:

ax.set_xlim([xdata - x_left*scale_factor,
                    xdata + x_right*scale_factor])
ax.set_ylim([ydata - y_top*scale_factor,
                    ydata + y_bottom*scale_factor])

By this way, when zooming in and zooming out, the pixel which the cursor is pointing to won't be changed.

@tacaswell

This comment has been minimized.

Copy link
Owner Author

@tacaswell tacaswell commented Jun 19, 2015

@giadang good point! That fixes one of the more annoying behaviors of this.

@PeterPablo

This comment has been minimized.

Copy link

@PeterPablo PeterPablo commented Jul 12, 2016

Thank you for this gist. Zooming in and out with the mousewheel (without moving the mouse pointer) does not restore the original view. It seems that the underlying view is paned.

@Arun-KR

This comment has been minimized.

Copy link

@Arun-KR Arun-KR commented Aug 2, 2018

I'm new to matplotlib. Trying to support zoom on a scatterplot programatically.
Its giving an inverted rubberband rectangle while doing the operations using the slots on mouse down (on LButton), mouse move and mouse up events. Tried few transforms, but no luck. Any suggestions on this, please.

@becorey

This comment has been minimized.

Copy link

@becorey becorey commented Mar 1, 2019

@giadang that works perfectly!

@ozsolarwind

This comment has been minimized.

Copy link

@ozsolarwind ozsolarwind commented Jun 29, 2019

Perfect. Thank you. I was trying to solve exactly this problem.

@3yan

This comment has been minimized.

Copy link

@3yan 3yan commented Nov 16, 2019

Thank you guys! I've found beneficial to implement 'event.step'. Posting the code.

Implementation of @giadang (commented on Jan 17, 2015) with event.step:

import numpy as np
#of course import matplotlib too... but that should be already part of your project

def zoom_factory(axis, base_scale=1e-3):
    """returns zooming functionality to axis"""
    def zoom_fun(event, ax, scale):
        """zoom when scrolling"""
        if event.inaxes == axis:
            scale_factor = np.power(scale,-event.step)
            xdata = event.xdata
            ydata = event.ydata
            x_left = xdata - ax.get_xlim()[0]
            x_right = ax.get_xlim()[1] - xdata
            y_top = ydata - ax.get_ylim()[0]
            y_bottom = ax.get_ylim()[1] - ydata

            ax.set_xlim([xdata - x_left * scale_factor,
                         xdata + x_right * scale_factor])
            ax.set_ylim([ydata - y_top * scale_factor,
                         ydata + y_bottom * scale_factor])
            ax.figure.canvas.draw()

    fig = axis.get_figure()
    fig.canvas.mpl_connect('scroll_event', lambda event: zoom_fun(
        event, axis, 1+base_scale))

My own implementation into Qt from mine project (stripped a bit) - shift selects the axis and also mods functionality:

import numpy as np
from PyQt5 import QtWidgets
#of course import matplotlib too... but that should be already part of your project

def zoom_factory(axis, base_scale=1e-3):
    """returns zooming functionality to axis"""
    def zoom_fun(event, ax, scale):
        """zoom when scrolling"""
        if event.inaxes == axis:
            scale_factor = np.power(scale,-event.step)
            if QtWidgets.QApplication.keyboardModifiers() !=\ 
                    QtCore.Qt.ShiftModifier:
                data = event.ydata
                new_top = data + (ax.get_ylim()[1] - data) \
                    * scale_factor
                ymin = -0.01
                axis.set_ylim([new_top * ymin, new_top])
            else:
                data = event.xdata
                x_left = data - ax.get_xlim()[0]
                x_right = ax.get_xlim()[1] - data
                ax.set_xlim([data - x_left * scale_factor,
                            data + x_right * scale_factor])
            ax.figure.canvas.draw()

    fig = axis.get_figure()
    fig.canvas.mpl_connect('scroll_event', lambda event: zoom_fun(
        event, axis, 1+base_scale))
@mapfiable

This comment has been minimized.

Copy link

@mapfiable mapfiable commented Apr 15, 2020

There is another way of doing it as far as I know. By chance I came across the Axis.zoom method. I don't know if this is faster or a good way in general, but it works and is certainly less code:

    def zoom(self, event):
        if event.inaxes == self.ax:
            scale_factor = np.power(self.zoom_factor, -event.step)*event.step
            self.ax.get_xaxis().zoom(scale_factor)
            self.ax.get_yaxis().zoom(scale_factor)
            self.ax.invert_yaxis()
            self.canvas.draw_idle()

If you plot an image though, for some reason, you have to invert the y-axis again.

@ianhi

This comment has been minimized.

Copy link

@ianhi ianhi commented May 10, 2020

If you're using this in an environment with a toolbar and you want the home button to return to the original view after zooming:
image
then I found that you need to force the initial position to be pushed into the toolbars _nav_stack. When using the ipympl backend I did this using like so:

fig.canvas.toolbar.push_current()
zoom_factory(ax)

you could also add this to the zoom factory, but that may be risky as I'm not sure that fig.canvas.toolbaris guaranteed to exist.

Edit:
I just peeked around the forks of this gist and what I described with the toolbar was implemented in this one: https://gist.github.com/scott-vsi/522e756d636557ae8f1ef3cdb069cecd

that gist also does some nice checking inside of zoom_fun to also set the _nav_stack there if there are no views already pushed:

        toolbar = ax.get_figure().canvas.toolbar # only set the home state
        if toolbar._views.empty():
            toolbar.push_current()
@nitishk24

This comment has been minimized.

Copy link

@nitishk24 nitishk24 commented May 11, 2020

@ianhi Can you please post an example code in which we zoom a plot. I'm a beginner with matplotlib and can't get to work the function right. Thanks!

@ozsolarwind

This comment has been minimized.

Copy link

@ozsolarwind ozsolarwind commented May 11, 2020

This may help.
You do the plot, call zoom_factory, then show the plot:

fig = `plt.figure(...
graph = fig.add_subplot(111)
graph.plot(...
zf = zoom_factory(graph)
plt.show()

I've been using it quite happily but my code is a bit too specific but derived from simp_zoom.py

@tacaswell

This comment has been minimized.

Copy link
Owner Author

@tacaswell tacaswell commented May 11, 2020

@mapfiable Interesting, I also did not know that existing until you pointed it out! The reason for the re-inversion is that you can invert the axis by setting the "min" to be greater than the "max" and inside of axis.zoom (which calls Locator.zoom) the inversion is discarded.

@mapfiable

This comment has been minimized.

Copy link

@mapfiable mapfiable commented May 11, 2020

@tacaswell: do you think it is a good alternative? There is also Axis.pan, but I didn't manage to make it work properly.

@Khirod1999

This comment has been minimized.

Copy link

@Khirod1999 Khirod1999 commented Jun 11, 2020

Can we implement this code in jupyter Notebook?
If yes, please explain.

@tacaswell

This comment has been minimized.

Copy link
Owner Author

@tacaswell tacaswell commented Jun 17, 2020

Yes, but you must use either %matplotlib notebook or %matplotlib widget which should "just work". If you are using %matplotlib inline it will not work (because that is not an interactive backend).

@ianhi

This comment has been minimized.

Copy link

@ianhi ianhi commented Jun 17, 2020

There's a small caveat to "just working" in the notebook - the widget backend doesn't have an option capture scroll events so you will end up scrolling the entire notebook while also zooming:
gif

The workaround I used for this was to to use jupyterlab sidecar widget to display the plot as that won't have a scroll bar so it doesn't matter that the scroll input wasn't captured. Long term - I opened an issue and PR about this matplotlib/ipympl#222 that I think would fix this.
Also I think you will need to install the widget backend (https://github.com/matplotlib/ipympl#installation) as that isn't included with standard matplotlib

@tacaswell

This comment has been minimized.

Copy link
Owner Author

@tacaswell tacaswell commented Jun 17, 2020

That is a fun bug! I left a review on your PR at ipympl.

@ianhi

This comment has been minimized.

Copy link

@ianhi ianhi commented Jun 18, 2020

Thanks!

For completeness for any future readers: The fix for scrolling in the notebook was merged and should be part of any release of ipympl after 0.5.6 (it is not in that release)

@peroman200

This comment has been minimized.

Copy link

@peroman200 peroman200 commented Feb 9, 2021

Hey, I tried to implement this in a wxPython Frame, but I keep getting a TypeError: zoom_fun() missing 1 required positional argument: 'event'. I know how to solve missing events in wxPython, but not in matplotlib. It should work because otherwise I wouldn't be getting the error on scrolling...
Edit: Turns out there is a work around, that only works if you're also pressing down the mouse wheel, not just scrolling with it...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment