Gestione eventi e picking #
Matplotlib funziona con una serie di toolkit dell'interfaccia utente (wxpython, tkinter, qt, gtk e macosx) e per supportare funzionalità come la panoramica interattiva e lo zoom delle figure, è utile agli sviluppatori disporre di un'API per interagire con la figura tramite pressioni di tasti e movimenti del mouse che è "GUI neutral", quindi non dobbiamo ripetere molto codice attraverso le diverse interfacce utente. Sebbene l'API di gestione degli eventi sia indipendente dalla GUI, si basa sul modello GTK, che è stata la prima interfaccia utente supportata da Matplotlib. Gli eventi che vengono attivati sono anche un po' più ricchi rispetto a Matplotlib rispetto agli eventi della GUI standard, incluse informazioni come in cui
Axes
si è verificato l'evento. Gli eventi comprendono anche il sistema di coordinate Matplotlib e riportano le posizioni degli eventi sia in pixel che in coordinate dati.
Connessioni evento #
Per ricevere eventi, devi scrivere una funzione di callback e quindi connettere la tua funzione al gestore eventi, che fa parte del
FigureCanvasBase
. Ecco un semplice esempio che stampa la posizione del clic del mouse e quale pulsante è stato premuto:
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)
Il FigureCanvasBase.mpl_connect
metodo restituisce un ID connessione (un numero intero), che può essere utilizzato per disconnettere il callback tramite
fig.canvas.mpl_disconnect(cid)
Nota
Il canvas conserva solo riferimenti deboli ai metodi di istanza usati come callback. Pertanto, è necessario mantenere un riferimento alle istanze che possiedono tali metodi. In caso contrario, l'istanza verrà sottoposta a Garbage Collection e il callback svanirà.
Ciò non influisce sulle funzioni libere utilizzate come callback.
Ecco gli eventi a cui puoi connetterti, le istanze di classe che ti vengono inviate quando si verifica l'evento e le descrizioni degli eventi:
Nome dell'evento |
Classe |
Descrizione |
---|---|---|
'evento_pressione_pulsante' |
viene premuto il pulsante del mouse |
|
'evento_rilascio_pulsante' |
pulsante del mouse viene rilasciato |
|
'chiudi_evento' |
la figura è chiusa |
|
'draw_evento' |
la tela è stata disegnata (ma il widget dello schermo non è ancora aggiornato) |
|
'key_press_evento' |
tasto viene premuto |
|
'evento_rilascio_chiave' |
la chiave viene rilasciata |
|
'evento_notifica_movimento' |
mosse del mouse |
|
'scegli_evento' |
l'artista nella tela è selezionato |
|
'ridimensiona_evento' |
la tela della figura viene ridimensionata |
|
'scroll_evento' |
la rotella di scorrimento del mouse viene ruotata |
|
'figure_enter_event' |
mouse entra in una nuova figura |
|
'figura_lascia_evento' |
il topo lascia una figura |
|
'axes_enter_event' |
il mouse entra in un nuovo asse |
|
'axes_leave_event' |
mouse lascia un assi |
Nota
Quando ti connetti agli eventi 'key_press_event' e 'key_release_event', potresti riscontrare incoerenze tra i diversi toolkit dell'interfaccia utente con cui lavora Matplotlib. Ciò è dovuto a incoerenze/limitazioni del toolkit dell'interfaccia utente. La tabella seguente mostra alcuni esempi di base di ciò che potresti aspettarti di ricevere come tasti (utilizzando un layout di tastiera QWERTY) dai diversi toolkit dell'interfaccia utente, dove una virgola separa i diversi tasti:
Tasto/i premuto/i |
WxPython |
Qt |
WebAgg |
Gtk |
Tkinter |
macosx |
---|---|---|---|---|---|---|
Maiusc+2 |
Maiusc, Maiusc+2 |
spostare, @ |
spostare, @ |
spostare, @ |
spostare, @ |
spostare, @ |
Maiusc+F1 |
maiusc, maiusc+f1 |
maiusc, maiusc+f1 |
maiusc, maiusc+f1 |
maiusc, maiusc+f1 |
maiusc, maiusc+f1 |
maiusc, maiusc+f1 |
Spostare |
spostare |
spostare |
spostare |
spostare |
spostare |
spostare |
Controllo |
controllo |
controllo |
controllo |
controllo |
controllo |
controllo |
Alt |
alt |
alt |
alt |
alt |
alt |
alt |
Alt Gr |
Niente |
Niente |
alt |
iso_level3_shift |
iso_level3_shift |
|
Blocco maiuscole |
blocco maiuscole |
blocco maiuscole |
blocco maiuscole |
blocco maiuscole |
blocco maiuscole |
blocco maiuscole |
BLOC MAIUSC+a |
maiuscolo_lock, a |
maiuscolo_lock, a |
maiuscolo_lock, A |
maiuscolo_lock, A |
maiuscolo_lock, A |
maiuscolo_lock, a |
un |
un |
un |
un |
un |
un |
un |
Maiusc+a |
spostamento, A |
spostamento, A |
spostamento, A |
spostamento, A |
spostamento, A |
spostamento, A |
BLOC MAIUSC+MAIUSC+a |
maiuscolo, maiusc, A |
maiuscolo, maiusc, A |
maiuscolo, maiuscole, a |
maiuscolo, maiuscole, a |
maiuscolo, maiuscole, a |
maiuscolo, maiusc, A |
Ctrl+Maiusc+Alt |
controllo, ctrl+maiusc, ctrl+alt |
controllo, ctrl+maiusc, ctrl+meta |
controllo, ctrl+maiusc, ctrl+meta |
controllo, ctrl+maiusc, ctrl+meta |
controllo, ctrl+maiusc, ctrl+meta |
controllo, ctrl+maiusc, ctrl+alt+maiusc |
Ctrl+Maiusc+a |
controllo, ctrl+maiusc, ctrl+A |
controllo, ctrl+maiusc, ctrl+A |
controllo, ctrl+maiusc, ctrl+A |
controllo, ctrl+maiusc, ctrl+A |
controllo, ctrl+maiusc, ctrl+a |
controllo, ctrl+maiusc, ctrl+A |
F1 |
f1 |
f1 |
f1 |
f1 |
f1 |
f1 |
Ctrl+F1 |
controllo, ctrl+f1 |
controllo, ctrl+f1 |
controllo, ctrl+f1 |
controllo, ctrl+f1 |
controllo, ctrl+f1 |
controllo, niente |
Matplotlib allega alcune richiamate alla pressione dei tasti per impostazione predefinita per l'interattività; sono documentati nella sezione Scelte rapide da tastiera di navigazione .
Attributi dell'evento #
Tutti gli eventi Matplotlib ereditano dalla classe base
matplotlib.backend_bases.Event
, che memorizza gli attributi:
name
il nome dell'evento
canvas
l'istanza FigureCanvas che genera l'evento
guiEvent
l'evento GUI che ha attivato l'evento Matplotlib
Gli eventi più comuni che sono il pane quotidiano della gestione degli eventi sono gli eventi di pressione/rilascio dei tasti e gli eventi di pressione/rilascio del mouse ed eventi di movimento. Le classi KeyEvent
e MouseEvent
che gestiscono questi eventi derivano entrambe dal LocationEvent, che ha i seguenti attributi
x
,y
posizione x e y del mouse in pixel dalla sinistra e dal fondo dell'area di disegno
inaxes
l'
Axes
istanza su cui si trova il mouse, se presente; altrimenti Nessunoxdata
,ydata
posizione x e y del mouse nelle coordinate dei dati, se il mouse si trova sopra un asse
Diamo un'occhiata a un semplice esempio di tela, in cui viene creato un semplice segmento di linea ogni volta che viene premuto il mouse:
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()
Quello MouseEvent
che abbiamo appena usato è un LocationEvent
, quindi abbiamo accesso ai dati e alle coordinate dei pixel tramite e . Oltre agli attributi, ha anche(event.x, event.y)
(event.xdata,
event.ydata)
LocationEvent
button
il pulsante premuto: Nessuno,
MouseButton
, 'su' o 'giù' (su e giù sono usati per gli eventi di scorrimento)key
il tasto premuto: Nessuno, qualsiasi carattere, 'shift', 'win' o 'control'
Esercizio rettangolo trascinabile n.
Scrivi una classe rettangolo trascinabile inizializzata con
Rectangle
un'istanza ma che sposterà la sua xy
posizione quando viene trascinata. Suggerimento: dovrai memorizzare la
xy
posizione originale del rettangolo che è memorizzato come rect.xy e connetterti agli eventi di pressione, movimento e rilascio del mouse. Quando si preme il mouse, verificare se il clic si verifica sul rettangolo (vedere
Rectangle.contains
) e, in caso affermativo, memorizzare il rettangolo xy e la posizione del clic del mouse nelle coordinate dei dati. Nel callback dell'evento di movimento, calcola il deltax e il deltay del movimento del mouse e aggiungi quei delta all'origine del rettangolo che hai memorizzato. Il ridisegna la figura. Nell'evento di rilascio del pulsante, è sufficiente reimpostare tutti i dati di pressione del pulsante archiviati come Nessuno.
Ecco la soluzione:
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()
Credito extra : usa il blitting per rendere il disegno animato più veloce e fluido.
Soluzione di credito extra:
# 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()
Mouse entra ed esci #
Se vuoi essere avvisato quando il mouse entra o esce da una figura o da un asse, puoi collegarti agli eventi di entrata/uscita della figura/assi. Ecco un semplice esempio che cambia i colori degli assi e dello sfondo della figura su cui si trova il mouse:
"""
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()
Raccolta oggetti #
Puoi abilitare il prelievo impostando la picker
proprietà di un Artist
(come Line2D
, Text
, Patch
, Polygon
, AxesImage
, ecc.)
La picker
proprietà può essere impostata utilizzando vari tipi:
None
La selezione è disabilitata per questo artista (impostazione predefinita).
boolean
Se True, la selezione sarà abilitata e l'artista attiverà un evento di selezione se l'evento del mouse si trova sopra l'artista.
callable
Se picker è un callable, è una funzione fornita dall'utente che determina se l'artista viene colpito dall'evento del mouse. La firma serve a determinare l'hit test. Se l'evento del mouse è sopra l'artista, return ; è un dizionario di proprietà che diventano attributi aggiuntivi su .
hit, props = picker(artist, mouseevent)
hit = True
props
PickEvent
La pickradius
proprietà dell'artista può inoltre essere impostata su un valore di tolleranza in punti (ci sono 72 punti per pollice) che determina quanto lontano può essere il mouse e attivare comunque un evento del mouse.
Dopo aver abilitato un artista per la selezione impostando la picker
proprietà, è necessario connettere un gestore alla figura canvas pick_event per ottenere i callback di selezione sugli eventi di pressione del mouse. Il gestore in genere assomiglia a
def pick_handler(event):
mouseevent = event.mouseevent
artist = event.artist
# now do something with this...
Il PickEvent
passato al tuo callback ha sempre i seguenti attributi:
mouseevent
Il
MouseEvent
che genera l'evento pick. Vedere event-attributes per un elenco di attributi utili sull'evento del mouse.artist
L'
Artist
elemento che ha generato l'evento pick.
Inoltre, ad alcuni artisti piacciono Line2D
e PatchCollection
possono allegare metadati aggiuntivi, come gli indici dei dati che soddisfano i criteri di selezione (ad esempio, tutti i punti nella linea che rientrano nella pickradius
tolleranza specificata).
Esempio di selezione semplice #
Nell'esempio seguente, abilitiamo il prelievo sulla linea e impostiamo una tolleranza del raggio di prelievo in punti. La onpick
funzione di callback verrà chiamata quando l'evento pick si trova all'interno della distanza di tolleranza dalla linea e ha gli indici dei vertici dei dati che si trovano all'interno della tolleranza della distanza di selezione. La nostra onpick
funzione di richiamata stampa semplicemente i dati che si trovano sotto la posizione di prelievo. Artisti Matplotlib diversi possono allegare dati diversi a PickEvent. Ad esempio, Line2D
allega la proprietà ind, che sono gli indici nei dati della riga sotto il punto di selezione. Vedere
Line2D.pick
per i dettagli sulle PickEvent
proprietà della linea.
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()
Esercizio di raccolta #
Crea un set di dati di 100 matrici di 1000 numeri casuali gaussiani e calcola la media campionaria e la deviazione standard di ciascuno di essi (suggerimento: le matrici NumPy hanno un metodo medio e std) e crea un grafico indicatore xy delle 100 medie rispetto ai 100 deviazioni standard. Collega la linea creata dal comando plot all'evento pick e traccia la serie temporale originale dei dati che hanno generato i punti cliccati. Se più di un punto rientra nella tolleranza del punto cliccato, è possibile utilizzare più sottotrame per tracciare le serie temporali multiple.
Soluzione dell'esercizio:
"""
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()