天天看點

Matplotlib 中文使用者指南 7.3 事件處理及拾取事件處理及拾取

事件處理及拾取

原文: Event handling and picking 譯者: 飛龍 協定: CC BY-NC-SA 4.0

matplotlib 使用了許多使用者界面工具包(wxpython,tkinter,qt4,gtk 和 macosx),為了支援互動式平移和縮放圖形等功能,擁有一套 API 通過按鍵和滑鼠移動與圖形互動,并且『GUI中立』,對開發人員十分有幫助,是以我們不必重複大量的代碼來跨不同的使用者界面。雖然事件處理 API 是 GUI 中立的,但它是基于 GTK 模型,這是 matplotlib 支援的第一個使用者界面。與标準 GUI 事件相比,被觸發的事件也比 matplotlib 豐富一些,例如包括發生事件的

matplotlib.axes.Axes

的資訊。事件還能夠了解 matplotlib 坐标系,并且在事件中以像素和資料坐标為機關報告事件位置。

事件連接配接

要接收事件,你需要編寫一個回調函數,然後将你的函數連接配接到事件管理器,它是

FigureCanvasBase

的一部分。這是一個簡單的例子,列印滑鼠點選的位置和按下哪個按鈕:

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(np.random.rand(10))

def onclick(event):
    print('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
          (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)           

注意

畫布僅保留回調的弱引用。 是以,如果回調是類執行個體的方法,你需要保留對該執行個體的引用。 否則執行個體将被垃圾回收,回調将消失。

以下是可以連接配接到的事件,在事件發生時發回給你的類執行個體以及事件描述:

事件名稱 類和描述

'button_press_event'

MouseEvent

- 滑鼠按鈕被按下

'button_release_event'

MouseEvent

- 滑鼠按鈕被釋放

'draw_event'

DrawEvent

- 畫布繪圖

'key_press_event'

KeyEvent

- 按鍵被按下

'key_release_event'

KeyEvent

- 按鍵被釋放

'motion_notify_event'

MouseEvent

- 滑鼠移動

'pick_event'

PickEvent

- 畫布中的對象被選中

'resize_event'

ResizeEvent

- 圖形畫布大小改變

'scroll_event'

MouseEvent

- 滑鼠滾輪被滾動

'figure_enter_event'

LocationEvent

- 滑鼠進入新的圖形

'figure_leave_event'

LocationEvent

- 滑鼠離開圖形

'axes_enter_event'

LocationEvent

- 滑鼠進入新的軸域

'axes_leave_event'

LocationEvent

- 滑鼠離開軸域

事件屬性

所有 matplotlib 事件繼承自基類

matplotlib.backend_bases.Event

,儲存以下屬性:

name

canvas

生成事件的

FigureCanvas

執行個體

guiEvent

觸發 matplotlib 事件的 GUI 事件

最常見的事件是按鍵按下/釋放事件、滑鼠按下/釋放和移動事件。 處理這些事件的

KeyEvent

MouseEvent

類都派生自

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.x

event.xdata

中的資料和像素坐标。 除了

LocationEvent

屬性,它擁有:

button

按下的按鈕,

None

、1、2、3、

'up'

'down'

'up'

'down'

用于滾動事件)

key

按下的鍵,

None

,任何字元,

'shift'

'win'

或者

'control'

可拖拽的矩形練習

編寫使用

Rectangle

執行個體初始化的可拖動矩形類,但在拖動時會移動其

x

y

位置。 提示:你需要存儲矩形的原始

xy

位置,存儲為

rect.xy

并連接配接到按下,移動和釋放滑鼠事件。 當滑鼠按下時,檢查點選是否發生在你的矩形上(見

matplotlib.patches.Rectangle.contains()

),如果是,存儲矩形

xy

和資料坐标為機關的滑鼠點選位置。 在移動事件回調中,計算滑鼠移動的

deltax

deltay

,并将這些增量添加到存儲的原始矩形,并重新繪圖。 在按鈕釋放事件中,隻需将所有你存儲的按鈕按下資料重置為

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

附加題:使用動畫秘籍中讨論的動畫 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()           

對象拾取

你可以通過設定藝術家的

picker

屬性(例如,matplotlib

Line2D

Text

Patch

Polygon

AxesImage

等)來啟用選擇,

picker

屬性有多種含義:

None

選擇對于該藝術家已禁用(預設)

boolean

如果為

True

,則啟用選擇,當滑鼠移動到該藝術家上方時,會觸發事件

float

如果選擇器是數字,則将其解釋為點的 ε 公差,并且如果其資料在滑鼠事件的 ε 内,則藝術家将觸發事件。 對于像線條和更新檔集合的一些藝術家,藝術家可以向生成的選擇事件提供附加資料,例如,在選擇事件的 ε 内的資料的索引。

函數

如果拾取器是可調用的,則它是使用者提供的函數,用于确定藝術家是否被滑鼠事件擊中。 簽名為

hit, props = picker(artist, mouseevent)

,用于測試是否命中。 如果滑鼠事件在藝術家上,傳回

hit = True

props

是一個屬性字典,它們會添加到

PickEvent

屬性。

通過設定

picker

屬性啟用對藝術家進行拾取後,你需要連接配接到圖畫布的

pick_event

,以便在滑鼠按下事件中擷取拾取回調。 例如:

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

傳給你的回調的

PickEvent

事件永遠有兩個屬性:

mouseevent

是生成拾取事件的滑鼠事件。滑鼠事件具有像

x

y

(顯示空間中的坐标,例如,距離左,下的像素)和

xdata

ydata

(資料空間中的坐标)的屬性。 此外,你可以擷取有關按下哪些按鈕,按下哪些鍵,滑鼠在哪個軸域上面等資訊。詳細資訊請參閱

matplotlib.backend_bases.MouseEvent

artist

生成拾取事件的

Artist

另外,像

Line2D

PatchCollection

的某些藝術家可以将附加的中繼資料(如索引)附加到滿足選擇器标準的資料中(例如,行中在指定 ε 容差内的所有點)

簡單拾取示例

在下面的示例中,我們将行選擇器屬性設定為标量,是以它表示以點為機關的容差(72 點/英寸)。 當拾取事件位于距離線條的容差範圍時,将調用

onpick

回調函數,并且帶有在拾取距離容差内的資料頂點索引。 我們的

onpick

回調函數隻列印在拾取位置上的資料。 不同的 matplotlib 藝術家可以将不同的資料附加到

PickEvent

。 例如,

Line2D

ind

屬性作為索引附加到拾取點下面的行資料中。 有關

Line

PickEvent

屬性的詳細資訊,請參閱

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