Introducción
En este tutorial, aprenderá a crear una etiqueta de ángulo invariante a la escala utilizando Matplotlib. La anotación de ángulos se utiliza a menudo para marcar los ángulos entre líneas o dentro de formas con un arco circular. Si bien Matplotlib proporciona un ~.patches.Arc, un problema inherente al usarlo directamente para este propósito es que un arco circular en el espacio de datos no necesariamente es circular en el espacio de visualización. Además, el radio del arco a menudo se define mejor en un sistema de coordenadas que es independiente de las coordenadas de datos reales - al menos si desea poder hacer un zoom libremente en su gráfico sin que la anotación crezca hasta el infinito. Esto requiere una solución donde el centro del arco se defina en el espacio de datos, pero su radio en una unidad física como puntos o píxeles, o como una proporción de la dimensión de los Ejes.
Consejos sobre la VM
Una vez finalizada la inicialización de la VM, haga clic en la esquina superior izquierda para cambiar a la pestaña Cuaderno y acceder a Jupyter Notebook para practicar.
A veces, es posible que tenga que esperar unos segundos a que Jupyter Notebook termine de cargarse. La validación de operaciones no puede automatizarse debido a las limitaciones de Jupyter Notebook.
Si tiene problemas durante el aprendizaje, no dude en preguntar a Labby. Deje sus comentarios después de la sesión y lo resolveremos rápidamente para usted.
Importar las bibliotecas necesarias
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Arc
from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox
Definir la clase AngleAnnotation
class AngleAnnotation(Arc):
"""
Dibuja un arco entre dos vectores que parece circular en el espacio de visualización.
"""
def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
text="", textposition="inside", text_kw=None, **kwargs):
"""
Parámetros
----------
xy, p1, p2 : tupla o matriz de dos flotantes
Posición del centro y dos puntos. La anotación de ángulo se dibuja entre
los dos vectores que conectan *p1* y *p2* con *xy*, respectivamente.
Las unidades son coordenadas de datos.
size : float
Diámetro de la anotación de ángulo en las unidades especificadas por *unit*.
unit : str
Una de las siguientes cadenas para especificar la unidad de *size*:
* "pixels": píxeles
* "points": puntos, use puntos en lugar de píxeles para no depender del DPI
* "axes width", "axes height": unidades relativas del ancho, alto de los Ejes
* "axes min", "axes max": mínimo o máximo de la anchura, altura relativa de los Ejes
ax : `matplotlib.axes.Axes`
Los Ejes donde se agregará la anotación de ángulo.
text : str
El texto para marcar el ángulo.
textposition : {"inside", "outside", "edge"}
Si se muestra el texto dentro o fuera del arco. "edge" se puede usar
para posiciones personalizadas ancladas en el borde del arco.
text_kw : dict
Diccionario de argumentos pasados a la Anotación.
**kwargs
Otros parámetros se pasan a `matplotlib.patches.Arc`. Use esto
para especificar el color, el ancho de línea, etc. del arco.
"""
self.ax = ax or plt.gca()
self._xydata = xy ## en coordenadas de datos
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):
factor = 1.
if self.unit == "points":
factor = 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}
factor = dic[self.unit[5:]]
return self.size * factor
def set_size(self, size):
self.size = size
def get_center_in_pixels(self):
"""devuelve el centro en píxeles"""
return self.ax.transData.transform(self._xydata)
def set_center(self, xy):
"""establece el centro en coordenadas de datos"""
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
## Redefine los atributos de Arc para siempre dar valores en el espacio de píxeles
_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)
## Los siguientes dos métodos son necesarios para actualizar la posición del texto.
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)])
Definir la función auxiliar 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)
Trazar dos líneas que se cruzan y etiquetar cada ángulo entre ellas con la herramienta AngleAnnotation descrita anteriormente.
fig, ax = plt.subplots()
fig.canvas.draw() ## Es necesario dibujar la figura para definir el renderer
ax.set_title("Ejemplo de AngleLabel")
## Trazar dos líneas que se cruzan y etiquetar cada ángulo entre ellas con la
## herramienta ``AngleAnnotation`` descrita anteriormente.
center = (4.5, 650)
p1 = [(2.5, 710), (6.0, 605)]
p2 = [(3.0, 275), (5.5, 900)]
line1, = ax.plot(*zip(*p1))
line2, = ax.plot(*zip(*p2))
point, = ax.plot(*center, marker="o")
am1 = AngleAnnotation(center, p1[1], p2[1], ax=ax, size=75, text=r"$\alpha$")
am2 = AngleAnnotation(center, p2[1], p1[0], ax=ax, size=35, text=r"$\beta$")
am3 = AngleAnnotation(center, p1[0], p2[0], ax=ax, size=75, text=r"$\gamma$")
am4 = AngleAnnotation(center, p2[0], p1[1], ax=ax, size=35, text=r"$\theta$")
## Mostrar algunas opciones de estilo para el arco del ángulo, así como para el texto.
p = [(6.0, 400), (5.3, 410), (5.6, 300)]
ax.plot(*zip(*p))
am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax, size=40, text=r"$\Phi$",
linestyle="--", color="gray", textposition="outside",
text_kw=dict(fontsize=16, color="gray"))
plt.show()
Mostrar diferentes posiciones de texto y unidades de tamaño
fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
fig.suptitle("Argumentos de palabras clave de AngleLabel")
fig.canvas.draw() ## Es necesario dibujar la figura para definir el renderer
## Mostrar diferentes posiciones de texto.
ax1.margins(y=0.4)
ax1.set_title("textposition")
kw = dict(size=75, unit="points", text=r"$60°$")
am6 = plot_angle(ax1, (2.0, 0), 60, textposition="inside", **kw)
am7 = plot_angle(ax1, (3.5, 0), 60, textposition="outside", **kw)
am8 = plot_angle(ax1, (5.0, 0), 60, textposition="edge",
text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw)
am9 = plot_angle(ax1, (6.5, 0), 60, textposition="edge",
text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->",
connectionstyle="arc3,rad=-0.2")), **kw)
for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"',
'"edge", flecha personalizada']):
ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(),
bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8,
annotation_clip=True)
## Mostrar diferentes unidades de tamaño. El efecto de esto se puede observar mejor
## cambiando interactivamente el tamaño de la figura
ax2.margins(y=0.4)
ax2.set_title("unit")
kw = dict(text=r"$60°$", textposition="outside")
am10 = plot_angle(ax2, (2.0, 0), 60, size=50, unit="pixels", **kw)
am11 = plot_angle(ax2, (3.5, 0), 60, size=50, unit="points", **kw)
am12 = plot_angle(ax2, (5.0, 0), 60, size=0.25, unit="axes min", **kw)
am13 = plot_angle(ax2, (6.5, 0), 60, size=0.25, unit="axes max", **kw)
for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"',
'"axes min"', '"axes max"']):
ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(),
bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8,
annotation_clip=True)
plt.show()
Resumen
En este tutorial, aprendiste cómo crear una etiqueta de ángulo invariante a la escala utilizando Matplotlib. La funcionalidad de la clase AngleAnnotation te permite anotar el arco con un texto. También puedes modificar la ubicación de la etiqueta de texto, así como las unidades de tamaño.