web-dev-qa-db-fra.com

Matplotlib zoom avec la molette de défilement

Est-il possible de lier la molette de la souris pour un zoom avant/arrière lorsque le curseur survole un graphique matplotlib?

25
dimka

Cela devrait marcher. Il recentre le graphique sur l'emplacement du pointeur lorsque vous faites défiler.

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()
        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])
        plt.draw() # force re-draw

    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

En supposant que vous ayez un objet axe ax

 ax.plot(range(10))
 scale = 1.5
 f = zoom_factory(ax,base_scale = scale)

L'argument optionnel base_scale vous permet de définir le facteur d'échelle comme vous le souhaitez. 

assurez-vous de conserver une copie de f. Le rappel utilise une référence faible. Par conséquent, si vous ne conservez pas une copie de f, il est possible qu’elle soit récupérée. 

Après avoir écrit cette réponse, j’ai décidé que c’était plutôt utile et je l’ai mise dans un Gist

20
tacaswell

Merci les gars, les exemples ont été très utiles. J'ai dû faire quelques modifications pour travailler avec un nuage de points et j'ai ajouté un panoramique avec un glisser du bouton gauche. Espérons que quelqu'un trouvera cela utile. 

from matplotlib.pyplot import figure, show
import numpy

class ZoomPan:
    def __init__(self):
        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


    def zoom_factory(self, ax, base_scale = 2.):
        def zoom(event):
            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()

        fig = ax.get_figure() # get the figure of interest
        fig.canvas.mpl_connect('scroll_event', zoom)

        return zoom

    def pan_factory(self, ax):
        def onPress(event):
            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):
            self.press = None
            ax.figure.canvas.draw()

        def onMotion(event):
            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()

        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


fig = figure()

ax = fig.add_subplot(111, xlim=(0,1), ylim=(0,1), autoscale_on=False)

ax.set_title('Click to zoom')
x,y,s,c = numpy.random.Rand(4,200)
s *= 200

ax.scatter(x,y,s,c)
scale = 1.1
zp = ZoomPan()
figZoom = zp.zoom_factory(ax, base_scale = scale)
figPan = zp.pan_factory(ax)
show()
11
seadoodude
def zoom(self, event, factor):
    curr_xlim = self.ax.get_xlim()
    curr_ylim = self.ax.get_ylim()

    new_width = (curr_xlim[1]-curr_ylim[0])*factor
    new_height= (curr_xlim[1]-curr_ylim[0])*factor

    relx = (curr_xlim[1]-event.xdata)/(curr_xlim[1]-curr_xlim[0])
    rely = (curr_ylim[1]-event.ydata)/(curr_ylim[1]-curr_ylim[0])

    self.ax.set_xlim([event.xdata-new_width*(1-relx),
                event.xdata+new_width*(relx)])
    self.ax.set_ylim([event.ydata-new_width*(1-rely),
                        event.ydata+new_width*(rely)])
    self.draw()

Ce code légèrement modifié a pour but de garder une trace de la position du curseur par rapport au nouveau centre du zoom. Ainsi, si vous effectuez un zoom avant ou arrière sur l’image en un point autre que le centre, vous restez sur le même point. 

4
RodericDay

Merci beaucoup. Cela a très bien fonctionné. Toutefois, pour les tracés dont l’échelle n’est plus linéaire (tracés logarithmiques, par exemple), cela se décompose. J'ai écrit une nouvelle version pour cela. J'espère que ça aide quelqu'un.

En gros, j'agrandis les coordonnées des axes normalisés à [0,1]. Donc, si je fais un zoom avant par deux en x, je veux maintenant être dans la plage [.25, .75]. J'ai aussi ajouté une fonction pour zoomer uniquement x si vous êtes directement au-dessus ou en dessous de l'axe des x et uniquement en y si vous êtes directement à gauche ou à droite de l'axe des y. Si vous n'en avez pas besoin, définissez simplement zoomx = True et zoomy = True et ignorez les instructions if.

Cette référence est très utile pour ceux qui veulent comprendre comment matplotlib se transforme entre différents systèmes de coordonnées: http://matplotlib.org/users/transforms_tutorial.html

Cette fonction se trouve dans un objet contenant un pointeur sur les axes (self.ax).

