【问题标题】:Matplotlib plot zooming with scroll wheelMatplotlib 绘图用滚轮缩放
【发布时间】:2012-07-18 01:25:58
【问题描述】:

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

【问题讨论】:

标签: user-interface plot matplotlib zooming


【解决方案1】:

这应该可行。当您滚动时,它会将图形重新置于指针位置的中心。

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

【讨论】:

  • 我也是独立完成的!我希望我早点检查过。我也想贡献一份力量。
  • @RodericDay 你可以抓住要点,让它变得更好
  • 我还没到提交真实代码供其他人使用的阶段,但如果用户对相对坐标感兴趣,我会在下面推荐一个修复方法
  • 为了获得更好的用户体验(类似于 CAD),请使用以下新限制:ax.set_xlim([xdata - (xdata-cur_xlim[0]) / scale_factor, xdata + (cur_xlim[1]-xdata) / scale_factor])ax.set_ylim([ydata - (ydata-cur_ylim[0]) / scale_factor, ydata + (cur_ylim[1]-ydata) / scale_factor])。有了这些限制,指针下方的位置始终保持固定,让您更好地感受/了解您正在缩放的​​内容
  • 在设置限制之前调用ax.figure.canvas.toolbar.push_current() 将解决主页按钮的问题
【解决方案2】:

谢谢大家,这些例子很有帮助。我必须进行一些更改才能使用散点图,并使用左键拖动添加平移。希望有人会发现这很有用。

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

【讨论】:

    【解决方案3】:
    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()
    

    这个稍加改动的代码的目的是跟踪光标相对于新缩放中心的位置。这样,如果您在中心以外的点放大和缩小图片,您将保持在同一点。

    【讨论】:

      【解决方案4】:

      非常感谢。这很好用。但是,对于比例不再是线性的图(例如对数图),这会失效。我为此写了一个新版本。我希望它可以帮助某人。

      基本上,我放大了标准化为 [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()
      

      【讨论】:

      【解决方案5】:

      我真的很喜欢图中的“仅 x”或“仅 y”模式。您可以绑定 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
      

      【讨论】:

        【解决方案6】:

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

            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)
        

        【讨论】:

          【解决方案7】:

          据我所知,还有另一种方法。偶然我遇到了Axis.zoom 方法。我不知道这是否更快或一般来说是一种好方法,但它确实有效,而且代码肯定更少:

              def __init(self):
                  ...
          
                  self.cid_zoom = self.canvas.mpl_connect('scroll_event', self.zoom)
          
              def zoom(self, event):
                  if event.inaxes == self.ax:
                      scale_factor = np.power(self.zoom_factor, -event.step)*event.step
                      self.ax.get_xaxis().zoom(scale_factor)
                      self.ax.get_yaxis().zoom(scale_factor)
                      self.ax.invert_yaxis()
                      self.canvas.draw_idle()
          

          如果您绘制图像,由于某种原因,您必须再次反转 y 轴。

          您也可以通过这种方式实现panning,但效果并不理想。我不知道为什么:

              def __init(self):
                  ...
          
                  self.cid_motion = self.canvas.mpl_connect(
                      'motion_notify_event', self.pan_move
                  )
                  self.cid_button = self.canvas.mpl_connect(
                      'button_press_event', self.pan_press
                  )
          
          
              def pan_press(self, event):
                  if event.inaxes == self.ax:
                      self.x_press = event.xdata
                      self.y_press = event.ydata
          
              def pan_move(self, event):
                  if event.button == 1 and event.inaxes == self.ax:
                      xdata = event.xdata
                      ydata = event.ydata
                      dx = (xdata - self.x_press)/np.diff(self.ax.get_xlim())
                      dy = (ydata - self.y_press)/np.diff(self.ax.get_ylim())
                      self.ax.get_xaxis().pan(-dx)
                      self.ax.get_yaxis().pan(-dy)
                      self.ax.drag_pan(event.button, event.key, dx, dy)
                      self.canvas.draw()
          

          【讨论】:

            【解决方案8】:

            让 tacaswell 的回答“顺利”

            def zoom_factory(ax, base_scale=2.):
                prex = 0
                prey = 0
                prexdata = 0
                preydata = 0
            
                def zoom_fun(event):
                    nonlocal prex, prey, prexdata, preydata
                    curx = event.x
                    cury = event.y
            
                    # if not changed mouse position(or changed so little)
                    # remain the pre scale center
                    if abs(curx - prex) < 10 and abs(cury - prey) < 10:
                        # remain same
                        xdata = prexdata
                        ydata = preydata
                    # if changed mouse position ,also change the cur scale center
                    else:
                        # change
                        xdata = event.xdata  # get event x location
                        ydata = event.ydata  # get event y location
            
                        # update previous location data
                        prex = event.x
                        prey = event.y
                        prexdata = xdata
                        preydata = ydata
            
                    # 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
            
                    # log.debug((xdata, ydata))
                    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
            

            【讨论】:

              【解决方案9】:

              使用ax.set_xlim()ax.set_ylim() 的其他答案对于设置轴速度较慢的图形并没有提供令人满意的用户体验。 (对我来说,这是一个带有 pcolormesh 的轴)ax.drag_pan() 方法要快得多,我相信它更适合大多数情况:

              def mousewheel_move( event):
                  ax=event.inaxes
                  ax._pan_start = types.SimpleNamespace(
                          lim=ax.viewLim.frozen(),
                          trans=ax.transData.frozen(),
                          trans_inverse=ax.transData.inverted().frozen(),
                          bbox=ax.bbox.frozen(),
                          x=event.x,
                          y=event.y)
                  if event.button == 'up':
                      ax.drag_pan(3, event.key, event.x+10, event.y+10)
                  else: #event.button == 'down':
                      ax.drag_pan(3, event.key, event.x-10, event.y-10)
                  fig=ax.get_figure()
                  fig.canvas.draw_idle()
              

              然后将你的图形与:

              fig.canvas.mpl_connect('scroll_event',mousewheel_move)
              

              使用 TkAgg 后端和 python 3.6 使用 matplotlib 3.0.2 进行测试

              【讨论】:

                猜你喜欢
                • 2020-10-07
                • 1970-01-01
                • 2014-04-05
                • 1970-01-01
                • 1970-01-01
                • 2014-07-26
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多