2012-07-18 301 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)