Matplotlib 事件处理

原文:Event handling and picking

matplotlib 使用了许多用户界面工具包(wxpython,tkinter,qt4,gtk 和 macosx),为了支持交互式平移和缩放图形等功能,拥有一套 API 通过按键和鼠标移动与图形交互,并且『GUI中立』,对开发人员十分有帮助,所以我们不必重复大量的代码来跨不同的用户界面。虽然事件处理 API 是 GUI 中立的,但它是基于 GTK 模型,这是 matplotlib 支持的第一个用户界面。与标准 GUI 事件相比,被触发的事件也比 matplotlib 丰富一些,例如包括发生事件的 matplotlib.axes.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)

FigureCanvas 的方法 mpl_connect() 返回一个连接 id,它只是一个整数。当你要断开回调时,只需调用:

fig.canvas.mpl_disconnect(cid)

注意:画布仅保留回调的弱引用。因此,如果回调是类实例的方法,你需要保留对该实例的引用。否则实例将被垃圾回收,回调将消失。

以下是可以连接到的事件,在事件发生时发回给你的类实例以及事件描述:

Event name Class Description
'button_press_event' MouseEvent mouse button is pressed
'button_release_event' MouseEvent mouse button is released
'close_event' CloseEvent a figure is closed
'draw_event' DrawEvent canvas draw (but before screen update)
'key_press_event' KeyEvent key is pressed
'key_release_event' KeyEvent key is released
'motion_notify_event' MouseEvent mouse motion
'pick_event' PickEvent an object in the canvas is selected
'resize_event' ResizeEvent figure canvas is resized
'scroll_event' MouseEvent mouse scroll wheel is rolled
'figure_enter_event' LocationEvent mouse enters a new figure
'figure_leave_event' LocationEvent mouse leaves a figure
'axes_enter_event' LocationEvent mouse enters a new axes
'axes_leave_event' LocationEvent mouse leaves an axes

事件属性

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

属性 描述
name 事件名称
canvas 生成事件的 FigureCanvas 实例
guiEvent 触发 matplotlib 事件的 GUI 事件

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

属性 描述
x x 位置,距离画布左端的像素
y y 位置,距离画布底端的像素
inaxes 如果鼠标经过轴域,则为 Axes 实例
xdata 鼠标的 x 坐标,以数据坐标为单位
ydata 鼠标的 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 = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click to build line segments')
line, = ax.plot([0], [0])  # empty line
linebuilder = LineBuilder(line)

plt.show()

我们刚刚使用的 MouseEvent 是一个 LocationEvent,因此我们可以访问 event.xevent.xdata 中的数据和像素坐标。 除了 LocationEvent 属性,它还拥有:

属性 描述
button 按下的按钮,None123'up''down''up''down' 用于滚动事件)
key 按下的键,None,任何字符,'shift''win' 或者 'control'

练习:可拖拽的矩形

编写使用 Rectangle 实例初始化的可拖动矩形类,但在拖动时会移动其 xy位置。提示:你需要存储矩形的原始 xy 位置,存储为 rect.xy 并连接到按下,移动和释放鼠标事件。当鼠标按下时,检查点击是否发生在你的矩形上(见 matplotlib.patches.Rectangle.contains()),如果是,存储矩形 xy 和数据坐标为单位的鼠标点击位置。在移动事件回调中,计算鼠标移动的 deltaxdeltay,并将这些增量添加到存储的原始矩形,并重新绘图。在按钮释放事件中,只需将所有你存储的按钮按下数据重置为 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):
        'on button press we will see if the mouse is over us and 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)
        x0, y0 = self.rect.xy
        self.press = x0, y0, event.xdata, event.ydata

    def on_motion(self, event):
        'on motion we will move the rect if the mouse is over us'
        if self.press is None: return
        if event.inaxes != self.rect.axes: return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        #print('x0=%f, xpress=%f, event.xdata=%f, dx=%f, x0+dx=%f' %
        #      (x0, xpress, event.xdata, 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):
        'on release we reset the press data'
        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        'disconnect all the stored connection ids'
        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 = plt.figure()
ax = fig.add_subplot(111)
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()

附加题:使用 animations recipe 中讨论的动画 blit 技术,使动画绘制更快更流畅

附加题解决方案:

