Création d'un angle invariant par rapport à l'échelle

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce tutoriel, vous allez apprendre à créer une étiquette d'angle invariant par échelle à l'aide de Matplotlib. L'annotation d'angle est souvent utilisée pour marquer les angles entre des lignes ou à l'intérieur de formes avec un arc circulaire. Bien que Matplotlib fournisse un ~.patches.Arc, un problème inhérent lorsqu'on l'utilise directement à de telles fins est que l'arc étant circulaire dans l'espace de données n'est pas nécessairement circulaire dans l'espace d'affichage. De plus, le rayon de l'arc est souvent mieux défini dans un système de coordonnées qui est indépendant des coordonnées de données réelles - du moins si vous voulez être en mesure de zoomer librement sur votre graphe sans que l'annotation ne devienne infinie. Cela nécessite une solution où le centre de l'arc est défini dans l'espace de données, mais son rayon en une unité physique comme les points ou les pixels, ou comme un rapport des dimensions de l'Axe.

Conseils sur la machine virtuelle

Une fois le démarrage de la machine virtuelle terminé, cliquez sur le coin supérieur gauche pour basculer vers l'onglet Carnet de notes pour accéder au carnet Jupyter pour pratiquer.

Parfois, vous devrez peut-être attendre quelques secondes pour que le carnet Jupyter ait fini de charger. La validation des opérations ne peut pas être automatisée en raison des limites du carnet Jupyter.

Si vous rencontrez des problèmes pendant l'apprentissage, n'hésitez pas à demander à Labby. Donnez votre feedback après la session, et nous réglerons rapidement le problème pour vous.

Importez les bibliothèques requises

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.patches import Arc
from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox

Définissez la classe AngleAnnotation

class AngleAnnotation(Arc):
    """
    Dessine un arc entre deux vecteurs qui apparaît circulaire dans l'espace d'affichage.
    """
    def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
                 text="", textposition="inside", text_kw=None, **kwargs):
        """
        Paramètres
        ----------
        xy, p1, p2 : tuple ou tableau de deux flottants
            Position du centre et deux points. L'annotation d'angle est dessinée entre
            les deux vecteurs reliant *p1* et *p2* à *xy*, respectivement.
            Les unités sont les coordonnées de données.

        size : float
            Diamètre de l'annotation d'angle en unités spécifiées par *unit*.

        unit : str
            L'une des chaînes suivantes pour spécifier l'unité de *size* :

            * "pixels" : pixels
            * "points" : points, utilisez des points au lieu de pixels pour ne pas dépendre du DPI
            * "largeur d'axe", "hauteur d'axe" : unités relatives de la largeur, de la hauteur de l'Axe
            * "minimum d'axe", "maximum d'axe" : minimum ou maximum de la largeur, de la hauteur relative de l'Axe

        ax : `matplotlib.axes.Axes`
            L'Axe sur lequel ajouter l'annotation d'angle.

        text : str
            Le texte pour marquer l'angle.

        textposition : {"inside", "outside", "edge"}
            Indique s'il faut afficher le texte à l'intérieur ou à l'extérieur de l'arc. "edge" peut être utilisé
            pour des positions personnalisées ancrées au bord de l'arc.

        text_kw : dict
            Dictionnaire d'arguments passés à l'annotation.

        **kwargs
            D'autres paramètres sont passés à `matplotlib.patches.Arc`. Utilisez ceci
            pour spécifier la couleur, la largeur de ligne, etc. de l'arc.

        """
        self.ax = ax or plt.gca()
        self._xydata = xy  ## en coordonnées de données
        self.vec1 = p1
        self.vec2 = p2
        self.size = size
        self.unit = unit
        self.textposition = textposition

        super().__init__(self._xydata, size, size, angle=0.0,
                         theta1=self.theta1, theta2=self.theta2, **kwargs)

        self.set_transform(IdentityTransform())
        self.ax.add_patch(self)

        self.kw = dict(ha="center", va="center",
                       xycoords=IdentityTransform(),
                       xytext=(0, 0), textcoords="offset points",
                       annotation_clip=True)
        self.kw.update(text_kw or {})
        self.text = ax.annotate(text, xy=self._center, **self.kw)

    def get_size(self):
        facteur = 1.
        if self.unit == "points":
            facteur = self.ax.figure.dpi / 72.
        elif self.unit[:4] == "axes":
            b = TransformedBbox(Bbox.unit(), self.ax.transAxes)
            dic = {"max": max(b.width, b.height),
                   "min": min(b.width, b.height),
                   "width": b.width, "height": b.height}
            facteur = dic[self.unit[5:]]
        return self.size * facteur

    def set_size(self, size):
        self.size = size

    def get_center_in_pixels(self):
        """renvoie le centre en pixels"""
        return self.ax.transData.transform(self._xydata)

    def set_center(self, xy):
        """définit le centre en coordonnées de données"""
        self._xydata = xy

    def get_theta(self, vec):
        vec_in_pixels = self.ax.transData.transform(vec) - self._center
        return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0]))

    def get_theta1(self):
        return self.get_theta(self.vec1)

    def get_theta2(self):
        return self.get_theta(self.vec2)

    def set_theta(self, angle):
        pass

    ## Redéfinissez les attributs de l'Arc pour toujours donner des valeurs dans l'espace en pixels
    _center = property(get_center_in_pixels, set_center)
    theta1 = property(get_theta1, set_theta)
    theta2 = property(get_theta2, set_theta)
    width = property(get_size, set_size)
    height = property(get_size, set_size)

    ## Les deux méthodes suivantes sont nécessaires pour mettre à jour la position du texte.
    def draw(self, renderer):
        self.update_text()
        super().draw(renderer)

    def update_text(self):
        c = self._center
        s = self.get_size()
        angle_span = (self.theta2 - self.theta1) % 360
        angle = np.deg2rad(self.theta1 + angle_span / 2)
        r = s / 2
        if self.textposition == "inside":
            r = s / np.interp(angle_span, [60, 90, 135, 180],
                                          [3.3, 3.5, 3.8, 4])
        self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)])
        if self.textposition == "outside":
            def R90(a, r, w, h):
                if a < np.arctan(h/2/(r+w/2)):
                    return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2)
                else:
                    c = np.sqrt((w/2)**2+(h/2)**2)
                    T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r)
                    xy = r * np.array([np.cos(a + T), np.sin(a + T)])
                    xy += np.array([w/2, h/2])
                    return np.sqrt(np.sum(xy**2))

            def R(a, r, w, h):
                aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \
                     (np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4)
                return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))])

            bbox = self.text.get_window_extent()
            X = R(angle, r, bbox.width, bbox.height)
            trans = self.ax.figure.dpi_scale_trans.inverted()
            offs = trans.transform(((X-s/2), 0))[0] * 72
            self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)])

