2012-07-18 30 views
22

¿Es posible enlazar la rueda de desplazamiento para acercar/alejar cuando el cursor se desplaza sobre un gráfico matplotlib?Gráfico Matplotlib zoom con rueda de desplazamiento

+0

puede escribir una función de devolución de llamada para hacer que los http://matplotlib.sourceforge.net/api/backend_bases_api.html?highlight=mpl_connect#matplotlib.backend_bases.FigureCanvasBase.mpl_connect – tacaswell

+0

ningún ejemplo de esto? – dimka

Respuesta

17

Esto debería funcionar. Reubica el gráfico en la ubicación del puntero cuando se desplaza.

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 

Asumiendo que tiene un objeto eje ax

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

El argumento opcional base_scale le permite establecer el factor de escala para ser cualquier cosa que quiera.

asegúrese de mantener una copia de f alrededor. La devolución de llamada utiliza un ref débil, por lo que si no conserva una copia de f, puede ser un recolector de basura.

Después de escribir esta respuesta decidí esto en realidad bastante útil y lo puso en un gist

+0

¡Yo también hice esto independientemente! Ojalá hubiera verificado antes. Me gustaría haber contribuido también. – RodericDay

+1

@RodericDay Usted puede agarrar la esencia y hacerlo mejor – tacaswell

+0

no estoy en la etapa donde presento código real que hay para que otras personas usen, pero voy a recomendar una solución a continuación en caso de que el usuario está interesado en coordenadas relativas – 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() 

El propósito de este código ligeramente alterada es hacer un seguimiento de la posición del cursor en relación con el nuevo centro de zoom . De esta forma, si amplía y aleja la imagen en puntos distintos al centro, permanece en el mismo punto.

10

Gracias chicos, los ejemplos fueron muy útiles. Tuve que hacer algunos cambios para trabajar con un diagrama de dispersión y agregué el barrido con un botón izquierdo. Con suerte, alguien encontrará esto útil.

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

Muchas gracias. Esto funcionó muy bien. Sin embargo, para las parcelas donde la escala ya no es lineal (parcelas de registro, por ejemplo), esto se descompone. He escrito una nueva versión para esto. Espero que esto ayude a alguien.

Básicamente, acerco las coordenadas de los ejes que están normalizadas a ser [0,1]. Entonces, si amplío dos en x, ahora quiero estar en el rango [.25, .75]. También he agregado una función para ampliar solo x si estás directamente encima o debajo del eje x, y solo en y si estás directamente a la izquierda o derecha en el eje y. Si no necesita esto, simplemente configure zoomx = True y zoomy = True e ignore las declaraciones if.

Esta referencia es muy útil para aquellos que quieren entender cómo se transforma matplotlib entre los diferentes sistemas de coordenadas: http://matplotlib.org/users/transforms_tutorial.html

Esta función está dentro de un objeto que contiene un puntero a los ejes (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

¿Puede enviar esta corriente ascendente? Debe haber un parche en https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backend_tools.py#L625 – tacaswell

+0

hecho. https://github.com/matplotlib/matplotlib/pull/4970 por primera vez haciendo esto, así que avíseme si hay algo que debería haber hecho, o podría haberlo hecho mejor. ¡Gracias! – julienl

2

Me gustan mucho los modos "x solo" o "solo y solo" en los diagramas de figuras. Puede enlazar las teclas x e y para que el zoom solo ocurra en una dirección. Tenga en cuenta que también puede tener que colocar el foco de nuevo en el lienzo si hace clic en un cuadro de entrada o algo -

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

El resto del código modificado es el siguiente:

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

Este es una sugerencia para una ligera modificación del código anterior: hace que mantener el zoom centrado sea más manejable.

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) 
Cuestiones relacionadas