Event-Handling und Picking#
Matplotlib arbeitet mit einer Reihe von Benutzeroberflächen-Toolkits (wxpython, tkinter, qt, gtk und macOS) und um Funktionen wie interaktives Zoomen und Schwenken von Figuren zu unterstützen, ist es für die Entwickler hilfreich, eine API für die Interaktion mit der Figur über Tastendrücke und Mausbewegungen zu haben, die "GUI-neutral" ist, damit wir nicht viel Code für die verschiedenen Benutzeroberflächen wiederholen müssen. Obwohl die Event-Handling-API GUI-neutral ist, basiert sie auf dem GTK-Modell, das die erste Benutzeroberfläche war, die Matplotlib unterstützte. Die ausgelösten Ereignisse sind auch etwas umfangreicher im Vergleich zu Matplotlib als Standard-GUI-Ereignisse, einschließlich Informationen darüber, in welchem Axes das Ereignis aufgetreten ist. Die Ereignisse verstehen auch das Matplotlib-Koordinatensystem und melden Ereignispositionen sowohl in Pixel- als auch in Datenkoordinaten.
Event-Verbindungen#
Um Ereignisse zu empfangen, müssen Sie eine Callback-Funktion schreiben und diese dann mit dem Event-Manager verbinden, der Teil der FigureCanvasBase ist. Hier ist ein einfaches Beispiel, das die Position des Mausklicks und die gedrückte Taste ausgibt
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)
Die Methode FigureCanvasBase.mpl_connect gibt eine Verbindungs-ID (eine Ganzzahl) zurück, mit der der Callback über
fig.canvas.mpl_disconnect(cid)
Hinweis
Die Canvas behält nur schwache Referenzen auf Instanzmethoden, die als Callbacks verwendet werden. Daher müssen Sie eine Referenz auf Instanzen behalten, die solche Methoden besitzen. Andernfalls wird die Instanz vom Garbage Collector entfernt und der Callback verschwindet.
Dies hat keine Auswirkungen auf freie Funktionen, die als Callbacks verwendet werden.
Hier sind die Ereignisse, mit denen Sie sich verbinden können, die Klasseninstanzen, die Ihnen bei Auftreten des Ereignisses zurückgegeben werden, und die Ereignisbeschreibungen
Ereignisname |
Klasse |
Beschreibung |
|---|---|---|
'button_press_event' |
Maus-Taste wird gedrückt |
|
'button_release_event' |
Maus-Taste wird losgelassen |
|
'close_event' |
Figur wird geschlossen |
|
'draw_event' |
Canvas wurde gezeichnet (aber Widget auf dem Bildschirm noch nicht aktualisiert) |
|
'key_press_event' |
Taste wird gedrückt |
|
'key_release_event' |
Taste wird losgelassen |
|
'motion_notify_event' |
Maus bewegt sich |
|
'pick_event' |
Element (Artist) auf der Canvas wird ausgewählt |
|
'resize_event' |
Figur-Canvas wird in der Größe geändert |
|
'scroll_event' |
Maus-Scrollrad wird gedreht |
|
'figure_enter_event' |
Maus betritt eine neue Figur |
|
'figure_leave_event' |
Maus verlässt eine Figur |
|
'axes_enter_event' |
Maus betritt eine neue Achse |
|
'axes_leave_event' |
Maus verlässt eine Achse |
Hinweis
Beim Verbinden mit den Ereignissen 'key_press_event' und 'key_release_event' können Inkonsistenzen zwischen den verschiedenen Benutzeroberflächen-Toolkits auftreten, mit denen Matplotlib arbeitet. Dies liegt an Inkonsistenzen/Einschränkungen des Benutzeroberflächen-Toolkits. Die folgende Tabelle zeigt einige grundlegende Beispiele dafür, was Sie als Taste(n) (unter Verwendung eines QWERTY-Tastaturlayouts) von den verschiedenen Benutzeroberflächen-Toolkits erhalten können, wobei ein Komma verschiedene Tasten trennt
Gedrückte(n) Taste(n) |
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 |
nothing |
alt |
iso_level3_shift |
nothing |
|
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, nothing |
control, ctrl+f1 |
control, ctrl+f1 |
control, ctrl+f1 |
Matplotlib hängt standardmäßig einige Tasten-Callback-Funktionen für Interaktivität an; diese sind im Abschnitt Navigation-Tastenkombinationen dokumentiert.
Event-Attribute#
Alle Matplotlib-Ereignisse erben von der Basisklasse matplotlib.backend_bases.Event, die die Attribute speichert
nameDer Ereignisname
canvasDie FigureCanvas-Instanz, die das Ereignis generiert
guiEventDas GUI-Ereignis, das das Matplotlib-Ereignis ausgelöst hat
Die gebräuchlichsten Ereignisse, die das Kerngeschäft des Event-Handlings sind, sind Tasten-Druck-/Loslass-Ereignisse und Maus-Druck-/Loslass- und Bewegungsereignisse. Die Klassen KeyEvent und MouseEvent, die diese Ereignisse behandeln, sind beide von LocationEvent abgeleitet, das die folgenden Attribute hat
x,yMaus x- und y-Position in Pixel vom linken und unteren Rand der Canvas
inaxesDie
Axes-Instanz, über der sich die Maus befindet, falls vorhanden; sonst Nonexdata,ydataMaus x- und y-Position in Datenkoordinaten, wenn die Maus über einem Achsenbereich liegt
Betrachten wir ein einfaches Beispiel einer Canvas, auf der jedes Mal ein einfaches Liniensegment erstellt wird, wenn eine Maustaste gedrückt wird
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()
Das von uns gerade verwendete MouseEvent ist ein LocationEvent, daher haben wir über (event.x, event.y) und (event.xdata, event.ydata) Zugriff auf die Daten- und Pixelkoordinaten. Zusätzlich zu den Attributen von LocationEvent gibt es noch
buttonDie gedrückte Taste: None,
MouseButton, 'up', oder 'down' (up und down werden für Scroll-Ereignisse verwendet)keyDie gedrückte Taste: None, jedes Zeichen, 'shift', 'win' oder 'control'
Übung mit ziehbarem Rechteck#
Schreiben Sie eine Klasse für ein ziehbares Rechteck, das mit einer Rectangle-Instanz initialisiert wird, aber seine xy-Position beim Ziehen verschiebt.
Hinweis: Sie müssen die ursprüngliche xy-Position des Rechtecks speichern, die als rect.xy gespeichert ist, und sich mit den Mausereignissen "press", "motion" und "release" verbinden. Wenn die Maustaste gedrückt wird, prüfen Sie, ob der Klick über Ihrem Rechteck erfolgt (siehe Rectangle.contains) und wenn ja, speichern Sie die xy-Koordinaten des Rechtecks und die Position des Mausklicks in Datenkoordinaten. Im Callback für das Bewegungsereignis berechnen Sie das Delta-x und Delta-y der Mausbewegung und addieren diese Deltas zum Ursprung des gespeicherten Rechtecks, dann zeichnen Sie die Figur neu. Beim Loslassen der Maustaste setzen Sie einfach alle gespeicherten Daten des Tastendrucks auf None zurück.
Hier ist die Lösung
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()
Zusatzaufgabe: Verwenden Sie Blitting, um die animierte Zeichnung schneller und flüssiger zu gestalten.
Lösung der Zusatzaufgabe
# 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()
Maus ein- und austreten#
Wenn Sie benachrichtigt werden möchten, wenn die Maus eine Figur oder Achse betritt oder verlässt, können Sie sich mit den Ereignissen "figure/axes enter/leave" verbinden. Hier ist ein einfaches Beispiel, das die Farben der Achse und des Figurenhintergrunds ändert, über dem sich die Maus befindet
"""
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()
Objekt-Picking#
Sie können das Picking aktivieren, indem Sie die Eigenschaft picker eines Artist (wie Line2D, Text, Patch, Polygon, AxesImage usw.)
Die Eigenschaft picker kann mit verschiedenen Typen eingestellt werden
NonePicking ist für diesen Künstler deaktiviert (Standard).
booleanWenn True, ist das Picking aktiviert und der Künstler löst ein Pick-Ereignis aus, wenn das Mausereignis über dem Künstler liegt.
callableWenn picker ein callable ist, ist es eine vom Benutzer bereitgestellte Funktion, die bestimmt, ob der Künstler vom Mausereignis getroffen wird. Die Signatur lautet
hit, props = picker(artist, mouseevent), um den Treffertest zu bestimmen. Wenn sich das Mausereignis über dem Künstler befindet, geben Siehit = Truezurück;propsist ein Wörterbuch von Eigenschaften, die zusätzliche Attribute auf demPickEventwerden.
Die Eigenschaft pickradius des Künstlers kann zusätzlich auf einen Toleranzwert in Punkten (72 Punkte pro Zoll) gesetzt werden, der bestimmt, wie weit die Maus entfernt sein kann und trotzdem ein Mausereignis auslöst.
Nachdem Sie einen Künstler für das Picking aktiviert haben, indem Sie die Eigenschaft picker gesetzt haben, müssen Sie einen Handler mit dem FigureCanvas-Pick-Ereignis verbinden, um Pick-Callbacks bei Maustastendrücken zu erhalten. Der Handler sieht typischerweise so aus
def pick_handler(event):
mouseevent = event.mouseevent
artist = event.artist
# now do something with this...
Das PickEvent, das an Ihren Callback übergeben wird, hat immer die folgenden Attribute
mouseeventDas
MouseEvent, das das Pick-Ereignis ausgelöst hat. Siehe Event-Attribute für eine Liste nützlicher Attribute des Mausereignisses.artistDer
Artist, der das Pick-Ereignis ausgelöst hat.
Zusätzlich können bestimmte Künstler wie Line2D und PatchCollection zusätzliche Metadaten anhängen, wie z. B. die Indizes der Daten, die die Pickerkriterien erfüllen (z. B. alle Punkte in der Linie, die sich innerhalb des angegebenen pickradius befinden).
Einfaches Picking-Beispiel#
Im folgenden Beispiel aktivieren wir das Picking für die Linie und legen eine Pick-Radius-Toleranz in Punkten fest. Die Callback-Funktion onpick wird aufgerufen, wenn das Pick-Ereignis innerhalb der Toleranzdistanz von der Linie liegt, und enthält die Indizes der Datenpunkte, die sich innerhalb der Pick-Distanz-Toleranz befinden. Unsere Callback-Funktion onpick gibt einfach die Daten aus, die sich unter der Pick-Position befinden. Verschiedene Matplotlib-Künstler können unterschiedliche Daten an das PickEvent anhängen. Zum Beispiel hängt Line2D die ind-Eigenschaft an, die die Indizes in den Linien-Daten unter dem Pick-Punkt sind. Sehen Sie sich Line2D.pick für Details zu den PickEvent-Eigenschaften der Linie an.
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()
Picking-Übung#
Erstellen Sie einen Datensatz aus 100 Arrays mit 1000 Gaußschen Zufallszahlen und berechnen Sie für jedes davon den Stichprobenmittelwert und die Standardabweichung (Hinweis: NumPy-Arrays haben eine mean- und std-Methode) und erstellen Sie ein xy-Marker-Diagramm der 100 Mittelwerte gegen die 100 Standardabweichungen. Verbinden Sie die durch den Plot-Befehl erzeugte Linie mit dem Pick-Ereignis und plotten Sie die ursprüngliche Zeitreihe der Daten, die die angeklickten Punkte erzeugt haben. Wenn mehr als ein Punkt innerhalb der Toleranz des angeklickten Punktes liegt, können Sie mehrere Unterdiagramme verwenden, um die mehreren Zeitreihen zu plotten.
Lösung der Übung
"""
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()