Manipulação de eventos e picking #
O Matplotlib funciona com vários kits de ferramentas de interface do usuário (wxpython, tkinter, qt, gtk e macosx) e, para oferecer suporte a recursos como panorâmica interativa e zoom de figuras, é útil para os desenvolvedores ter uma API para interagir com a figura por meio de pressionamentos de tecla e movimentos do mouse que são "GUI neutros" para que não tenhamos que repetir muito código nas diferentes interfaces de usuário. Embora a API de manipulação de eventos seja GUI neutra, ela é baseada no modelo GTK, que foi a primeira interface de usuário suportada pelo Matplotlib. Os eventos que são acionados também são um pouco mais ricos em relação ao Matplotlib do que os eventos padrão da GUI, incluindo informações como
Axes
em que o evento ocorreu.
Conexões de evento #
Para receber eventos, você precisa escrever uma função de retorno de chamada e, em seguida, conectar sua função ao gerenciador de eventos, que faz parte do arquivo
FigureCanvasBase
. Aqui está um exemplo simples que imprime a localização do clique do mouse e qual botão foi pressionado:
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)
O FigureCanvasBase.mpl_connect
método retorna um ID de conexão (um número inteiro), que pode ser usado para desconectar o callback via
fig.canvas.mpl_disconnect(cid)
Observação
A tela retém apenas referências fracas a métodos de instância usados como retornos de chamada. Portanto, você precisa manter uma referência às instâncias que possuem tais métodos. Caso contrário, a instância será coletada como lixo e o retorno de chamada desaparecerá.
Isso não afeta as funções gratuitas usadas como callbacks.
Aqui estão os eventos aos quais você pode se conectar, as instâncias de classe que são enviadas de volta para você quando o evento ocorre e as descrições do evento:
Nome do evento |
Classe |
Descrição |
---|---|---|
'button_press_event' |
botão do mouse é pressionado |
|
'button_release_event' |
botão do mouse é liberado |
|
'fechar_evento' |
figura está fechada |
|
'desenhar_evento' |
a tela foi desenhada (mas o widget de tela ainda não foi atualizado) |
|
'key_press_event' |
tecla é pressionada |
|
'key_release_event' |
chave é liberada |
|
'motion_notify_event' |
movimentos do mouse |
|
'escolher_evento' |
artista na tela é selecionado |
|
'resize_event' |
a tela da figura é redimensionada |
|
'scroll_event' |
a roda de rolagem do mouse é rolada |
|
'figura_enter_event' |
mouse entra em uma nova figura |
|
'figura_leave_event' |
rato deixa uma figura |
|
'axes_enter_event' |
mouse entra em um novo eixo |
|
'axes_leave_event' |
mouse deixa um machado |
Observação
Ao conectar-se aos eventos 'key_press_event' e 'key_release_event', você pode encontrar inconsistências entre os diferentes kits de ferramentas de interface do usuário com os quais o Matplotlib trabalha. Isso ocorre devido a inconsistências/limitações do kit de ferramentas da interface do usuário. A tabela a seguir mostra alguns exemplos básicos do que você pode esperar receber como tecla(s) (usando um layout de teclado QWERTY) dos diferentes kits de ferramentas de interface do usuário, onde uma vírgula separa as diferentes teclas:
Tecla(s) pressionada(s) |
WxPythonGenericName |
Qt |
WebAgg |
Gtk |
Tkinter |
Mac OS X |
---|---|---|---|---|---|---|
Shift+2 |
turno, turno+2 |
mudança, @ |
mudança, @ |
mudança, @ |
mudança, @ |
mudança, @ |
Shift+F1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
shift, shift+f1 |
Mudança |
mudança |
mudança |
mudança |
mudança |
mudança |
mudança |
Ao controle |
ao controle |
ao controle |
ao controle |
ao controle |
ao controle |
ao controle |
Alt |
alternativo |
alternativo |
alternativo |
alternativo |
alternativo |
alternativo |
AltGr |
Nada |
Nada |
alternativo |
iso_level3_shift |
iso_level3_shift |
|
Caps Lock |
Caps Lock |
Caps Lock |
Caps Lock |
Caps Lock |
Caps Lock |
Caps Lock |
CapsLock+a |
caps_lock, um |
caps_lock, um |
caps_lock, A |
caps_lock, A |
caps_lock, A |
caps_lock, um |
uma |
uma |
uma |
uma |
uma |
uma |
uma |
Shift+a |
turno, A |
turno, A |
turno, A |
turno, A |
turno, A |
turno, A |
CapsLock+Shift+a |
caps_lock, shift, A |
caps_lock, shift, A |
caps_lock, shift, um |
caps_lock, shift, um |
caps_lock, shift, um |
caps_lock, shift, A |
Ctrl+Shift+Alt |
controle, ctrl+shift, ctrl+alt |
controle, ctrl+shift, ctrl+meta |
controle, ctrl+shift, ctrl+meta |
controle, ctrl+shift, ctrl+meta |
controle, ctrl+shift, ctrl+meta |
controle, ctrl+shift, ctrl+alt+shift |
Ctrl+Shift+a |
controle, ctrl+shift, ctrl+A |
controle, ctrl+shift, ctrl+A |
controle, ctrl+shift, ctrl+A |
controle, ctrl+shift, ctrl+A |
controle, ctrl+shift, ctrl+a |
controle, ctrl+shift, ctrl+A |
F1 |
f1 |
f1 |
f1 |
f1 |
f1 |
f1 |
Ctrl+F1 |
controle, ctrl+f1 |
controle, ctrl+f1 |
controle, ctrl+f1 |
controle, ctrl+f1 |
controle, ctrl+f1 |
controle, nada |
Matplotlib anexa alguns retornos de chamada de pressionamento de tecla por padrão para interatividade; eles estão documentados na seção Atalhos de teclado de navegação .
Atributos do evento #
Todos os eventos Matplotlib herdam da classe base
matplotlib.backend_bases.Event
, que armazena os atributos:
name
o nome do evento
canvas
a instância FigureCanvas gerando o evento
guiEvent
o evento da GUI que acionou o evento Matplotlib
Os eventos mais comuns que são o pão com manteiga do tratamento de eventos são eventos de pressionamento/soltura de teclas e eventos de movimento e pressionamento/soltura do mouse. As classes KeyEvent
e MouseEvent
que lidam com esses eventos são derivadas do LocationEvent, que tem os seguintes atributos
x
,y
posição x e y do mouse em pixels da esquerda e da parte inferior da tela
inaxes
a
Axes
instância sobre a qual o mouse está, se houver; senão nenhumxdata
,ydata
posição x e y do mouse nas coordenadas de dados, se o mouse estiver sobre um eixo
Vejamos um exemplo simples de tela, onde um segmento de linha simples é criado toda vez que o mouse é pressionado:
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()
O MouseEvent
que acabamos de usar é a LocationEvent
, então temos acesso aos dados e coordenadas de pixel via e . Além dos atributos, também possui(event.x, event.y)
(event.xdata,
event.ydata)
LocationEvent
button
o botão pressionado: None,
MouseButton
, 'up' ou 'down' (para cima e para baixo são usados para eventos de rolagem)key
a tecla pressionada: Nenhum, qualquer personagem, 'shift', 'win' ou 'control'
Exercício de retângulo arrastável #
Escreva uma classe de retângulo arrastável que é inicializada com uma
Rectangle
instância, mas moverá sua xy
localização quando arrastada. Dica: você precisará armazenar o
xy
local original do retângulo que está armazenado como rect.xy e conectar-se aos eventos de pressionar, mover e liberar o mouse. Quando o mouse for pressionado, verifique se o clique ocorre sobre o retângulo (consulte
Rectangle.contains
) e, se ocorrer, armazene o retângulo xy e a localização do clique do mouse nas coordenadas de dados. No retorno de chamada do evento de movimento, calcule o deltax e o deltay do movimento do mouse e adicione esses deltas à origem do retângulo que você armazenou. O redesenho da figura. No evento de liberação do botão, basta redefinir todos os dados de pressionamento de botão que você armazenou como Nenhum.
Aqui está a solução:
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()
Crédito extra : Use blitting para tornar o desenho animado mais rápido e suave.
Solução de crédito 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 e sai #
Se você deseja ser notificado quando o mouse entra ou sai de uma figura ou eixo, você pode se conectar aos eventos de entrada/saída de figura/eixos. Aqui está um exemplo simples que muda as cores dos eixos e fundo da figura que o mouse está sobre:
"""
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()
Seleção de objetos #
Você pode ativar a seleção definindo a picker
propriedade de um Artist
(como Line2D
, Text
, Patch
, Polygon
, AxesImage
etc.)
A picker
propriedade pode ser definida usando vários tipos:
None
A seleção está desativada para este artista (padrão).
boolean
Se True, a seleção será habilitada e o artista disparará um evento de seleção se o evento do mouse estiver sobre o artista.
callable
Se o seletor for um callable, é uma função fornecida pelo usuário que determina se o artista é atingido pelo evento do mouse. A assinatura é para determinar o teste de clique. Se o evento do mouse for sobre o artista, retorne ; é um dicionário de propriedades que se tornam atributos adicionais no .
hit, props = picker(artist, mouseevent)
hit = True
props
PickEvent
A pickradius
propriedade do artista também pode ser definida como um valor de tolerância em pontos (há 72 pontos por polegada) que determina a distância que o mouse pode alcançar e ainda acionar um evento de mouse.
Depois de habilitar um artista para seleção definindo a picker
propriedade, você precisa conectar um manipulador à tela de figura pick_event para obter retornos de chamada de seleção em eventos de pressionamento do mouse. O manipulador normalmente se parece com
def pick_handler(event):
mouseevent = event.mouseevent
artist = event.artist
# now do something with this...
O PickEvent
passado para o seu callback sempre tem os seguintes atributos:
mouseevent
O
MouseEvent
que gera o evento pick. Consulte atributos de evento para obter uma lista de atributos úteis no evento do mouse.artist
O
Artist
que gerou o evento pick.
Além disso, alguns artistas gostam Line2D
e PatchCollection
podem anexar metadados adicionais, como os índices dos dados que atendem aos critérios do seletor (por exemplo, todos os pontos na linha que estão dentro da pickradius
tolerância especificada).
Exemplo de seleção simples #
No exemplo abaixo, ativamos a coleta na linha e definimos uma tolerância de raio de coleta em pontos. A onpick
função de retorno de chamada será chamada quando o evento pick estiver dentro da distância de tolerância da linha, e tiver os índices dos vértices de dados que estiverem dentro da tolerância de distância de pick. Nossa onpick
função de callback simplesmente imprime os dados que estão sob o local de coleta. Diferentes artistas do Matplotlib podem anexar dados diferentes ao PickEvent. Por exemplo, Line2D
anexa a propriedade ind, que são os índices nos dados da linha sob o ponto de seleção. Consulte
Line2D.pick
para obter detalhes sobre as PickEvent
propriedades da linha.
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()
Escolhendo o exercício #
Crie um conjunto de dados de 100 matrizes de 1.000 números aleatórios gaussianos e calcule a média amostral e o desvio padrão de cada um deles (dica: as matrizes NumPy têm uma média e um método padrão) e faça um gráfico de marcador xy das 100 médias versus 100 desvio padrão. Conecte a linha criada pelo comando plot ao evento pick e plote a série temporal original dos dados que geraram os pontos clicados. Se mais de um ponto estiver dentro da tolerância do ponto clicado, você pode usar vários subgráficos para plotar as várias séries temporais.
Solução do exercício:
"""
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()