Home > Enterprise >  Matplotlib Zoom Subplot Separately
Matplotlib Zoom Subplot Separately

Time:08-10

Newbie to Matplotlib

There is a dated discussion on how to zoom a scatter plot here which is very handy.

I tried a little hack to change the code to display 2 separate scatter plots. I modified the code to have two scatter plots and instantiate the class ZoomPan to each of the plots. The Pan function works fine -- I can choose to pan either plot without affecting the other. However, when I move my mouse to one scatter plot and use the mouse wheel to zoom the plot. Both plots zoom in and out together.

from matplotlib.pyplot import figure, show
import matplotlib.pyplot as plt
import numpy

class ZoomPan:
    def __init__(self,ax):
        self.press = None
        self.cur_xlim = None
        self.cur_ylim = None
        self.x0 = None
        self.y0 = None
        self.x1 = None
        self.y1 = None
        self.xpress = None
        self.ypress = None
        self.ax = ax

    def zoom_factory(self, base_scale = 2.):
        def zoom(event):
            ax = self.ax
            
            cur_xlim = ax.get_xlim()
            cur_ylim = ax.get_ylim()

            xdata = event.xdata # get event x location
            ydata = event.ydata # get event y location

            if event.button == 'down':
                # deal with zoom in
                scale_factor = 1 / base_scale
            elif event.button == 'up':
                # deal with zoom out
                scale_factor = base_scale
            else:
                # deal with something that should never happen
                scale_factor = 1
                print(event.button)

            new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
            new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor

            relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
            rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])

            ax.set_xlim([xdata - new_width * (1-relx), xdata   new_width * (relx)])
            ax.set_ylim([ydata - new_height * (1-rely), ydata   new_height * (rely)])
            ax.figure.canvas.draw()

        ax = self.ax
        self.ax.canvas.mpl_connect('scroll_event',zoom)
        fig.canvas.mpl_connect('scroll_event', zoom)
        
        return zoom

    def pan_factory(self):
        def onPress(event):
            ax = self.ax
            if event.inaxes != ax: return
            self.cur_xlim = ax.get_xlim()
            self.cur_ylim = ax.get_ylim()
            self.press = self.x0, self.y0, event.xdata, event.ydata
            self.x0, self.y0, self.xpress, self.ypress = self.press

        def onRelease(event):
            ax = self.ax
            self.press = None
            ax.figure.canvas.draw()

        def onMotion(event):
            ax = self.ax
            if self.press is None: return
            if event.inaxes != ax: return
            dx = event.xdata - self.xpress
            dy = event.ydata - self.ypress
            self.cur_xlim -= dx
            self.cur_ylim -= dy
            ax.set_xlim(self.cur_xlim)
            ax.set_ylim(self.cur_ylim)

            ax.figure.canvas.draw()

        ax = self.ax
        fig = ax.get_figure() # get the figure of interest

        # attach the call back
        fig.canvas.mpl_connect('button_press_event',onPress)
        fig.canvas.mpl_connect('button_release_event',onRelease)
        fig.canvas.mpl_connect('motion_notify_event',onMotion)

        #return the function
        return onMotion

def setscatter(scatter_label,ax):
    ax.set_title(scatter_label)
    x,y,s,c = numpy.random.rand(4,200)
    s *= 200
    ax.scatter(x,y,s,c)
    scale = 1.1
    zp = ZoomPan(ax)
    zp.zoom_factory(base_scale=scale)

def main():
    fig, axs = plt.subplots(nrows=1,ncols=2)
    setscatter('ax',axs[0])
    setscatter('bx',axs[1])
    fig.tight_layout()
    plt.show()

if __name__ == '__main__':
    main()

I tried to dig a bit inside the code and I wonder if I can attach the call back functions to individual subplot instead to the figure itself

    self.ax.canvas.mpl_connect('scroll_event',zoom)
    fig.canvas.mpl_connect('scroll_event', zoom)

It seems there is no such method .canvas for a subplot and I couldn't wrap my head around to find a way to pass the current inaxes to the class when the zoom function is triggered as in the example shown here.

I realized that the original discussion has been dated for a while. Maybe there are new features in Matplotlib can achieve the same.

Thanks in advance for any help.

CodePudding user response:

I tried to dig a bit inside the code and I wonder if I can attach the call back functions to individual subplot instead to the figure itself.

You cannot. However, you can employ the same logic used at the start of onPress such that the function body of zoom is only executed if the cursor is within the axis limits:

def zoom(event):
    ax = self.ax
    if event.inaxes != ax: return 
    ...
    # rest of function 
  • Related