Crear una etiqueta de ángulo invariante a la escala

PythonPythonBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

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.