Matplotlib 을 이용한 크기 불변 각도 레이블 생성

Beginner

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

소개

이 튜토리얼에서는 Matplotlib 을 사용하여 스케일 불변 각도 레이블을 만드는 방법을 배웁니다. 각도 주석은 종종 선 사이 또는 원형 호를 사용하여 도형 내부의 각도를 표시하는 데 사용됩니다. Matplotlib 은 ~.patches.Arc를 제공하지만, 이러한 목적으로 직접 사용할 때 내재된 문제는 데이터 공간에서 원형인 호가 디스플레이 공간에서 반드시 원형이 아니라는 것입니다. 또한, 호의 반경은 실제 데이터 좌표와 독립적인 좌표계에서 정의하는 것이 가장 좋습니다. 적어도 주석이 무한대로 커지지 않고 플롯을 자유롭게 확대/축소할 수 있으려면 말입니다. 이는 호의 중심은 데이터 공간에서 정의되지만 반경은 포인트 또는 픽셀과 같은 물리적 단위 또는 Axes 차원의 비율로 정의되는 솔루션을 필요로 합니다.

VM 팁

VM 시작이 완료되면 왼쪽 상단을 클릭하여 Notebook 탭으로 전환하여 실습을 위해 Jupyter Notebook에 액세스하십시오.

때로는 Jupyter Notebook 이 로딩을 완료하는 데 몇 초 정도 기다려야 할 수 있습니다. Jupyter Notebook 의 제한으로 인해 작업의 유효성 검사를 자동화할 수 없습니다.

학습 중에 문제가 발생하면 언제든지 Labby 에게 문의하십시오. 세션 후 피드백을 제공해주시면 문제를 신속하게 해결해 드리겠습니다.

필요한 라이브러리 가져오기

import matplotlib.pyplot as plt
import numpy as np

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

AngleAnnotation 클래스 정의

class AngleAnnotation(Arc):
    """
    디스플레이 공간에서 원형으로 보이는 두 벡터 사이의 호를 그립니다.
    """
    def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
                 text="", textposition="inside", text_kw=None, **kwargs):
        """
        매개변수
        ----------
        xy, p1, p2 : 튜플 또는 두 개의 float 배열
            중심 위치와 두 점. 각도 주석은 *p1*과 *p2*를 *xy*에 각각 연결하는 두 벡터 사이에 그려집니다.
            단위는 데이터 좌표입니다.

        size : float
            *unit*으로 지정된 단위의 각도 주석의 지름입니다.

        unit : str
            *size*의 단위를 지정하는 다음 문자열 중 하나:

            * "pixels": 픽셀
            * "points": 포인트, DPI 에 의존하지 않도록 픽셀 대신 포인트를 사용
            * "axes width", "axes height": Axes 너비, 높이의 상대 단위
            * "axes min", "axes max": 상대 Axes 너비, 높이의 최소 또는 최대

        ax : `matplotlib.axes.Axes`
            각도 주석을 추가할 Axes 입니다.

        text : str
            각도를 표시할 텍스트입니다.

        textposition : {"inside", "outside", "edge"}
            텍스트를 호 안 또는 밖에 표시할지 여부입니다. "edge"는 호의 가장자리에 고정된 사용자 정의 위치에 사용할 수 있습니다.

        text_kw : dict
            Annotation 에 전달되는 인수의 사전입니다.

        **kwargs
            추가 매개변수는 `matplotlib.patches.Arc` 에 전달됩니다. 호의 색상, 선 너비 등을 지정하는 데 사용합니다.

        """
        self.ax = ax or plt.gca()
        self._xydata = xy  ## 데이터 좌표
        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):
        """return center in pixels"""
        return self.ax.transData.transform(self._xydata)

    def set_center(self, xy):
        """set center in data coordinates"""
        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

    ## Arc 의 속성을 재정의하여 항상 픽셀 공간의 값을 제공합니다.
    _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)

    ## 텍스트 위치를 업데이트하려면 다음 두 가지 메서드가 필요합니다.
    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)])

헬퍼 함수 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)

두 개의 교차하는 선을 그리고 위에 정의된 AngleAnnotation 도구를 사용하여 각 각도에 레이블을 지정합니다.

fig, ax = plt.subplots()
fig.canvas.draw()  ## 렌더러를 정의하려면 그림을 그려야 합니다.
ax.set_title("AngleLabel 예시")

## 두 개의 교차하는 선을 그리고 위에 정의된
## ``AngleAnnotation`` 도구를 사용하여 각 각도에 레이블을 지정합니다.
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$")


## 각도 호와 텍스트에 대한 몇 가지 스타일 옵션을 보여줍니다.
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()

다양한 텍스트 위치 및 크기 단위 시연

fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
fig.suptitle("AngleLabel 키워드 인수")
fig.canvas.draw()  ## 렌더러를 정의하려면 그림을 그려야 합니다.

## 다양한 텍스트 위치를 시연합니다.
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)

## 다양한 크기 단위를 시연합니다. 이 효과는 그림 크기를 대화식으로 변경하여 가장 잘 관찰할 수 있습니다.
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()

요약

이 튜토리얼에서는 Matplotlib 을 사용하여 크기 불변 각도 레이블을 만드는 방법을 배웠습니다. AngleAnnotation 클래스의 기능을 통해 텍스트로 호 (arc) 에 주석을 달 수 있습니다. 또한 텍스트 레이블의 위치와 크기 단위를 수정할 수 있습니다.