def zoom(self,event):
    '''This function zooms the image upon scrolling the mouse wheel.
    Scrolling it in the plot zooms the plot. Scrolling above or below the
    plot scrolls the x axis. Scrolling to the left or the right of the plot
    scrolls the y axis. Where it is ambiguous nothing happens. 
    NOTE: If expanding figure to subplots, you will need to add an extra
    check to make sure you are not in any other plot. It is not clear how to
    go about this.
    Since we also want this to work in loglog plot, we work in axes
    coordinates and use the proper scaling transform to convert to data
    limits.'''

    x = event.x
    y = event.y

    #convert pixels to axes
    tranP2A = self.ax.transAxes.inverted().transform
    #convert axes to data limits
    tranA2D= self.ax.transLimits.inverted().transform
    #convert the scale (for log plots)
    tranSclA2D = self.ax.transScale.inverted().transform

    if event.button == 'down':
        # deal with zoom in
        scale_factor = self.zoom_scale
    Elif event.button == 'up':
        # deal with zoom out
        scale_factor = 1 / self.zoom_scale
    else:
        # deal with something that should never happen
        scale_factor = 1

    #get my axes position to know where I am with respect to them
    xa,ya = tranP2A((x,y))
    zoomx = False
    zoomy = False 
    if(ya < 0):
        if(xa >= 0 and xa <= 1):
            zoomx = True
            zoomy = False
    Elif(ya <= 1):
        if(xa <0): 
            zoomx = False
            zoomy = True
        Elif(xa <= 1):
            zoomx = True
            zoomy = True
        else:
            zoomx = False
            zoomy = True
    else:
        if(xa >=0 and xa <= 1):
            zoomx = True
            zoomy = False

    new_alimx = (0,1)
    new_alimy = (0,1)
    if(zoomx):
        new_alimx = (np.array([1,1]) + np.array([-1,1])*scale_factor)*.5
    if(zoomy):
        new_alimy = (np.array([1,1]) + np.array([-1,1])*scale_factor)*.5

    #now convert axes to data
    new_xlim0,new_ylim0 = tranSclA2D(tranA2D((new_alimx[0],new_alimy[0])))
    new_xlim1,new_ylim1 = tranSclA2D(tranA2D((new_alimx[1],new_alimy[1])))

    #and set limits
    self.ax.set_xlim([new_xlim0,new_xlim1])
    self.ax.set_ylim([new_ylim0,new_ylim1])
    self.redraw()
2
julienl

J'aime beaucoup les modes "x only" ou "y only" dans les diagrammes de figures. Vous pouvez relier les touches x et y pour que le zoom ne se produise que dans une direction. Notez que vous devrez peut-être aussi remettre le focus sur la toile si vous cliquez sur une zone de saisie ou quelque chose - 

canvas.mpl_connect('button_press_event', lambda event:canvas._tkcanvas.focus_set())

Le reste du code modifié est ci-dessous:

from matplotlib.pyplot import figure, show
import numpy

class ZoomPan:
    def __init__(self):
        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.xzoom = True
        self.yzoom = True
        self.cidBP = None
        self.cidBR = None
        self.cidBM = None
        self.cidKeyP = None
        self.cidKeyR = None
        self.cidScroll = None

    def zoom_factory(self, ax, base_scale = 2.):
        def zoom(event):
            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(xdata is None):
                return()
            if(ydata is None):
                return()

            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])

            if(self.xzoom):
                ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
            if(self.yzoom):
                ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
            ax.figure.canvas.draw()
            ax.figure.canvas.flush_events()

        def onKeyPress(event):
            if event.key == 'x':
                self.xzoom = True
                self.yzoom = False
            if event.key == 'y':
                self.xzoom = False
                self.yzoom = True

        def onKeyRelease(event):
            self.xzoom = True
            self.yzoom = True

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

        self.cidScroll = fig.canvas.mpl_connect('scroll_event', zoom)
        self.cidKeyP = fig.canvas.mpl_connect('key_press_event',onKeyPress)
        self.cidKeyR = fig.canvas.mpl_connect('key_release_event',onKeyRelease)

        return zoom

    def pan_factory(self, ax):
        def onPress(event):
            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):
            self.press = None
            ax.figure.canvas.draw()

        def onMotion(event):
            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.figure.canvas.flush_events()

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

        self.cidBP = fig.canvas.mpl_connect('button_press_event',onPress)
        self.cidBR = fig.canvas.mpl_connect('button_release_event',onRelease)
        self.cidBM = fig.canvas.mpl_connect('motion_notify_event',onMotion)
        # attach the call back

        #return the function
        return onMotion
2
johnml1135

Ceci est une suggestion pour une légère modification du code ci-dessus - cela permet de garder le zoom centré plus facile à gérer.

    cur_xrange = (cur_xlim[1] - cur_xlim[0])*.5
    cur_yrange = (cur_ylim[1] - cur_ylim[0])*.5
    xmouse = event.xdata # get event x location                                                                                                                                                                                                                            
    ymouse = event.ydata # get event y location                                                                                                                                                                                                                            
    cur_xcentre = (cur_xlim[1] + cur_xlim[0])*.5
    cur_ycentre = (cur_ylim[1] + cur_ylim[0])*.5
    xdata = cur_xcentre+ 0.25*(xmouse-cur_xcentre)
    ydata = cur_ycentre+ 0.25*(ymouse-cur_ycentre)
1
Robbie