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 Axesem 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_connectmé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'

MouseEvent

botão do mouse é pressionado

'button_release_event'

MouseEvent

botão do mouse é liberado

'fechar_evento'

CloseEvent

figura está fechada

'desenhar_evento'

DrawEvent

a tela foi desenhada (mas o widget de tela ainda não foi atualizado)

'key_press_event'

KeyEvent

tecla é pressionada

'key_release_event'

KeyEvent

chave é liberada

'motion_notify_event'

MouseEvent

movimentos do mouse

'escolher_evento'

PickEvent

artista na tela é selecionado

'resize_event'

ResizeEvent

a tela da figura é redimensionada

'scroll_event'

MouseEvent

a roda de rolagem do mouse é rolada

'figura_enter_event'

LocationEvent

mouse entra em uma nova figura

'figura_leave_event'

LocationEvent

rato deixa uma figura

'axes_enter_event'

LocationEvent

mouse entra em um novo eixo

'axes_leave_event'

LocationEvent

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 KeyEvente MouseEventque 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 Axesinstância sobre a qual o mouse está, se houver; senão nenhum

xdata,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 MouseEventque 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 Rectangleinstância, mas moverá sua xy localização quando arrastada. Dica: você precisará armazenar o xylocal 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 pickerpropriedade de um Artist(como Line2D, Text, Patch, Polygon, AxesImageetc.)

A pickerpropriedade 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 = TruepropsPickEvent

A pickradiuspropriedade 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 PickEventpassado para o seu callback sempre tem os seguintes atributos:

mouseevent

O MouseEventque gera o evento pick. Consulte atributos de evento para obter uma lista de atributos úteis no evento do mouse.

artist

O Artistque gerou o evento pick.

Além disso, alguns artistas gostam Line2De PatchCollectionpodem 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 pickradiustolerâ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, Line2Danexa a propriedade ind, que são os índices nos dados da linha sob o ponto de seleção. Consulte Line2D.pickpara obter detalhes sobre as PickEventpropriedades 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()