Gestion des événements et préparation des commandes #

Matplotlib fonctionne avec un certain nombre de boîtes à outils d'interface utilisateur (wxpython, tkinter, qt, gtk et macosx) et afin de prendre en charge des fonctionnalités telles que le panoramique interactif et le zoom des figures, il est utile pour les développeurs d'avoir une API pour interagir avec la figure via des pressions sur les touches et des mouvements de souris qui sont "neutres à l'interface graphique" afin que nous n'ayons pas à répéter beaucoup de code sur les différentes interfaces utilisateur. Bien que l'API de gestion des événements soit indépendante de l'interface graphique, elle est basée sur le modèle GTK, qui était la première interface utilisateur prise en charge par Matplotlib. Les événements déclenchés sont également un peu plus riches vis-à-vis de Matplotlib que les événements d'interface graphique standard, y compris des informations telles que Axesl'événement dans lequel l'événement s'est produit. Les événements comprennent également le système de coordonnées Matplotlib et signalent les emplacements des événements à la fois en pixels et en coordonnées de données.

Connexions événementielles #

Pour recevoir des événements, vous devez écrire une fonction de rappel, puis connecter votre fonction au gestionnaire d'événements, qui fait partie du FigureCanvasBase. Voici un exemple simple qui imprime l'emplacement du clic de la souris et le bouton qui a été pressé :

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)

La FigureCanvasBase.mpl_connectméthode renvoie un identifiant de connexion (un entier), qui peut être utilisé pour déconnecter le rappel via

fig.canvas.mpl_disconnect(cid)

Noter

Le canevas ne conserve que des références faibles aux méthodes d'instance utilisées comme rappels. Par conséquent, vous devez conserver une référence aux instances possédant de telles méthodes. Sinon, l'instance sera récupérée et le rappel disparaîtra.

Cela n'affecte pas les fonctions libres utilisées comme rappels.

Voici les événements auxquels vous pouvez vous connecter, les instances de classe qui vous sont renvoyées lorsque l'événement se produit et les descriptions des événements :

Nom de l'événement

Classer

La description

'bouton_press_event'

MouseEvent

le bouton de la souris est enfoncé

'bouton_release_event'

MouseEvent

le bouton de la souris est relâché

'fermer_événement'

CloseEvent

la figure est fermée

'draw_event'

DrawEvent

la toile a été dessinée (mais le widget d'écran n'est pas encore mis à jour)

'key_press_event'

KeyEvent

la touche est enfoncée

'key_release_event'

KeyEvent

la clé est libérée

'motion_notify_event'

MouseEvent

mouvements de souris

'choisir_événement'

PickEvent

l'artiste dans la toile est sélectionné

'resize_event'

ResizeEvent

la toile de la figure est redimensionnée

'scroll_event'

MouseEvent

la molette de défilement de la souris tourne

'figure_enter_event'

LocationEvent

la souris entre dans un nouveau chiffre

'figure_leave_event'

LocationEvent

la souris laisse une figure

'axes_enter_event'

LocationEvent

la souris entre dans un nouvel axe

'axes_leave_event'

LocationEvent

la souris laisse un axes

Noter

Lors de la connexion aux événements 'key_press_event' et 'key_release_event', vous pouvez rencontrer des incohérences entre les différents kits d'outils d'interface utilisateur avec lesquels Matplotlib fonctionne. Cela est dû à des incohérences/limitations de la boîte à outils de l'interface utilisateur. Le tableau suivant montre quelques exemples de base de ce que vous pouvez vous attendre à recevoir comme touche(s) (en utilisant une disposition de clavier QWERTY) des différents kits d'outils d'interface utilisateur, où une virgule sépare les différentes touches :

Touche(s) appuyée(s)

WxPython

Qt

WebAggName

Gtk

Tkinter

Mac OS X

Maj+2

Maj, Maj+2

décalage, @

décalage, @

décalage, @

décalage, @

décalage, @

Maj+F1

maj, maj+f1

maj, maj+f1

maj, maj+f1

maj, maj+f1

maj, maj+f1

maj, maj+f1

Décalage

décalage

décalage

décalage

