事件处理和挑选#

Matplotlib 与许多用户界面工具包(wxpython、tkinter、qt、gtk 和 macosx)一起使用,为了支持图形的交互式平移和缩放等功能,开发人员有一个与图形交互的 API 很有帮助通过“GUI 中性”的按键和鼠标移动,我们不必在不同的用户界面上重复大量代码。尽管事件处理 API 是 GUI 中立的,但它基于 GTK 模型,这是 Matplotlib 支持的第一个用户界面。与标准 GUI 事件相比,触发的事件也比 Matplotlib 更丰富,包括 Axes事件发生的位置等信息。这些事件还了解 Matplotlib 坐标系,并以像素和数据坐标报告事件位置。

事件连接#

要接收事件,您需要编写一个回调函数,然后将您的函数连接到事件管理器,它是 FigureCanvasBase. 这是一个简单的示例,它打印鼠标单击的位置以及按下了哪个按钮:

fig, ax = plt.subplots()
ax.plot(np.random.rand(10))

def onclick(event):
    print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
          ('double' if event.dblclick else 'single', event.button,
           event.x, event.y, event.xdata, event.ydata))

cid = fig.canvas.mpl_connect('button_press_event', onclick)

FigureCanvasBase.mpl_connect方法返回一个连接id(一个整数),可用于断开回调通过

fig.canvas.mpl_disconnect(cid)

笔记

画布仅保留对用作回调的实例方法的弱引用。因此,您需要保留对拥有此类方法的实例的引用。否则,实例将被垃圾收集,回调将消失。

这不会影响用作回调的自由函数。

以下是您可以连接的事件、事件发生时发回给您的类实例以及事件描述:

活动名称

班级

描述

'button_press_event'

MouseEvent

鼠标按钮被按下

'button_release_event'

MouseEvent

鼠标按钮被释放

“关闭事件”

CloseEvent

图已关闭

'draw_event'

DrawEvent

画布已绘制(但屏幕小部件尚未更新)

'key_press_event'

KeyEvent

键被按下

'key_release_event'

KeyEvent

钥匙被释放

'motion_notify_event'

MouseEvent

鼠标移动

'pick_event'

PickEvent

画布中的艺术家被选中

“调整大小事件”

ResizeEvent

图形画布已调整大小

“滚动事件”

MouseEvent

鼠标滚轮滚动

'figure_enter_event'

LocationEvent

鼠标进入新图形

'figure_leave_event'

LocationEvent

老鼠留下一个身影

'axes_enter_event'

LocationEvent

鼠标进入一个新的轴

'axes_leave_event'

LocationEvent

鼠标离开轴

笔记

当连接到 'key_press_event' 和 'key_release_event' 事件时,您可能会遇到 Matplotlib 使用的不同用户界面工具包之间的不一致。这是由于用户界面工具包的不一致/限制造成的。下表显示了您可能期望从不同的用户界面工具包中作为键(使用 QWERTY 键盘布局)接收的一些基本示例,其中逗号分隔不同的键:

按下的键

WxPython

Qt

网络聚合

gtk

特金特

macosx

Shift+2

班次,班次+2

转移, @

转移, @

转移, @

转移, @

转移, @

Shift+F1

换档,换档+f1

换档,换档+f1

换档,换档+f1

换档,换档+f1

换档,换档+f1

换档,换档+f1

转移

转移

转移

转移

转移

转移

转移

控制

控制

控制

控制

控制

控制

控制

Alt

alt

alt

alt

alt

alt

alt

AltGr

没有什么

没有什么

alt

iso_level3_shift

iso_level3_shift

大写锁定

大写锁定

大写锁定

大写锁定

大写锁定

大写锁定

大写锁定

大写锁定+a

大写锁定,一个

大写锁定,一个

大写锁定,一个

大写锁定,一个

大写锁定,一个

大写锁定,一个

一个

一个

一个

一个

一个

一个

一个

Shift+a

班次,一个

班次,一个

班次,一个

班次,一个

班次,一个

班次,一个

大写锁定+Shift+a

大写锁定,移位,A

大写锁定,移位,A

大写锁定,移位,一个

大写锁定,移位,一个

大写锁定,移位,一个

大写锁定,移位,A