# draggable rectangle with the animation blit techniques; see
# http://www.scipy.org/Cookbook/Matplotlib/Animations
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):
        'on button press we will see if the mouse is over us and store some data'
        if event.inaxes != self.rect.axes: return
        if DraggableRectangle.lock is not None: return
        contains, attrd = self.rect.contains(event)
        if not contains: return
        print('event contains', self.rect.xy)
        x0, y0 = self.rect.xy
        self.press = x0, y0, 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):
        'on motion we will move the rect if the mouse is over us'
        if DraggableRectangle.lock is not self:
            return
        if event.inaxes != self.rect.axes: 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):
        'on release we reset the press data'
        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 the stored connection ids'
        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 = plt.figure()
ax = fig.add_subplot(111)
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 = plt.figure()
fig1.suptitle('mouse hover over figure or axes to trigger events')
ax1 = fig1.add_subplot(211)
ax2 = fig1.add_subplot(212)

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 = plt.figure()
fig2.suptitle('mouse hover over figure or axes to trigger events')
ax1 = fig2.add_subplot(211)
ax2 = fig2.add_subplot(212)

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

对象采集

你可以通过设置 Artistpicker 属性 (例如,matplotlib Line2D, Text, Patch, Polygon, AxesImage, 等) 来启用选择对象。

picker 属性有多种含义:

属性 描述
None 选择的 artist 已禁用(默认)
boolean 如果为 True,则启用选择,当鼠标移动到该 artist 上方时,会触发事件
float 如果 picker 是数字,则将其解释为点的 epsilon 公差,并且如果其数据在鼠标事件的 epsilon 内,则 artist 将触发事件。对于像 lines 和 patch collections 的一些 artist,artist 可以向生成的选择事件提供附加数据,例如,在选择事件的 epsilon 内的数据的索引。
function 如果 picker 是可调用的,则它是用户提供的函数,用于确定艺术家是否被鼠标事件击中。签名为 hit, props = picker(artist, mouseevent),用于测试是否命中。如果鼠标事件在 artist 上,返回 hit = Trueprops 是一个属性字典,它们会添加到 PickEvent 属性。

通过设置 picker 属性启用对 artist 进行拾取后,你需要连接到图画布的 pick_event,以便在鼠标按下事件中获取拾取回调。 例如:

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

传给你的回调的 PickEvent 事件永远有两个属性:

mouseevent|是生成拾取事件的鼠标事件。鼠标事件具有像 x 和 y(显示空间中的坐标,例如,距离左,下的像素)和 xdata,ydata(数据空间中的坐标)的属性。此外,你可以获取有关按下哪些按钮,按下哪些键,鼠标在哪个轴域(Axes)上面等信息。详细信息请参阅 matplotlib.backend_bases.MouseEvent
artist|生成拾取事件的 Artist

另外,像 Line2DPatchCollection 的某些 artist 可以将附加的元数据(如索引)附加到满足选择器标准的数据中(例如,行中在指定 epsilon 容差内的所有点)。

简单拾取示例

在下面的示例中,我们将行选择器属性设置为标量,因此它表示以点为单位的容差(72 点/英寸)。 当拾取事件位于距离线条的容差范围时,将调用 onpick 回调函数,并且带有在拾取距离容差内的数据顶点索引。 我们的 onpick 回调函数只打印在拾取位置上的数据。 不同的 matplotlib 艺术家可以将不同的数据附加到 PickEvent。 例如,Line2D将ind属性作为索引附加到拾取点下面的行数据中。 有关 LinePickEvent 属性的详细信息,请参阅pick()。 这里是代码:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), 'o', picker=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 mu, sigma points, plot the raw data from
the dataset that generated the mean and stddev
"""
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 = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on point to plot time series')
line, = ax.plot(xs, ys, 'o', picker=5)  # 5 points tolerance


def onpick(event):

    if event.artist!=line: return True

    N = len(event.ind)
    if not N: return True


    figi = plt.figure()
    for subplotnum, dataind in enumerate(event.ind):
        ax = figi.add_subplot(N,1,subplotnum+1)
        ax.plot(X[dataind])
        ax.text(0.05, 0.9, 'mu=%1.3f\nsigma=%1.3f'%(xs[dataind], ys[dataind]),
                transform=ax.transAxes, va='top')
        ax.set_ylim(-0.5, 1.5)
    figi.show()
    return True

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

plt.show()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352