décalage

décalage

décalage

Contrôler

contrôler

contrôler

contrôler

contrôler

contrôler

contrôler

Autre

autre

autre

autre

autre

autre

autre

GrAlt

Rien

Rien

autre

iso_level3_shift

iso_level3_shift

Verrouillage des majuscules

verrouillage des majuscules

verrouillage des majuscules

verrouillage des majuscules

verrouillage des majuscules

verrouillage des majuscules

verrouillage des majuscules

Verr Maj+a

majuscules, un

majuscules, un

majuscules, A

majuscules, A

majuscules, A

majuscules, un

un

un

un

un

un

un

un

Maj+a

décalage, un

décalage, un

décalage, un

décalage, un

décalage, un

décalage, un

Verr Maj+Maj+a

majuscules, maj, A

majuscules, maj, A

majuscules, maj, un

majuscules, maj, un

majuscules, maj, un

majuscules, maj, A

Ctrl+Maj+Alt

contrôle, ctrl+maj, ctrl+alt

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

control, ctrl+shift, ctrl+meta

contrôle, ctrl+maj, ctrl+alt+maj

Ctrl+Maj+a

contrôle, ctrl+maj, ctrl+A

contrôle, ctrl+maj, ctrl+A

contrôle, ctrl+maj, ctrl+A

contrôle, ctrl+maj, ctrl+A

contrôle, ctrl+maj, ctrl+a

contrôle, ctrl+maj, ctrl+A

F1

f1

f1

f1

f1

f1

f1

Ctrl+F1

contrôle, ctrl+f1

contrôle, ctrl+f1

contrôle, ctrl+f1

contrôle, ctrl+f1

contrôle, ctrl+f1

contrôle, Rien

Matplotlib attache certains rappels de touches par défaut pour l'interactivité ; ils sont documentés dans la section Raccourcis clavier de navigation .

Attributs d'événement #

Tous les événements Matplotlib héritent de la classe de base matplotlib.backend_bases.Event, qui stocke les attributs :

name

le nom de l'événement

canvas

l'instance FigureCanvas générant l'événement

guiEvent

l'événement GUI qui a déclenché l'événement Matplotlib

Les événements les plus courants qui sont le pain quotidien de la gestion des événements sont les événements d'appui/relâchement des touches et les événements d'appui/relâchement de la souris et de mouvement. Les classes KeyEventet MouseEventqui gèrent ces événements sont toutes deux dérivées de LocationEvent, qui a les attributs suivants

x,y

position x et y de la souris en pixels à partir de la gauche et du bas du canevas

inaxes

l' Axesinstance sur laquelle se trouve la souris, le cas échéant ; sinon Aucun

xdata,ydata

position x et y de la souris dans les coordonnées des données, si la souris est sur un axe

Regardons un exemple simple de canevas, où un simple segment de ligne est créé chaque fois qu'une souris est pressée :

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

Le MouseEventque nous venons d'utiliser est un LocationEvent, nous avons donc accès aux données et aux coordonnées des pixels via et . En plus des attributs, il a également(event.x, event.y)(event.xdata, event.ydata)LocationEvent

button

le bouton enfoncé : Aucun, MouseButton, 'haut' ou 'bas' (haut et bas sont utilisés pour les événements de défilement)

key

la touche enfoncée : Aucun, n'importe quel caractère, 'shift', 'win' ou 'control'

Exercice rectangle déplaçable #

Écrivez une classe de rectangle déplaçable qui est initialisée avec une Rectangleinstance mais qui déplacera son xy emplacement lorsqu'elle sera déplacée. Astuce : vous devrez stocker l' xyemplacement d'origine du rectangle qui est stocké en tant que rect.xy et vous connecter aux événements d'appui, de mouvement et de relâchement de la souris. Lorsque la souris est enfoncée, vérifiez si le clic se produit sur votre rectangle (voir Rectangle.contains) et si c'est le cas, stockez le rectangle xy et l'emplacement du clic de la souris dans les coordonnées de données. Dans le rappel d'événement de mouvement, calculez le deltax et le deltay du mouvement de la souris et ajoutez ces deltas à l'origine du rectangle que vous avez stocké. Le redessiner la figure. Lors de l'événement de relâchement du bouton, réinitialisez simplement toutes les données d'appui sur le bouton que vous avez stockées comme Aucune.

