Matplotlib 绘图用滚轮缩放

Posted

技术标签:

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

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

【问题讨论】:

你可以写一个回调函数来做到这一点matplotlib.sourceforge.net/api/… 【参考方案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()

【讨论】:

你能提交这个上游吗?应该在github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/…附近打补丁 完成。 github.com/matplotlib/matplotlib/pull/4970 第一次这样做,如果有什么我应该做的,或者可以做得更好,请告诉我。谢谢!【参考方案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 进行测试

【讨论】:

以上是关于Matplotlib 绘图用滚轮缩放的主要内容,如果未能解决你的问题,请参考以下文章

matplot绘图

Python之神奇的绘图库matplotlib

python matplot怎么设置rgb

python matplot怎么画箭头

Matplotlib利用Python进行绘图

Python 绘图包 Matplotlib Pyplot 教程