2012-07-18 170 views
22

当光标悬停在matplotlib图上时,是否可以绑定滚轮以放大/缩小?使用滚轮缩放Matplotlib plot

+0

您可以编写一个回调函数做http://matplotlib.sourceforge.net/api/backend_bases_api.html?highlight=mpl_connect#matplotlib.backend_bases.FigureCanvasBase.mpl_connect – tacaswell 2012-07-18 22:39:55

+0

任何的例子? – dimka 2012-07-18 22:42:35

回答

17

这应该工作。当您滚动时,它将图形重新居中在指针位置上。

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 

假设你有一个轴对象ax

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

可选参数base_scale允许您设置比例因子是你什么都想要。

请确保您保留了f的副本。回电使用弱引用,所以如果你不保留副本f它可能会被垃圾收集。

写这个答案后,我决定这其实非常有用,并把它放在一个gist

+0

我也是独立做的! 我希望我早点检查过SO。我也想贡献一下。 – RodericDay 2012-10-08 14:51:14

+1

@RodericDay你可以抓住要点,让它变得更好 – tacaswell 2012-10-08 15:08:44

+0

我不是在提交真实代码给其他人使用的阶段,但是我会在下面的情况下推荐一个修正,以防用户对相对坐标感兴趣 – RodericDay 2012-10-09 04:54:49

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

的这种稍微改变代码的目的是跟踪相对光标的位置到新的缩放中心。这样,如果您在中心以外的位置放大和缩小图片,则可以保持在同一点上。

10

谢谢你们,这些例子非常有帮助。我不得不做一些改变来处理散点图,并用左键拖动来添加平移。希望有人会觉得这很有用。

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

非常感谢。这很好。但是,对于比例尺不再是线性的地块(例如日志图),这会发生故障。我为此写了一个新版本。我希望它能帮助别人。

基本上,我放大了标准化为[0,1]的轴坐标。所以,如果我用x放大两倍,我现在想要在[.25,.75]范围内。 我还添加了一个功能,如果您直接位于x轴的上方或下方,则只放大x;如果您直接位于y轴的左侧或右侧,则只在y中放大。如果你不需要这个,只需设置zoomx = True和zoomy = True并忽略if语句。

此引用的,谁想要了解matplotlib不同的坐标系之间是如何将是非常有用的:http://matplotlib.org/users/transforms_tutorial.html

此函数是一个包含一个指向轴(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

您可以向上游提交吗?应该在https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backend_tools.py#L625 – tacaswell 2015-08-20 00:30:00

+0

左右。 https://github.com/matplotlib/matplotlib/pull/4970 第一次这样做,所以让我知道是否应该做些什么,或者可以做得更好。谢谢! – julienl 2015-08-20 05:15:06

2

我真的很喜欢图中的“x only”或“y only”模式。您可以绑定x和y键,以便缩放只发生在一个方向上。请注意,您可能还需要将焦点放回在画布上,如果你点击一个条目框或东西 -

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

修改后的代码的其余部分是如下:

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

这是对上述代码进行细微修改的建议 - 它使得缩放居中更易于管理。

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)