Voici la solution :

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édit supplémentaire : utilisez le blitting pour rendre le dessin animé plus rapide et plus fluide.

Solution de crédit supplémentaire :

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

Souris entrer et sortir #

Si vous souhaitez être averti lorsque la souris entre ou sort d'une figure ou d'axes, vous pouvez vous connecter aux événements d'entrée/sortie de figure/axes. Voici un exemple simple qui change les couleurs des axes et de l'arrière-plan de la figure sur lesquels se trouve la souris :

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

Cueillette d'objets #

Vous pouvez activer la sélection en définissant la pickerpropriété d'un Artist(comme Line2D, Text, Patch, Polygon, AxesImage, etc.)

La pickerpropriété peut être définie à l'aide de différents types :

None

Le picking est désactivé pour cet artiste (par défaut).

boolean

Si True, la sélection sera activée et l'artiste déclenchera un événement de sélection si l'événement de la souris est sur l'artiste.

callable

Si le sélecteur est un appelable, c'est une fonction fournie par l'utilisateur qui détermine si l'artiste est touché par l'événement de la souris. La signature est de déterminer le test de réussite. Si l'événement mouse est sur l'artiste, return ; est un dictionnaire de propriétés qui deviennent des attributs supplémentaires sur le .hit, props = picker(artist, mouseevent)hit = TruepropsPickEvent

La propriété de l'artiste pickradiuspeut en outre être définie sur une valeur de tolérance en points (il y a 72 points par pouce) qui détermine la distance à laquelle la souris peut se trouver tout en déclenchant un événement de souris.

Après avoir activé la sélection d'un artiste en définissant la picker propriété, vous devez connecter un gestionnaire au canevas de la figure pick_event pour obtenir des rappels de sélection sur les événements de pression de la souris. Le gestionnaire ressemble généralement à

def pick_handler(event):
    mouseevent = event.mouseevent
    artist = event.artist
    # now do something with this...

Le PickEventpassé à votre rappel a toujours les attributs suivants :

mouseevent

Les MouseEventqui génèrent l'événement pick. Voir event-attributes pour une liste d'attributs utiles sur l'événement de souris.

artist

Celui Artistqui a généré l'événement pick.

De plus, certains artistes aiment Line2Det PatchCollectionpeuvent joindre des métadonnées supplémentaires, comme les indices des données qui répondent aux critères du sélecteur (par exemple, tous les points de la ligne qui se situent dans la pickradiustolérance spécifiée).

Exemple de cueillette simple #

Dans l'exemple ci-dessous, nous activons la sélection sur la ligne et définissons une tolérance de rayon de sélection en points. La onpick fonction de rappel sera appelée lorsque l'événement de sélection se situe dans la distance de tolérance de la ligne et a les indices des sommets de données qui se trouvent dans la tolérance de distance de sélection. Notre onpick fonction de rappel imprime simplement les données qui se trouvent sous l'emplacement de prélèvement. Différents artistes Matplotlib peuvent joindre différentes données au PickEvent. Par exemple, Line2Dattache la propriété ind, qui sont les indices dans les données de ligne sous le point de sélection. Voir Line2D.pickpour plus de détails sur les PickEventpropriétés de la ligne.

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

Exercice de cueillette #

Créez un ensemble de données de 100 tableaux de 1000 nombres aléatoires gaussiens et calculez la moyenne de l'échantillon et l'écart type de chacun d'eux (indice : les tableaux NumPy ont une méthode moyenne et std) et faites un tracé de marqueur xy des 100 moyennes par rapport aux 100 écarts-types. Connectez la ligne créée par la commande plot à l'événement pick et tracez la série chronologique d'origine des données qui ont généré les points cliqués. Si plusieurs points se trouvent dans la tolérance du point sur lequel vous avez cliqué, vous pouvez utiliser plusieurs sous-parcelles pour tracer les multiples séries temporelles.

Solution d'exercice :

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