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.