2012-07-18 18 views
22

È possibile associare la rotella di scorrimento a zoom in/out quando il cursore passa con il mouse su un grafico matplotlib?Zoom con trama di plastilina con rotella di scorrimento

+0

È possibile scrivere una funzione di chiamata indietro per farlo http://matplotlib.sourceforge.net/api/backend_bases_api.html?highlight=mpl_connect#matplotlib.backend_bases.FigureCanvasBase.mpl_connect – tacaswell

+0

eventuali esempi di questo? – dimka

risposta

17

Questo dovrebbe funzionare. Ricalcola il grafico sulla posizione del puntatore quando si scorre.

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 

Supponendo di avere un oggetto asse ax

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

L'argomento opzionale base_scale consente di impostare il fattore di scala per essere ciò che più desiderate.

assicurarsi di conservare una copia di f in giro. La richiamata utilizza un riferimento debole, quindi se non si mantiene una copia di f potrebbe essere raccolta dati inutili.

Dopo aver scritto questa risposta ho deciso che questo in realtà molto utile e metterlo in un gist

+0

Ho fatto anche questo in modo indipendente! Vorrei aver controllato SO in precedenza. Mi piacerebbe anche aver contribuito. – RodericDay

+1

@RodericDay Puoi afferrare l'essenza e renderla migliore – tacaswell

+0

Non sono nella fase in cui invio il codice reale là fuori per altre persone, ma ti suggerirò una correzione sotto nel caso in cui l'utente sia interessato alle coordinate relative – RodericDay

4
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() 

Lo scopo di questo codice leggermente modificato è quello di tenere traccia della posizione del cursore rispetto al nuovo centro zoom . In questo modo, se ingrandisci e rimpicciolisci l'immagine in punti diversi dal centro, rimani sullo stesso punto.

10

Grazie ragazzi, gli esempi sono stati molto utili. Ho dovuto apportare alcune modifiche per lavorare con un grafico a dispersione e ho aggiunto il panning con il tasto sinistro del mouse. Spero che qualcuno lo troverà 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() 
2

Grazie mille. Questo ha funzionato alla grande. Tuttavia, per i grafici in cui la scala non è più lineare (per esempio, i grafici dei log), questo si rompe. Ho scritto una nuova versione per questo. Spero che aiuti qualcuno.

Fondamentalmente, zoomare le coordinate degli assi che sono normalizzate per essere [0,1]. Quindi, se ingrandisco di due in x, ora voglio essere nell'intervallo [.25, .75]. Ho anche aggiunto una funzione per ingrandire solo x se sei direttamente sopra o sotto l'asse x, e solo in y se sei direttamente a sinistra o a destra sull'asse y. Se non ti serve, imposta zoomx = True e zoomy = True e ignora le istruzioni if.

La domanda è molto utile per chi vuole comprendere come matplotlib trasforma tra i diversi sistemi di coordinate: http://matplotlib.org/users/transforms_tutorial.html

Questa funzione è all'interno di un oggetto che contiene un puntatore agli assi (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() 
+0

Puoi inviarlo a monte? Dovrebbe essere patch in https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backend_tools.py#L625 – tacaswell

+0

fatto. https://github.com/matplotlib/matplotlib/pull/4970 per la prima volta, quindi fammi sapere se c'è qualcosa che avrei dovuto fare o che avrei potuto fare meglio. Grazie! – julienl

2

Mi piacciono molto le modalità "solo x" o "solo y" nei grafici delle figure. È possibile associare i tasti xey in modo che lo zoom avvenga solo in una direzione. Si noti che si può anche avere per mettere il back focus sulla tela, se si fa clic su una casella di immissione o qualcosa -

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

Il resto del codice modificato è qui sotto:

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 
1

Questo è un suggerimento per una leggera modifica al codice sopra - rende più centrato lo zoom centrato.

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) 
Problemi correlati