Ctrl+Shift+Alt

控制, ctrl+shift, ctrl+alt

控制, ctrl+shift, ctrl+meta

控制, ctrl+shift, ctrl+meta

控制, ctrl+shift, ctrl+meta

控制, ctrl+shift, ctrl+meta

控制, ctrl+shift, ctrl+alt+shift

Ctrl+Shift+a

控制, ctrl+shift, ctrl+A

控制, ctrl+shift, ctrl+A

控制, ctrl+shift, ctrl+A

控制, ctrl+shift, ctrl+A

控制, ctrl+shift, ctrl+a

控制, ctrl+shift, ctrl+A

F1

f1

f1

f1

f1

f1

f1

Ctrl+F1

控制, ctrl+f1

控制, ctrl+f1

控制, ctrl+f1

控制, ctrl+f1

控制, ctrl+f1

控制,无

Matplotlib 默认附加一些按键回调以实现交互性;它们记录在导航键盘快捷键部分。

事件属性#

所有 Matplotlib 事件都继承自 matplotlib.backend_bases.Event存储属性的基类:

name

事件名称

canvas

生成事件的 FigureCanvas 实例

guiEvent

触发 Matplotlib 事件的 GUI 事件

作为事件处理基础的最常见事件是按键/释放事件和鼠标按下/释放和移动事件。处理这些事件的KeyEventMouseEvent类都派生自LocationEvent,它具有以下属性

x,y

鼠标从画布左侧和底部开始的 x 和 y 位置(以像素为单位)

inaxes

鼠标所在的Axes实例(如果有);其他 无

xdata,ydata

数据坐标中的鼠标 x 和 y 位置(如果鼠标位于轴上)

让我们看一个简单的画布示例,每次按下鼠标时都会创建一个简单的线段:

from matplotlib import pyplot as plt

class LineBuilder:
    def __init__(self, line):
        self.line = line
        self.xs = list(line.get_xdata())
        self.ys = list(line.get_ydata())
        self.cid = line.figure.canvas.mpl_connect('button_press_event', self)

    def __call__(self, event):
        print('click', event)
        if event.inaxes!=self.line.axes: return
        self.xs.append(event.xdata)
        self.ys.append(event.ydata)
        self.line.set_data(self.xs, self.ys)
        self.line.figure.canvas.draw()

fig, ax = plt.subplots()
ax.set_title('click to build line segments')
line, = ax.plot([0], [0])  # empty line
linebuilder = LineBuilder(line)

plt.show()

MouseEvent我们刚刚使用的是 a ,因此我们可以通过和LocationEvent访问数据和像素坐标。除了属性,它还有(event.x, event.y)(event.xdata, event.ydata)LocationEvent

button

按下的按钮:无、、MouseButton“向上”或“向下”(向上和向下用于滚动事件)

key

按下的键:无、任何字符、“shift”、“win”或“control”

可拖动矩形练习#

Rectangle编写使用实例初始化但在拖动时会移动其xy 位置的可拖动矩形类 。提示:您需要将 xy矩形的原始位置存储为 rect.xy 并连接到按下、移动和释放鼠标事件。按下鼠标时,检查单击是否发生在您的矩形上(请参阅 参考资料 Rectangle.contains),如果是,则将矩形 xy 和鼠标单击的位置存储在数据坐标中。在运动事件回调中,计算鼠标移动的 deltax 和 deltay,并将这些 deltas 添加到您存储的矩形的原点。重画图。在按钮释放事件中,只需将您存储的所有按钮按下数据重置为无。

这是解决方案:

import numpy as np
import matplotlib.pyplot as plt

class DraggableRectangle:
    def __init__(self, rect):
        self.rect = rect
        self.press = None

    def connect(self):
        """Connect to all the events we need."""
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        """Check whether mouse is over us; if so, store some data."""
        if event.inaxes != self.rect.axes:
            return
        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print('event contains', self.rect.xy)
        self.press = self.rect.xy, (event.xdata, event.ydata)

    def on_motion(self, event):
        """Move the rectangle if the mouse is over us."""
        if self.press is None or event.inaxes != self.rect.axes:
            return
        (x0, y0), (xpress, ypress) = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        # print(f'x0={x0}, xpress={xpress}, event.xdata={event.xdata}, '
        #       f'dx={dx}, x0+dx={x0+dx}')
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        self.rect.figure.canvas.draw()

    def on_release(self, event):
        """Clear button press information."""
        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        """Disconnect all callbacks."""
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

