Introdução
Neste tutorial, você aprenderá como criar um rótulo de ângulo invariante à escala usando Matplotlib. A anotação de ângulo é frequentemente usada para marcar ângulos entre linhas ou dentro de formas com um arco circular. Embora o Matplotlib forneça um ~.patches.Arc, um problema inerente ao usá-lo diretamente para tais propósitos é que um arco sendo circular no espaço de dados não é necessariamente circular no espaço de exibição. Além disso, o raio do arco é frequentemente melhor definido em um sistema de coordenadas que é independente das coordenadas reais dos dados - pelo menos se você quiser poder ampliar livremente seu gráfico sem que a anotação cresça até o infinito. Isso exige uma solução onde o centro do arco é definido no espaço de dados, mas seu raio em uma unidade física como pontos ou pixels, ou como uma proporção da dimensão dos Eixos (Axes).
Dicas para a VM (VM Tips)
Após a inicialização da VM, clique no canto superior esquerdo para mudar para a aba Notebook e acessar o Jupyter Notebook para praticar.
Às vezes, pode ser necessário aguardar alguns segundos para que o Jupyter Notebook termine de carregar. A validação das operações não pode ser automatizada devido a limitações no Jupyter Notebook.
Se você enfrentar problemas durante o aprendizado, sinta-se à vontade para perguntar ao Labby. Forneça feedback após a sessão, e resolveremos o problema prontamente para você.
Importar as Bibliotecas Necessárias
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Arc
from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox
Definir a Classe AngleAnnotation
class AngleAnnotation(Arc):
"""
Desenha um arco entre dois vetores que aparece circular no espaço de exibição.
"""
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 ou array de dois floats
Posição central e dois pontos. A anotação de ângulo é desenhada entre
os dois vetores conectando *p1* e *p2* com *xy*, respectivamente.
As unidades são coordenadas de dados.
size : float
Diâmetro da anotação de ângulo em unidades especificadas por *unit*.
unit : str
Uma das seguintes strings para especificar a unidade de *size*:
* "pixels": pixels
* "points": pontos, use pontos em vez de pixels para não ter uma
dependência do DPI
* "axes width", "axes height": unidades relativas da largura e altura dos Eixos (Axes)
* "axes min", "axes max": mínimo ou máximo da largura e altura relativas dos Eixos (Axes)
ax : `matplotlib.axes.Axes`
Os Eixos (Axes) para adicionar a anotação de ângulo.
text : str
O texto para marcar o ângulo com.
textposition : {"inside", "outside", "edge"}
Se deve mostrar o texto dentro ou fora do arco. "edge" pode ser usado
para posições personalizadas ancoradas na borda do arco.
text_kw : dict
Dicionário de argumentos passados para a Anotação.
**kwargs
Outros parâmetros são passados para `matplotlib.patches.Arc`. Use isso
para especificar, cor, largura da linha, etc. do arco.
"""
self.ax = ax or plt.gca()
self._xydata = xy ## em coordenadas de dados
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):
"""retorna o centro em pixels"""
return self.ax.transData.transform(self._xydata)
def set_center(self, xy):
"""define o centro em coordenadas de dados"""
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
## Redefinir atributos do Arc para sempre dar valores no espaço de 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)
## Os dois métodos a seguir são necessários para atualizar a posição do 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 a Função 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)
Plotar Duas Linhas Cruzadas e Rotular Cada Ângulo Entre Elas com a Ferramenta AngleAnnotation Acima.
fig, ax = plt.subplots()
fig.canvas.draw() ## Precisa desenhar a figura para definir o renderizador (renderer)
ax.set_title("Exemplo de AngleLabel")
## Plotar duas linhas cruzadas e rotular cada ângulo entre elas com a ferramenta
## ``AngleAnnotation`` acima.
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$")
## Apresentar algumas opções de estilo para o arco do ângulo, bem como o 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()
Apresentar Diferentes Posições de Texto e Unidades de Tamanho
fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
fig.suptitle("Argumentos de palavra-chave (keyword arguments) AngleLabel")
fig.canvas.draw() ## Precisa desenhar a figura para definir o renderizador (renderer)
## Apresentar diferentes posições 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", custom arrow']):
ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(),
bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8,
annotation_clip=True)
## Apresentar diferentes unidades de tamanho. O efeito disso pode ser melhor observado
## mudando interativamente o tamanho da 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()
Resumo
Neste tutorial, você aprendeu como criar um rótulo de ângulo invariante à escala usando Matplotlib. A funcionalidade da classe AngleAnnotation permite que você anote o arco com um texto. Você também pode modificar a localização do rótulo de texto, bem como as unidades de tamanho.