事件處理與選取#

Matplotlib 與多種使用者介面工具組 (wxpython、tkinter、qt、gtk 和 macOS) 搭配使用,為了支援圖表的互動式平移和縮放等功能,對於開發人員而言,擁有一個透過按鍵和滑鼠移動與圖表互動的 API 會很有幫助,這個 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

MouseEvent

放開滑鼠按鈕

'close_event'

CloseEvent

關閉圖表

'draw_event'

DrawEvent

已繪製畫布 (但螢幕 Widget 尚未更新)

'key_press_event'

KeyEvent

按下按鍵

'key_press_event'

'key_release_event'

KeyEvent

MouseEvent

放開按鍵

'motion_notify_event'

MouseEvent

滑鼠移動

'pick_event'

PickEvent

選取畫布中的藝術家

'resize_event'

MouseEvent

ResizeEvent

調整圖表畫布大小

'scroll_event'

MouseEvent

滾動滑鼠滾輪

'scroll_event'

'figure_enter_event'

LocationEvent

'scroll_event'

滑鼠進入新的圖表

'figure_leave_event'

'scroll_event'

LocationEvent

注意

滑鼠離開圖表

'axes_enter_event'

LocationEvent

滑鼠進入新的軸

'axes_leave_event'

LocationEvent

滑鼠離開軸

當連線到 'key_press_event' 和 'key_release_event' 事件時,您可能會遇到 Matplotlib 使用的不同使用者介面工具組之間的不一致情況。這是因為使用者介面工具組的不一致/限制所導致。下表顯示了一些基本範例,說明您可能會從不同的使用者介面工具組中收到哪些按鍵 (使用 QWERTY 鍵盤配置),其中逗號分隔不同的按鍵

按下的按鍵

Tkinter

Tkinter

Tkinter

Tkinter

Tkinter

Qt

macosx

WebAgg

WebAgg

WebAgg

WebAgg

WebAgg

WebAgg

GTK

WxPython

WxPython

WxPython

WxPython

WxPython

WxPython

Shift+2

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, shift+2

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

shift, @

Shift+F1

shift, 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

shift

shift

shift

shift

shift

shift

shift

Control

Control

Control

Control

Control

Control

control

control

control

control

control

control

control

control

control

control

Alt

control

control

alt

alt

alt

alt

alt

alt

alt

alt

alt

AltGr

AltGr

AltGr

AltGr

AltGr

AltGr

iso_level3_shift

iso_level3_shift

iso_level3_shift

nothing

iso_level3_shift

iso_level3_shift

iso_level3_shift

nothing

nothing

CapsLock

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

a

Shift+a

shift, A

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,並將這些 deltas 加到您儲存的矩形原點,然後重新繪製圖形。在按鈕釋放事件中,只需將您儲存的所有按鈕按下資料重設為 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。 請參閱 事件屬性 以取得滑鼠事件上可用屬性的清單。

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 標記圖。 將 plot 命令建立的線條連接到選取事件,並繪製產生點擊點的原始資料時間序列。 如果在點擊點的容差範圍內有多個點,您可以使用多個子圖來繪製多個時間序列。

練習解答

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