Définissez la fonction d'aide plot_angle

def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs):
    vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))])
    xy = np.c_[[length, 0], [0, 0], vec2*length].T + np.array(pos)
    ax.plot(*xy.T, color=acol)
    return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs)

Tracez deux lignes qui se croisent et étiquetez chaque angle entre elles avec l'outil AngleAnnotation ci-dessus.

fig, ax = plt.subplots()
fig.canvas.draw()  ## Il est nécessaire de tracer la figure pour définir le renderer
ax.set_title("Exemple d'AngleLabel")

## Tracez deux lignes qui se croisent et étiquetez chaque angle entre elles avec l'outil
## ``AngleAnnotation`` ci-dessus.
centre = (4.5, 650)
p1 = [(2.5, 710), (6.0, 605)]
p2 = [(3.0, 275), (5.5, 900)]
ligne1, = ax.plot(*zip(*p1))
ligne2, = ax.plot(*zip(*p2))
point, = ax.plot(*centre, marqueur="o")

am1 = AngleAnnotation(centre, p1[1], p2[1], ax=ax, taille=75, texte=r"$\alpha$")
am2 = AngleAnnotation(centre, p2[1], p1[0], ax=ax, taille=35, texte=r"$\beta$")
am3 = AngleAnnotation(centre, p1[0], p2[0], ax=ax, taille=75, texte=r"$\gamma$")
am4 = AngleAnnotation(centre, p2[0], p1[1], ax=ax, taille=35, texte=r"$\theta$")


## Montrez quelques options de style pour l'arc d'angle, ainsi que le texte.
p = [(6.0, 400), (5.3, 410), (5.6, 300)]
ax.plot(*zip(*p))
am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax, taille=40, texte=r"$\Phi$",
                      style_de_ligne="--", couleur="gris", position_du_texte="à l'extérieur",
                      kw_du_texte=dict(taille_de_la_police=16, couleur="gris"))

plt.show()

Montrez différentes positions de texte et unités de taille

fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
fig.suptitle("Arguments clés d'AngleLabel")
fig.canvas.draw()  ## Il est nécessaire de tracer la figure pour définir le renderer

## Montrez différentes positions de texte.
ax1.marges(y=0.4)
ax1.set_title("textposition")
kw = dict(taille=75, unité="points", texte=r"$60°$")

am6 = plot_angle(ax1, (2.0, 0), 60, textposition="à l'intérieur", **kw)
am7 = plot_angle(ax1, (3.5, 0), 60, textposition="à l'extérieur", **kw)
am8 = plot_angle(ax1, (5.0, 0), 60, textposition="bord",
                 text_kw=dict(bbox=dict(boxstyle="arrondi", fc="w")), **kw)
am9 = plot_angle(ax1, (6.5, 0), 60, textposition="bord",
                 text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->",
                              connectionstyle="arc3,rad=-0.2")), **kw)

for x, texte in zip([2.0, 3.5, 5.0, 6.5], ['"à l'intérieur"', '"à l'extérieur"', '"bord"',
                                          '"bord", flèche personnalisée']):
    ax1.annotate(texte, xy=(x, 0), xycoords=ax1.get_xaxis_transform(),
                 bbox=dict(boxstyle="arrondi", fc="w"), ha="gauche", taille_de_la_police=8,
                 annotation_clip=True)

## Montrez différentes unités de taille. L'effet de cela peut être observé au mieux
## en modifiant interactivement la taille de la figure
ax2.marges(y=0.4)
ax2.set_title("unité")
kw = dict(texte=r"$60°$", textposition="à l'extérieur")

am10 = plot_angle(ax2, (2.0, 0), 60, taille=50, unité="pixels", **kw)
am11 = plot_angle(ax2, (3.5, 0), 60, taille=50, unité="points", **kw)
am12 = plot_angle(ax2, (5.0, 0), 60, taille=0.25, unité="minimum d'axe", **kw)
am13 = plot_angle(ax2, (6.5, 0), 60, taille=0.25, unité="maximum d'axe", **kw)

for x, texte in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"',
                                          '"minimum d'axe"', '"maximum d'axe"']):
    ax2.annotate(texte, xy=(x, 0), xycoords=ax2.get_xaxis_transform(),
                 bbox=dict(boxstyle="arrondi", fc="w"), ha="gauche", taille_de_la_police=8,
                 annotation_clip=True)

plt.show()

Sommaire

Dans ce tutoriel, vous avez appris à créer une étiquette d'angle invariante par rapport à l'échelle à l'aide de Matplotlib. La fonctionnalité de la classe AngleAnnotation vous permet d'ajouter une annotation de texte sur l'arc. Vous pouvez également modifier l'emplacement de l'étiquette de texte, ainsi que les unités de taille.