fig, ax = plt.subplots()
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
    dr = DraggableRectangle(rect)
    dr.connect()
    drs.append(dr)

plt.show()

额外功劳:使用 blitting 使动画绘图更快、更流畅。

额外信用解决方案:

# Draggable rectangle with blitting.
import numpy as np
import matplotlib.pyplot as plt

class DraggableRectangle:
    lock = None  # only one can be animated at a time

    def __init__(self, rect):
        self.rect = rect
        self.press = None
        self.background = None

    def connect(self):
        """Connect to all the events we need."""
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        """Check whether mouse is over us; if so, store some data."""
        if (event.inaxes != self.rect.axes
                or DraggableRectangle.lock is not None):
            return
        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print('event contains', self.rect.xy)
        self.press = self.rect.xy, (event.xdata, event.ydata)
        DraggableRectangle.lock = self

        # draw everything but the selected rectangle and store the pixel buffer
        canvas = self.rect.figure.canvas
        axes = self.rect.axes
        self.rect.set_animated(True)
        canvas.draw()
        self.background = canvas.copy_from_bbox(self.rect.axes.bbox)

        # now redraw just the rectangle
        axes.draw_artist(self.rect)

        # and blit just the redrawn area
        canvas.blit(axes.bbox)

    def on_motion(self, event):
        """Move the rectangle if the mouse is over us."""
        if (event.inaxes != self.rect.axes
                or DraggableRectangle.lock is not self):
            return
        (x0, y0), (xpress, ypress) = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        canvas = self.rect.figure.canvas
        axes = self.rect.axes
        # restore the background region
        canvas.restore_region(self.background)

        # redraw just the current rectangle
        axes.draw_artist(self.rect)

        # blit just the redrawn area
        canvas.blit(axes.bbox)

    def on_release(self, event):
        """Clear button press information."""
        if DraggableRectangle.lock is not self:
            return

        self.press = None
        DraggableRectangle.lock = None

        # turn off the rect animation property and reset the background
        self.rect.set_animated(False)
        self.background = None

        # redraw the full figure
        self.rect.figure.canvas.draw()

    def disconnect(self):
        """Disconnect all callbacks."""
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

fig, ax = plt.subplots()
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
    dr = DraggableRectangle(rect)
    dr.connect()
    drs.append(dr)

plt.show()

鼠标进出#

如果您想在鼠标进入或离开图形或轴时收到通知,您可以连接到图形/轴进入/离开事件。这是一个简单的例子,它改变了鼠标悬停的坐标轴和图形背景的颜色:

"""
Illustrate the figure and axes enter and leave events by changing the
frame colors on enter and leave
"""
import matplotlib.pyplot as plt

def enter_axes(event):
    print('enter_axes', event.inaxes)
    event.inaxes.patch.set_facecolor('yellow')
    event.canvas.draw()

def leave_axes(event):
    print('leave_axes', event.inaxes)
    event.inaxes.patch.set_facecolor('white')
    event.canvas.draw()

def enter_figure(event):
    print('enter_figure', event.canvas.figure)
    event.canvas.figure.patch.set_facecolor('red')
    event.canvas.draw()

def leave_figure(event):
    print('leave_figure', event.canvas.figure)
    event.canvas.figure.patch.set_facecolor('grey')
    event.canvas.draw()

fig1, axs = plt.subplots(2)
fig1.suptitle('mouse hover over figure or axes to trigger events')

fig1.canvas.mpl_connect('figure_enter_event', enter_figure)
fig1.canvas.mpl_connect('figure_leave_event', leave_figure)
fig1.canvas.mpl_connect('axes_enter_event', enter_axes)
fig1.canvas.mpl_connect('axes_leave_event', leave_axes)

fig2, axs = plt.subplots(2)
fig2.suptitle('mouse hover over figure or axes to trigger events')

