事件處理與選取#
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' |
按下滑鼠按鈕 |
|
'button_release_event' |
MouseEvent |
|
放開滑鼠按鈕 |
CloseEvent |
|
關閉圖表 |
DrawEvent |
|
已繪製畫布 (但螢幕 Widget 尚未更新) |
KeyEvent |
|
按下按鍵 |
'key_release_event' |
|
KeyEvent |
放開按鍵 |
|
'motion_notify_event' |
滑鼠移動 |
|
'pick_event' |
選取畫布中的藝術家 |
|
'resize_event' |
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 鍵盤配置),其中逗號分隔不同的按鍵 |
---|---|---|---|---|---|---|
按下的按鍵 |
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
(例如 Line2D
、Text
、Patch
、Polygon
、AxesImage
等) 的 picker
屬性來啟用選取。
picker
屬性可以使用各種型別進行設定
None
此 artist 的選取功能已停用(預設)。
布林值
如果為 True,則會啟用選取功能,如果滑鼠事件發生在 artist 上方,則 artist 會觸發選取事件。
可呼叫
如果 picker 是可呼叫物件,它是一個使用者提供的函數,用於決定 artist 是否被滑鼠事件命中。簽名是
hit, props = picker(artist, mouseevent)
以判斷命中測試。如果滑鼠事件發生在 artist 上方,則傳回hit = True
;props
是一個屬性字典,它會成為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(例如 Line2D
和 PatchCollection
)可能會附加額外的元資料,例如符合選取器條件的資料索引(例如,線條中所有在指定 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()