スケール不変の角度ラベルの作成

PythonPythonBeginner
今すぐ練習

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

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このチュートリアルでは、Matplotlibを使ってスケール不変な角度ラベルを作成する方法を学びます。角度の注釈は、線の間や円弧を持つ形状の内部の角度をマークするためによく使われます。Matplotlibは~.patches.Arcを提供していますが、この目的に直接使用する際の固有の問題は、データ空間で円形である弧が表示空間で必ずしも円形であるとは限らないことです。また、弧の半径は、少なくともプロットを自由にズームしても注釈が無限大にならないようにするために、実際のデータ座標とは独立した座標系で定義するのが最適です。これには、弧の中心がデータ空間で定義され、半径がポイントやピクセルなどの物理単位で、またはAxes寸法の比率として定義される解決策が必要です。

VMのヒント

VMの起動が完了したら、左上隅をクリックしてノートブックタブに切り替えて、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):
    """
    表示空間で円形に見える2つのベクトル間の弧を描画します。
    """
    def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
                 text="", textposition="inside", text_kw=None, **kwargs):
        """
        パラメータ
        ----------
        xy, p1, p2 : 2つの浮動小数点数のタプルまたは配列
            中心位置と2つの点。角度注釈は、それぞれ *p1* と *p2* を *xy* と接続する2つのベクトルの間に描画されます。
            単位はデータ座標です。

        size : 浮動小数点数
            *unit* で指定された単位での角度注釈の直径。

        unit : 文字列
            *size* の単位を指定するための次の文字列の1つ:

            * "pixels": ピクセル
            * "points": ポイント。DPIに依存しないように、ピクセルの代わりにポイントを使用します
            * "axes width", "axes height": Axes幅、高さの相対単位
            * "axes min", "axes max": 相対Axes幅、高さの最小値または最大値

        ax : `matplotlib.axes.Axes`
            角度注釈を追加するAxes。

        text : 文字列
            角度をマークするためのテキスト。

        textposition : {"inside", "outside", "edge"}
            テキストを弧の内側または外側に表示するかどうか。「edge」は、弧の端に固定されたカスタム位置に使用できます。

        text_kw : 辞書
            注釈に渡される引数の辞書。

        **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 self.ax.transData.transform(self._xydata)

    def set_center(self, xy):
        """データ座標での中心を設定する"""
        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)

    ## 以下の2つのメソッドは、テキスト位置を更新するために必要です。
    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)

2本の交差する線を描画し、それらの間の各角度に上記の AngleAnnotation ツールを使ってラベルを付ける。

fig, ax = plt.subplots()
fig.canvas.draw()  ## レンダラーを定義するために図を描画する必要があります
ax.set_title("AngleLabel example")

## 2本の交差する線を描画し、それらの間の各角度に上記の
## ``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 keyword arguments")
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 クラスの機能を使うと、弧にテキストで注釈を付けることができます。また、テキストラベルの位置やサイズ単位も変更できます。