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'

MouseEvent

Maus-Taste wird gedrückt

'button_release_event'

MouseEvent

Maus-Taste wird losgelassen

'close_event'

CloseEvent

Figur wird geschlossen

'draw_event'

DrawEvent

Canvas wurde gezeichnet (aber Widget auf dem Bildschirm noch nicht aktualisiert)

'key_press_event'

KeyEvent

Taste wird gedrückt

'key_release_event'

KeyEvent

Taste wird losgelassen

'motion_notify_event'

MouseEvent

Maus bewegt sich

'pick_event'

PickEvent

Element (Artist) auf der Canvas wird ausgewählt

'resize_event'

ResizeEvent

Figur-Canvas wird in der Größe geändert

'scroll_event'

MouseEvent

Maus-Scrollrad wird gedreht

'figure_enter_event'

LocationEvent

Maus betritt eine neue Figur

'figure_leave_event'

LocationEvent

Maus verlässt eine Figur

'axes_enter_event'

LocationEvent

Maus betritt eine neue Achse

'axes_leave_event'

LocationEvent

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

name

Der Ereignisname

canvas

Die FigureCanvas-Instanz, die das Ereignis generiert

guiEvent

Das 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, y

Maus x- und y-Position in Pixel vom linken und unteren Rand der Canvas

inaxes

Die Axes-Instanz, über der sich die Maus befindet, falls vorhanden; sonst None

xdata, ydata

Maus 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

button

Die gedrückte Taste: None, MouseButton, 'up', oder 'down' (up und down werden für Scroll-Ereignisse verwendet)

key

Die 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

None

Picking ist für diesen Künstler deaktiviert (Standard).

boolean

Wenn True, ist das Picking aktiviert und der Künstler löst ein Pick-Ereignis aus, wenn das Mausereignis über dem Künstler liegt.

callable

Wenn 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 Sie hit = True zurück; props ist ein Wörterbuch von Eigenschaften, die zusätzliche Attribute auf dem PickEvent werden.

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

mouseevent

Das MouseEvent, das das Pick-Ereignis ausgelöst hat. Siehe Event-Attribute für eine Liste nützlicher Attribute des Mausereignisses.

artist

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