fig2.canvas.mpl_connect('figure_enter_event', enter_figure)
fig2.canvas.mpl_connect('figure_leave_event', leave_figure)
fig2.canvas.mpl_connect('axes_enter_event', enter_axes)
fig2.canvas.mpl_connect('axes_leave_event', leave_axes)

plt.show()

对象拾取#

picker您可以通过设置一个Artist(例如Line2D, Text, Patch, Polygon, AxesImage, 等)的属性来启用拾取。

可以使用picker各种类型设置属性:

None

此艺术家禁用挑选(默认)。

boolean

如果为 True,则拾取将被启用,如果鼠标事件在艺术家上方,艺术家将触发一个拾取事件。

callable

如果选择器是可调用的,则它是用户提供的函数,用于确定艺术家是否被鼠标事件击中。签名是确定命中测试。如果鼠标事件在艺术家上方,则返回;是一个属性字典,这些属性成为.hit, props = picker(artist, mouseevent)hit = TruepropsPickEvent

艺术家的pickradius属性还可以设置为以点为单位的容差值(每英寸有 72 个点),该值决定了鼠标可以走多远并且仍然触发鼠标事件。

通过设置属性启用艺术家进行拾取后picker ,您需要将处理程序连接到图形画布 pick_event 以获取鼠标按下事件的拾取回调。处理程序通常看起来像

def pick_handler(event):
    mouseevent = event.mouseevent
    artist = event.artist
    # now do something with this...

传递给您的PickEvent回调始终具有以下属性:

mouseevent

MouseEvent生成选择事件的。有关鼠标事件的有用属性列表,请参见event-attributes

artist

Artist生成选择事件的。

此外,某些艺术家喜欢Line2D并且PatchCollection可能附加额外的元数据,例如满足选取器标准的数据索引(例如,在指定pickradius容差范围内的线中的所有点)。

简单的挑选示例#

在下面的示例中,我们启用在线拾取并以点为单位设置拾取半径公差。当onpick 拾取事件在距离线的公差距离内时,回调函数将被调用,并且具有在拾取距离公差内的数据顶点的索引。我们的onpick 回调函数只是打印选择位置下的数据。不同的 Matplotlib 艺术家可以将不同的数据附加到 PickEvent。例如,Line2D附加 ind 属性,它是拾取点下线数据的索引。有关 该行的属性的Line2D.pick详细信息,请参阅。PickEvent

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), 'o',
                picker=True, pickradius=5)  # 5 points tolerance

def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    points = tuple(zip(xdata[ind], ydata[ind]))
    print('onpick points:', points)

fig.canvas.mpl_connect('pick_event', onpick)

plt.show()

采摘练习#

创建一个由 100 个包含 1000 个高斯随机数的数组组成的数据集,并计算每个数组的样本均值和标准差(提示:NumPy 数组具有均值和标准差方法)并绘制 100 个均值与 100 个均值的 xy 标记图标准偏差。将 plot 命令创建的线连接到 pick 事件,并绘制生成点击点的数据的原始时间序列。如果多个点在单击点的容差范围内,您可以使用多个子图来绘制多个时间序列。

运动解决方案:

"""
Compute the mean and stddev of 100 data sets and plot mean vs. stddev.
When you click on one of the (mean, stddev) points, plot the raw dataset
that generated that point.
"""

import numpy as np
import matplotlib.pyplot as plt

X = np.random.rand(100, 1000)
xs = np.mean(X, axis=1)
ys = np.std(X, axis=1)

fig, ax = plt.subplots()
ax.set_title('click on point to plot time series')
line, = ax.plot(xs, ys, 'o', picker=True, pickradius=5)  # 5 points tolerance


def onpick(event):
    if event.artist != line:
        return
    n = len(event.ind)
    if not n:
        return
    fig, axs = plt.subplots(n, squeeze=False)
    for dataind, ax in zip(event.ind, axs.flat):
        ax.plot(X[dataind])
        ax.text(0.05, 0.9,
                f"$\\mu$={xs[dataind]:1.3f}\n$\\sigma$={ys[dataind]:1.3f}",
                transform=ax.transAxes, verticalalignment='top')
        ax.set_ylim(-0.5, 1.5)
    fig.show()
    return True


fig.canvas.mpl_connect('pick_event', onpick)
plt.show()