事件处理和选择#

Matplotlib 支持多种用户界面工具包(wxpython、tkinter、qt、gtk 和 macOS),为了支持交互式平移和缩放图形等功能,为开发者提供一个通过按键和鼠标移动与图形交互的“GUI 中立”API 会很有帮助,这样我们就不必在不同的用户界面中重复大量代码。尽管事件处理 API 是 GUI 中立的,但它是基于 GTK 模型的,GTK 是 Matplotlib 最初支持的用户界面。触发的事件相对于 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(一个整数),该 ID 可用于通过以下方式断开回调:

fig.canvas.mpl_disconnect(cid)

注意

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

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

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

事件名称

描述

'button_press_event'

MouseEvent

鼠标按钮按下

'button_release_event'

MouseEvent

鼠标按钮松开

'close_event'

CloseEvent

图形关闭

'draw_event'

DrawEvent

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

'key_press_event'

KeyEvent

按键按下

'key_release_event'

KeyEvent

按键松开

'motion_notify_event'

MouseEvent

鼠标移动

'pick_event'

PickEvent

画布中的 artist 被选中

'resize_event'

ResizeEvent

图形画布大小调整

'scroll_event'

MouseEvent

鼠标滚轮滚动

'figure_enter_event'

LocationEvent

鼠标进入新图形

'figure_leave_event'

LocationEvent

鼠标离开图形

'axes_enter_event'

LocationEvent

鼠标进入新 axes

'axes_leave_event'

LocationEvent

鼠标离开 axes

注意

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

按下的键

Tkinter

Qt

macosx

WebAgg

GTK

WxPython

Shift+2

shift, @

shift, @

shift, @

shift, @

shift, @

shift, shift+2

Shift+F1

shift, shift+f1

shift, shift+f1

shift, shift+f1

shift, shift+f1

shift, shift+f1

shift, shift+f1

Shift

shift

shift

shift

shift

shift

shift

Control

control

control

control

control

control

control

Alt

alt

alt

alt

alt

alt

alt

AltGr

iso_level3_shift

alt

iso_level3_shift

CapsLock

caps_lock

caps_lock

caps_lock

caps_lock

caps_lock

caps_lock

CapsLock+a

caps_lock, A

caps_lock, a

caps_lock, a

caps_lock, A

caps_lock, A

caps_lock, a

a

a

a

a

a

a

a

Shift+a

shift, A

shift, A

shift, A

shift, A

shift, A

shift, A

CapsLock+Shift+a

caps_lock, shift, a

caps_lock, shift, A

caps_lock, shift, A

caps_lock, shift, a

caps_lock, shift, a

caps_lock, shift, A

Ctrl+Shift+Alt

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+alt+shift

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+alt

Ctrl+Shift+a

control, ctrl+shift, ctrl+a

control, ctrl+shift, ctrl+A

control, ctrl+shift, ctrl+A

control, ctrl+shift, ctrl+A

control, ctrl+shift, ctrl+A

control, ctrl+shift, ctrl+A

F1

f1

f1

f1

f1

f1

f1

Ctrl+F1

control, ctrl+f1

control, ctrl+f1

control,

control, ctrl+f1

control, ctrl+f1

control, ctrl+f1

Matplotlib 默认附加了一些按键回调以实现交互性;它们在导航键盘快捷键部分有详细说明。

事件属性#

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

name

事件名称

canvas

生成事件的 FigureCanvas 实例

guiEvent

触发 Matplotlib 事件的 GUI 事件

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

x, y

鼠标在画布左下角起算的像素 x 和 y 位置

inaxes

鼠标所在的Axes实例,如果存在;否则为 None

xdata, ydata

鼠标在数据坐标中的 x 和 y 位置,如果鼠标在 axes 上

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

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是一个LocationEvent,因此我们可以通过(event.x, event.y)(event.xdata, event.ydata)访问数据和像素坐标。除了LocationEvent属性之外,它还有:

button

按下的按钮:None、MouseButton>、'up' 或 'down'(up 和 down 用于滚动事件)

key

按下的键:None、任意字符、'shift'、'win' 或 'control'

可拖动矩形练习#

编写一个可拖动矩形类,它以一个Rectangle实例初始化,但在拖动时会移动其xy位置。

提示:你需要存储矩形的原始xy位置,该位置存储为rect.xy,并连接到鼠标按下、移动和释放事件。当鼠标按下时,检查点击是否发生在你的矩形上(参见Rectangle.contains),如果是,则存储矩形 xy 和鼠标点击在数据坐标中的位置。在移动事件回调中,计算鼠标移动的 deltax 和 deltay,并将这些 delta 添加到你存储的矩形原点,然后重绘图形。在按钮释放事件中,只需将你存储的所有按钮按下数据重置为 None。

这是解决方案:

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

对象选择#

您可以通过设置Artist(例如Line2DTextPatchPolygonAxesImage等)的picker属性来启用选择。

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

None

此 artist 的选择功能被禁用(默认)。

布尔值

如果为 True,则启用选择功能,并且如果鼠标事件在 artist 上方,则 artist 将触发一个选择事件。

可调用对象

如果 picker 是一个可调用对象,它是一个用户提供的函数,用于确定 artist 是否被鼠标事件命中。其签名为 hit, props = picker(artist, mouseevent),用于执行命中测试。如果鼠标事件在 artist 上方,则返回 hit = Trueprops 是一个属性字典,这些属性将成为PickEvent的附加属性。

artist 的pickradius属性还可以设置为一个以点为单位的容差值(每英寸72点),它决定了鼠标可以距离多远仍然触发鼠标事件。

在通过设置picker属性启用 artist 进行拾取后,您需要将处理程序连接到图形画布的 pick_event,以在鼠标按下事件上获得拾取回调。处理程序通常如下所示:

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

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

mouseevent

生成拾取事件的MouseEvent。有关鼠标事件的有用属性列表,请参阅event-attributes

artist

生成拾取事件的Artist

此外,某些 artist(如Line2DPatchCollection)可能会附加额外的元数据,例如符合拾取器标准的数据索引(例如,线中在指定pickradius容差内的所有点)。

简单拾取示例#

在下面的例子中,我们在线条上启用了拾取功能,并设置了一个以点为单位的拾取半径容差。onpick回调函数将在拾取事件在线条的容差距离内时被调用,并包含在拾取距离容差内的数据顶点的索引。我们的onpick回调函数只是打印拾取位置下方的数据。不同的 Matplotlib Artist 可以将不同的数据附加到 PickEvent。例如,Line2D附加了 ind 属性,该属性是拾取点下方线条数据的索引。有关线条的PickEvent属性的详细信息,请参阅Line2D.pick

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 数组有 mean 和 std 方法),并绘制100个均值与100个标准差的 xy 标记图。将绘图命令创建的线连接到拾取事件,并绘制生成被点击点的数据的原始时间序列。如果点击点容差范围内有多个点,您可以使用多个子图来绘制多个时间序列。

练习解决方案

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