matplotlibの高級用法#

import numpy as np
import helper.matplotlib
from matplotlib import pyplot as plt

座標変換と注釈#

一つの図表には複数の座標系と座標変換が関わっており、各種座標系の意味を理解し、その使い方をマスターすることで、matplotlibを使って理想的な図表を自由自在に描くことができます。この節では、図表中の文字、矢印、注釈を例に、各種座標系とその変換について紹介します。

def func1(x):  #❶
    return 0.6 * x + 0.3


def func2(x):  #❶
    return 0.4 * x * x + 0.1 * x + 0.2


def find_curve_intersects(x, y1, y2):
    d = y1 - y2
    idx = np.where(d[:-1] * d[1:] <= 0)[0]
    x1, x2 = x[idx], x[idx + 1]
    d1, d2 = d[idx], d[idx + 1]
    return -d1 * (x2 - x1) / (d2 - d1) + x1


x = np.linspace(-3, 3, 100)  #❷
f1 = func1(x)
f2 = func2(x)
fig, ax = plt.subplots(figsize=(8, 4), dpi=80)
ax.plot(x, f1)
ax.plot(x, f2)

x1, x2 = find_curve_intersects(x, f1, f2)  #❸
ax.plot(x1, func1(x1), "o")
ax.plot(x2, func1(x2), "o")

ax.fill_between(x, f1, f2, where=f1 > f2, facecolor="green", alpha=0.5)  #❹

from matplotlib import transforms

trans = transforms.blended_transform_factory(ax.transData, ax.transAxes)
ax.fill_between([x1, x2], 0, 1, transform=trans, alpha=0.1)  #❺

a = ax.text(
    0.05,
    0.95,
    "直線と二次曲線の交点",  #❻
    transform=ax.transAxes,
    verticalalignment="top",
    fontsize=18,
    bbox={"facecolor": "red", "alpha": 0.4, "pad": 10},
)

arrow = {
    "arrowstyle": "fancy,tail_width=0.6",
    "facecolor": "gray",
    "connectionstyle": "arc3,rad=-0.3",
}

ax.annotate(
    "交点",  #❼
    xy=(x1, func1(x1)),
    xycoords="data",
    xytext=(0.05, 0.5),
    textcoords="axes fraction",
    arrowprops=arrow,
)

ax.annotate(
    "交点",  #❼
    xy=(x2, func1(x2)),
    xycoords="data",
    xytext=(0.05, 0.5),
    textcoords="axes fraction",
    arrowprops=arrow,
)

xm = (x1 + x2) / 2
ym = (func1(xm) - func2(xm)) / 2 + func2(xm)
o = ax.annotate(
    "直線が曲線より大きい領域",  #❼
    xy=(xm, ym),
    xycoords="data",
    xytext=(30, -30),
    textcoords="offset points",
    bbox={"boxstyle": "round", "facecolor": (1.0, 0.7, 0.7), "edgecolor": "none"},
    fontsize=16,
    arrowprops={"arrowstyle": "->"},
)

ax.set_xlim(-3, 3)
ax.set_ylim(-2, 5);

プログラムの出力は上のグラフのようになります。図中では以下の注釈効果が示されています:

  • 二つの小さな円点で直線と曲線の交点を示しています。

  • 二つの交点の間で、直線と曲線の間の面積を塗りつぶしています。

  • 二つの交点の間の区間を示すために、高さがサブプロット全体の高さで、左右の辺が二つの交点を通る矩形を使用しています。

  • 図の左上隅に説明文を配置しています。

  • 二つの交点と塗りつぶされた面積に矢印付きの注釈を使用しています。

まず、❶func1func2という二つの関数を定義しています。これらはそれぞれ直線と二次曲線を計算する関数です。次に❷これらの関数を区間(-3, 3)で計算し、plot()を呼び出して曲線図を描画します。

❸二つの交点をマークするために、find_curve_intersects()を使用して二つの曲線f1f2の交点に対応するX軸座標x1x2を計算します。交点の小さな円点はplot()を使用して描画され、この時渡されるX-Y軸のデータは単一の数値で、'o'をスタイルとして描画します。

二つの曲線の交点を計算する方法

二つの曲線のY軸座標値y1y2が同じX軸座標配列xで計算されている場合、それらの交点を簡単に計算できます。まず、二つの曲線のY軸の差d = y1 - y2を計算し、符号が反対の連続する二つの差のインデックスidxidx + 1を見つけます。直線(x[idx],d[idx])-(x[idx+1],d[idx+1])とX軸の交点を計算することで、二つの曲線の交点のX軸座標xcを得ることができます。交点のY軸座標を計算するには、np.interp(xc, x, y1)を呼び出して曲線を線形補間します。

❹次に、fill_between()を呼び出して、X軸上で二つの交点の間、Y軸上で二つの曲線の間の面積部分を描画し、facecoloralphaパラメータで塗りつぶしの色と透明度を指定します。fill_between()の呼び出しパラメータは以下の通りです:

fill_between(x, y1, y2=0, where=None)

ここで、xパラメータは長さNの配列で、y1y2パラメータは長さNの配列または単一の数値です。y1またはy2が単一の数値の場合、それらは長さNで、すべての要素が同じ数値の配列と同等です。fill_between()はY軸でy1y2の間の部分を塗りつぶします。whereパラメータがNoneの場合、配列xのすべての要素を塗りつぶします。whereがブール配列の場合、Trueに対応する部分のみを塗りつぶします。プログラム中の配列xの範囲は(-3, 3)で、条件where = f1 > f2が設定されているため、直線が二次曲線より上にある部分のみを描画します。

❺二つの交点の間のX軸上の矩形領域を描画します。❻text()を使用して図表に説明文を追加します。❼最後にannotate()を使用して、図表に三つの矢印付き注釈を追加します。

プログラムの詳細を本当に理解するためには、まずmatplotlibの座標変換の仕組みを理解する必要があります。

四つの座標系#

matplotlibで描画される一つの図表には、四つの座標系があります:

  • データ座標系:データ空間内の位置を記述する座標系です。例えば、上のグラフの場合、データ座標系の範囲はX軸が(-3, 3)、Y軸が(-2, 5)です。

  • サブプロット座標系:サブプロット内の位置を記述する座標系で、サブプロットの左下隅が(0, 0)、右上隅が(1, 1)です。

  • 図表座標系:一つの図表には複数のサブプロットが含まれ、サブプロットの周りには余白があります。そのため、図表表示領域内の点を記述するために図表座標系が必要です。図表の左下隅が(0, 0)、右上隅が(1, 1)です。

  • ウィンドウ座標系:描画ウィンドウ内のピクセル単位の座標系です。左下隅が(0, 0)、右上隅が(width, height)で、widthheightはそれぞれピクセル単位の描画ウィンドウの内側の幅と高さで、タイトルバー、ツールバー、ステータスバーなどを含みません。

AxesオブジェクトのtransData属性はデータ座標変換オブジェクトで、transAxes属性はサブプロット座標変換オブジェクトです。FigureオブジェクトのtransFigure属性は図表座標変換オブジェクトです。

上記の座標変換オブジェクトのtransform()メソッドを使用して、この座標系の座標をウィンドウ座標系の座標に変換できます。以下のプログラムは、データ座標系の座標点(-3, -2)(3, 5)を描画ウィンドウ内の座標に変換します:

print(type(ax.transData))
ax.transData.transform([(-3, -2), (3, 5)])
<class 'matplotlib.transforms.CompositeGenericTransform'>
array([[ 80. ,  35.2],
       [576. , 281.6]])

以下のプログラムは、サブプロット座標系の座標点(0, 0)(1, 1)を描画ウィンドウ内の位置に変換し、結果は上記と同じです。つまり、サブプロットの左下隅の座標(0, 0)とデータ座標系の座標(-3, -2)は画面上では同じ点です。上のグラフを観察すると、これが正しいことがわかります。

ax.transAxes.transform([(0, 0), (1, 1)])
array([[ 80. ,  35.2],
       [576. , 281.6]])

最後に、図表座標系の座標点(0, 0)(1, 1)を描画ウィンドウ内の位置に変換します。描画領域の幅が640ピクセル、高さが320ピクセルであることがわかります:

fig.transFigure.transform([(0, 0), (1, 1)])
array([[  0.,   0.],
       [640., 320.]])

座標変換オブジェクトのinverted()メソッドを使用して、その逆変換オブジェクトを取得できます。例えば、以下のプログラムは描画ウィンドウ内の座標点(320, 160)をデータ座標系の座標に変換します。

inv = ax.transData.inverted()
print(type(inv))
inv.transform((320, 160))
<class 'matplotlib.transforms.CompositeGenericTransform'>
array([-0.09677419,  1.54545455])

読者はプログラムが出力する図表を注意深く観察してください。サブプロットの上下左右の余白は異なるため、描画領域の中心点(320, 160)はデータ領域の中心点(0, 1.5)ではありません。

set_xlim()を呼び出してサブプロットの表示範囲を変更すると、そのデータ座標変換オブジェクトも同時に変更されます:

print(ax.set_xlim(-3, 2))  # X軸の範囲を-3から2に設定
print(ax.transData.transform((3, 5)))  # データ座標変換オブジェクトが変更されました
(-3.0, 2.0)
[675.2 281.6]

以下に、上のグラフで矩形区間を描画するプログラムを見てみましょう:

from matplotlib import transforms
trans = transforms.blended_transform_factory(ax.transData, ax.transAxes)
ax.fill_between([x1, x2], 0, 1, transform=trans, alpha=0.1)

矩形区間はfill_between()を使用して描画されます。描画される矩形の左右の辺が常に二つの交点を通るため、矩形のX軸座標はデータ座標系の座標x1x2を使用する必要があります。また、矩形の高さが常にサブプロット全体の高さになるため、矩形のY軸座標はサブプロット座標系の座標0と1を使用する必要があります。

Tip

axvspan()axhspan()を使用して、垂直方向と水平方向の区間を簡単に描画できます。

プログラムでは、blended_transform_factory()を使用してこの混合座標系を作成します。その二つのパラメータはどちらも座標変換オブジェクトで、最初のパラメータからX軸の座標変換を取得し、二番目のパラメータからY軸の座標変換を取得します。そのため、返される座標変換オブジェクトtransはX軸にデータ座標系を使用し、Y軸にサブプロット座標系を使用します。プログラムでは、混合座標変換オブジェクトtransfill_between()transformパラメータに渡し、描画される塗りつぶし領域が常に左右の辺が二つの交点を通り、上下の辺がサブプロットの枠上に位置するようにします。

座標変換のパイプライン#

一つの座標系から別の座標系に変換するには、いくつかのステップが必要です。また、データ座標系は必ずしもデカルト座標系ではなく、極座標系や対数座標系である場合もあります。そのため、座標系の変換は単純な二次元アフィン変換(2D Affine Transformation)ではありません。最も単純な図表座標変換オブジェクトtransFigureから始めて、matplotlibの座標変換がどのように行われるかを紹介します。

本書で提供されているGraphvizMPLTransformを使用して、座標変換オブジェクトを関係図として表示できます。次のグラフはfig.transFigureの内部構造を示しています。

from helper.dot import GraphvizMPLTransform
import graphviz

graphviz.Source(GraphvizMPLTransform.graphviz(fig.transFigure))
_images/5c02a0388d7e1ea3e574018a2a6cfabd02f2666ad4d55a516301e48493eb1410.svg

この座標変換オブジェクトの内容は少し複雑で、BboxTransformToオブジェクトであり、その中にTransformedBboxオブジェクトが含まれています。TransformedBboxオブジェクトにはBboxオブジェクトとAffine2Dオブジェクトが含まれています:

  • Bbox:矩形領域を定義します:[[x0, y0], [x1, y1]]。この例では、矩形の二つの頂点座標は(0, 0)(8 ,4)で、ウィンドウのインチサイズであり、figure()figsizeパラメータとして渡されます。

  • Affine2D:二次元アフィン変換オブジェクトで、行列であり、それと同次ベクトルの積によって変換後の座標が得られます。行列の対角線上以外の値はゼロであるため、このアフィン変換はスケーリング変換のみを行います。座標(x, y)(80*x, 80*y)に変換します。

アフィン変換

二次元空間のアフィン変換行列のサイズは\(3 \times 3\)で、アフィン変換を行うには同次座標を使用する必要があります。つまり、二次元平面上の点(x, y)を三次元ベクトル(x, y, 1)で表します。アフィン変換はアフィン行列とベクトルの積です。変換行列の最下行の値は常に(0, 0, 1)であるため、\(2 \times 3\)の行列形式で表されることもあります。

\[\begin{split}\begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{pmatrix} a_{00} & a_{01} & b_0 \\ a_{10} & a_{11} & b_1 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\end{split}\]
  • TransformedBbox:矩形領域をアフィン変換した後に新しい矩形領域を取得します。例では、得られた矩形領域の2つの頂点は(0, 0)(640, 320)です。重複計算を避けるために、その_points属性はこれらの頂点の座標をキャッシュしています。これはちょうどピクセル単位のウィンドウのサイズであるため、アフィン変換行列の数値80は実際にはFigureオブジェクトのdpi属性です。

  • BboxTransformTo:単位矩形領域から指定された矩形領域への変換です。本例では、矩形領域(0, 0) - (1, 1)を矩形領域(0, 0) - (640, 320)に変換する座標変換オブジェクトであるため、座標をチャート座標系からウィンドウ座標系の座標に変換できます。その_mtx属性はこの変換行列をキャッシュしています。

fig.transFigureのアフィン変換オブジェクトはfig.dpi_scale_transで取得できます:

fig.dpi_scale_trans == fig.transFigure._boxout._transform
np.True_

次に、サブプロット座標変換オブジェクトの内容を確認します:

graphviz.Source(GraphvizMPLTransform.graphviz(ax.transAxes))
_images/acb332dee6f538b317788e8f61399cae9a4e8ea56fcc68bad86c8d0adcb8ddce.svg

ax.transAxesBboxTransformToオブジェクトであるため、(0, 0) - (1, 1)の領域を別の領域に変換します。この領域はTransformedBboxオブジェクトであり、矩形領域(0.125, 0.1)-(0.9, 0.9)fig.transFigureで変換した後の領域です。したがって、transAxesオブジェクト内部でtransFigure変換が使用されています:

ax.transAxes._boxout._transform == fig.transFigure
np.True_

この変換中の矩形領域(0.125, 0.11)-(0.9, 0.88)はサブプロットのチャート座標系での位置です:

ax.get_position()
Bbox([[0.125, 0.10999999999999999], [0.9, 0.88]])

サブプロットのウィンドウ座標系での矩形領域は:

ax.transAxes._boxout.bounds
(np.float64(80.0),
 np.float64(35.199999999999996),
 np.float64(496.0),
 np.float64(246.40000000000003))

したがって、ax.transAxesは実際には矩形領域(0, 0)-(1, 1)を矩形領域(80.0, 35.2)-(496.0, 246.4)に変換する座標変換オブジェクトです。

最後に、データ座標系の変換オブジェクトax.transDataを観察します。これはax.transScaleax.transLimitsax.transAxesで構成されているため、まずax.transLimitsax.transScaleの内容を見てみます。transLimitsBboxTransformFromオブジェクトであり、指定された矩形領域を(0,0)-(1,1)の矩形領域に変換する変換オブジェクトです。

graphviz.Source(GraphvizMPLTransform.graphviz(ax.transLimits))
_images/96c21a90564d5f396e09edce48c28fb667d4ca4e16da309a4c3781c070e5a0b6.svg

transLimitsのソース矩形領域はTransformedBboxオブジェクトであり、矩形領域(-3, -2)-(2, 5)を座標変換した後の矩形領域です。ここでの変換はTransformWrapperオブジェクトで定義されており、上のグラフでは恒等変換です。したがって、transLimitsの最終的な効果は矩形領域(-3, 2)-(2, 5)を矩形領域(0, 0)-(1, 1)に変換することです:

print(ax.transLimits.transform((-3, -2)))
print(ax.transLimits.transform((2, 5)))
[0. 0.]
[1. 1.]

矩形領域(-3, -2)-(2, 5)はX軸とY軸の表示範囲によって決定されます:

print(ax.get_xlim())  # X軸の表示範囲を取得
print(ax.get_ylim())  # Y軸の表示範囲を取得
(np.float64(-3.0), np.float64(2.0))
(np.float64(-2.0), np.float64(5.0))

transLimitsはデータ座標系の表示範囲を単位矩形に変換し、transAxesは単位矩形をピクセル単位のウィンドウ矩形範囲に変換するため、これらの変換の総合効果はデータ座標をウィンドウ座標に変換することです。2つの変換を「+」で接続して新しい変換オブジェクトを作成できます。例えば、ax.transLimits + ax.transAxesはまずax.transLimits変換を行い、次にax.transAxes変換を行うことを示します。変換オブジェクトは生産ライン上の製品のように、座標点を段階的に変換します。以下のプログラムはそれとax.transDataの変換結果を比較します:

t = ax.transLimits + ax.transAxes
print(t.transform((0, 0)))
print(ax.transData.transform((0, 0)))
[377.6 105.6]
[377.6 105.6]

異なる比率の座標軸をサポートするために、transDataにはtransScale変換も含まれています。つまり、transData = transScale + transLimits + transAxesです。本例ではtransScaleは恒等変換であるため、ax.transLimits + ax.transAxesax.transDataの変換効果は同じです:

ax.transScale
<matplotlib.transforms.TransformWrapper at 0x17b9bc00d70>

semilogx()semilogy()loglog()などのプロット関数を使用して対数座標軸のチャートを描画する場合、またはAxesset_xscale()set_yscale()メソッドを使用して座標軸を対数座標に設定する場合、transScaleはもはや恒等変換ではなくなり、その内部構造は次のグラフのようになります。

Warning

本例のX軸の範囲は(-3,3)であるため、X軸を対数座標に変更して再描画すると、多くのエラーメッセージが表示されます。

ax.set_xscale("log")  # X軸を対数座標に変更
graphviz.Source(GraphvizMPLTransform.graphviz(ax.transScale))
_images/256a75a706bd16fa9458f522ef0c0922e7ef4ab0a08953779b5251f7f0538f61.svg
ax.set_xscale("linear")  # X軸を線形座標に変更

シャドウ効果の作成#

前節で紹介した座標変換を使用して、シャドウ効果のある曲線を描画します。完全なプログラムは以下の通りで、その効果は次のグラフのようになります。

fig, ax = plt.subplots()
x = np.arange(0.0, 2.0, 0.01)
y = np.sin(2 * np.pi * x)

N = 7  # シャドウの本数
for i in range(N, 0, -1):
    offset = transforms.ScaledTranslation(i, -i, transforms.IdentityTransform())  #❶
    shadow_trans = plt.gca().transData + offset  #❷
    ax.plot(
        x,
        y,
        linewidth=4,
        color="black",
        transform=shadow_trans,  #❸
        alpha=(N - i) / 2.0 / N,
    )

ax.plot(x, y, linewidth=4, color="black")
ax.set_ylim((-1.5, 1.5));

まず、ループを使用して透明度とオフセットが徐々に変化するN本の曲線を描画し、その後実際の曲線を描画してシャドウ効果を実現します。

offsetScaledTranslationオブジェクトであり、最初の2つのパラメータはX軸とY軸のオフセットを決定し、3番目のパラメータは座標変換オブジェクトです。これに変換された後、オフセット変換が行われます。プログラムの3番目のパラメータは恒等変換であるため、offsetは実際には単純なオフセット変換です:X軸座標にiを加え、Y軸座標からiを減算します。

以下に、iが1の場合のoffsetを示します:

offset.transform((0, 0))  # (0,0)を(1,-1)に変換
array([ 1., -1.])

❷シャドウ曲線の座標変換はshadow_transによって行われ、データ座標変換オブジェクトtransDataoffsetで構成されています。

print(ax.transData.transform((0, 0)))  # (0,0)をデータ座標変換
print(shadow_trans.transform((0, 0)))  # (0,0)をデータ座標変換とオフセット変換
[102.54545455 237.6       ]
[103.54545455 236.6       ]

❸最後に、パラメータtransformを使用してshadow_transplot()に渡します。shadow_transはデータ座標からウィンドウ座標への変換が完了した後、オフセット変換を行うため、現在のスケールに関係なく、シャドウ効果は常に一貫しています。

注釈の追加#

pyplotモジュールには、テキストを描画する2つの関数が提供されています:text()figtext()です。これらはそれぞれ現在のAxesオブジェクトと現在のFigureオブジェクトのtext()メソッドを呼び出して描画します。text()はデフォルトでデータ座標系にテキストを追加し、figtext()はデフォルトでチャート座標系にテキストを追加します。transformパラメータを使用してテキストの座標系を変更できます。以下のプログラムは、データ座標系、サブプロット座標系、およびチャート座標系にテキストを追加する方法を示しています。

x = np.linspace(-1, 1, 10)
y = x**2

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y)

for i, (_x, _y) in enumerate(zip(x, y)):
    ax.text(_x, _y, str(i), color="red", fontsize=i + 10)  #❶

ax.text(
    0.5,
    0.8,
    "サブプロット座標系のテキスト",
    color="blue",
    ha="center",
    transform=ax.transAxes,
)  #❷

plt.figtext(0.1, 0.92, "チャート座標系のテキスト", color="green")  #❸;
Text(0.1, 0.92, 'チャート座標系のテキスト')

transformパラメータが設定されていないため、text()はデフォルトでデータ座標系にテキストを作成します。ここではfontsizeパラメータを使用してテキストのサイズを変更しています。❷transformパラメータを使用してテキストの座標変換をax.transAxesに変更しているため、テキストはサブプロット座標系にあります。haパラメータは'center'であり、座標点(0.5, 0.8)が水平方向にテキストの中心であることを示します。hahorizontalalignmentの略で、水平方向の配置を意味します。❸figtext()を呼び出してチャート座標系にテキストを追加します。

プログラムの出力は上のグラフのようになります。サブプロットの表示範囲を変更するためにズームとパン工具を使用すると、データ座標系のテキストが曲線に追従し、他の2つの座標系のテキストの位置は変わらないことがわかります。プロットウィンドウのツールバーの最後から2番目のアイコンボタンをクリックして「Subplot Configuration Tool」ダイアログを開き、toprightbottomleftなどのパラメータを調整すると、サブプロット座標系のテキストも位置を変更し、水平方向ではサブプロットの中心と常に一致します。チャート座標系のテキストの位置は、ウィンドウのサイズを変更した場合にのみ変更されます。

テキストを描画する関数には、テキストや外枠のスタイルを設定するための多くのキーワードパラメータがあります。詳細については、matplotlibのユーザーマニュアルを参照してください。ここでは詳細には触れません。

pyplotモジュールのannotate()を使用して、矢印付きの注釈テキストを描画します。その呼び出しパラメータは以下の通りです:

annotate(s, xy, xytext=None, xycoords='data', textcoords='data', arrowprops=None, ...)

ここで、sパラメータは注釈テキスト、xyは矢印が指す座標、xytextは注釈テキストの座標です。xycoordstextcoordsはそれぞれ矢印座標と注釈テキスト座標の座標変換方法を指定します。

矢印付きの注釈には、矢印が指す座標と注釈テキストの座標の2つの座標が必要です。これらの座標は異なる座標変換を使用できます。パラメータxycoordstextcoordsは文字列であり、以下のオプションがあります:

属性値

座標変換方法

figure points

ポイント単位で、チャートの左下隅からの座標

figure pixels

ピクセル単位で、チャートの左下隅からの座標

figure fraction

チャート座標系の座標

axes points

ポイント単位で、サブプロットの左下隅からの座標

axes pixels

ピクセル単位で、サブプロットの左下隅からの座標

axes fraction

サブプロット座標系の座標

data

データ座標系の座標

offset points

ポイント単位で、点xyからの座標

polar

データ座標系の極座標

ここで、'figure fraction''axes fraction''data'はそれぞれチャート座標系、サブプロット座標系、データ座標系の座標変換オブジェクトを使用することを示します。チャートとサブプロット座標系は正規化された座標であるため、使用するのが不便です。そのため、チャートとサブプロットにはそれぞれポイント単位とピクセル単位の座標変換方法が提供されています。ポイントとピクセルの単位は似ていますが、チャートのdpi属性値に依存せず、常に1インチあたり72ポイントで計算されます。

上記の座標変換はすべて固定点を原点として変換を行いますが、時には矢印からのオフセットでテキストの座標を指定したい場合があります。その場合は、'offset points'オプションを使用できます。

この章先頭のグラフでは、すべての注釈の矢印座標は'data'を使用しているため、プロット領域を拡大または移動しても、矢印は常にデータ座標系の固定点を指します。注釈テキスト「交点」の座標変換方法は'axes fraction'を使用しているため、「交点」は常にサブプロットの固定位置にあります。「直線が曲線領域より大きい」注釈テキストの座標は'offset points'変換を使用しているため、テキストと矢印の相対位置は常に変わりません。

最後に、arrowpropsパラメータは矢印のスタイルを記述する辞書です。注釈スタイルの詳細な設定については、matplotlibの関連ドキュメントを参照してください。

テクニック集#

最後の節として、いくつかの特別な使い方を紹介します。

aggバックエンドて描画する#

matplotlibが描画するグラフは非常に細やかです。これは、その描画バックエンドライブラリがC++で開発された高品質なアンチエイリアス2D描画ライブラリであるAnti-Grain Geometry (AGG)を使用しているためです。もし2Dグラフィックを描画したいが、matplotlibのグラフ機能が必要ない場合、メモリ内で直接画像を描画し、それをNumPy配列に変換することができます。

以下のコードはRendererAgg(キャンバス)をロードし、幅と高さがそれぞれ250ピクセルのRendererAggオブジェクトを作成します。3番目のパラメータはDPIで、このパラメータはキャンバスのサイズに影響しません。buffer_rgba()メソッドはキャンバスに保存された描画結果のバッファを取得し、frombuffer()を使用してこのバッファをNumPy配列に変換し、キャンバスのサイズに合わせてreshape()を呼び出します。最終的に得られる配列arrの形状は(250, 250, 4)で、2番目の軸の4はキャンバスが4つのチャンネル(赤、緑、青、透明)を持っていることを示します。

import numpy as np
from matplotlib.backends.backend_agg import RendererAgg

w, h = 250, 250
renderer = RendererAgg(w, h, 90)
buf = renderer.buffer_rgba()
arr = np.frombuffer(buf, np.uint8).reshape(h, w, -1)
print(arr.shape)
(250, 250, 4)

RendererAggオブジェクトは、キャンバス上に描画するためのいくつかのdraw_*()メソッドを提供します。例えば、以下のコードはまずPathオブジェクトを作成し、renderer.draw_path()を呼び出してキャンバス上にこのPathオブジェクトを描画します。最初のパラメータはGraphicsContextBaseオブジェクトで、線幅や線の色などの描画時の属性を設定するために使用されます。3番目のパラメータは座標変換オブジェクトで、この例では恒等変換を使用します。4番目のパラメータはパスの塗りつぶし色です。

from matplotlib.path import Path
from matplotlib import transforms

path_data = [
    (Path.MOVETO, (179, 1)),
    (Path.CURVE4, (117, 75)),
    (Path.CURVE4, (12, 230)),
    (Path.CURVE4, (118, 230)),
    (Path.LINETO, (142, 187)),
    (Path.CURVE4, (210, 290)),
    (Path.CURVE4, (250, 132)),
    (Path.CURVE4, (200, 105)),
    (Path.CLOSEPOLY, (179, 1)),
]

code, points = zip(*path_data)
path = Path(points, code)

gc = renderer.new_gc()
gc.set_linewidth(2)
gc.set_foreground((1, 0, 0))
gc.set_antialiased(True)
renderer.draw_path(gc, path, transforms.IdentityTransform(), (0, 1, 0))

matplotlibが提供するArtistオブジェクトを使用して描画することもできます。以下では、まずCircleオブジェクトとTextオブジェクトを作成し、それらのdraw()メソッドを呼び出してキャンバス上に描画します。Textオブジェクトは描画時にキャンバスのdpi属性を取得する必要があるため、draw()を呼び出す前にそのfigure属性をrendererに設定します。

from matplotlib.patches import Circle
from matplotlib.text import Text

c = Circle(
    (w / 2, h / 2), 50, edgecolor="blue", facecolor="yellow", linewidth=2, alpha=0.5
)
c.draw(renderer)

renderer.get_figure = lambda root=True:renderer
text = Text(w / 2, h / 2, "Circle", va="center", ha="center")
text.figure = renderer
text.draw(renderer)

Notebookでarrが表す画像を表示するには、plt.imsave()を呼び出すことができます。最初のパラメータはファイル名またはファイルインターフェースを持つオブジェクトで、ここではBytesIOオブジェクトを使用してPNG画像の内容をpng_bufに保存します。次に、IPythondisplay_png()を使用してメモリ内のPNG画像を表示します。

from io import BytesIO
from IPython.display import display_png

png_buf = BytesIO()
plt.imsave(png_buf, arr, format="png")
display_png(png_buf.getvalue(), raw=True)
_images/33a733b1d9716d95b450af6d8e2ddb37b9dbe3ec18dc63ed7627a7e6028352c8.png

本書は、画像上に描画するためのImageDrawerクラスを提供しています。以下では、ImageDrawerを使用して画像上にマーカー、テキスト、直線、円、矩形、および楕円を描画し、本書が提供する%array_imageマジックコマンドを使用して結果の画像をNotebookに表示します。その結果は次のグラフに示されています。

ImageDrawerreverseパラメータはY軸の方向を決定します。デフォルト値はTrueで、Y軸が下向きであり、画像のピクセル座標系の方向と同じです。Falseの場合、Y軸は上向きであり、数学的なデカルト座標系の定義と同じです。

from helper.matplotlib import ImageDrawer
from helper.numpy import array_image

img = plt.imread("data/vinci_target.png")
drawer = ImageDrawer(img)
drawer.set_parameters(lw=2, color="white", alpha=0.5)
drawer.line(8, 60, 280, 60)
drawer.circle(123, 130, 50, facecolor="yellow", lw=4)
drawer.markers("x", [82, 182], [218, 218], [50, 100])
drawer.rectangle(81, 330, 100, 30, facecolor="blue")
drawer.text(10, 50, "Mona Lisa", fontsize=40)
drawer.ellipse(119, 255, 200, 100, 100, facecolor="red")
array_image(drawer.to_array())
_images/a765c4f152dcadd13e1f5406fb2d1c67eb6f667c7638fb44d7d32df045a9fac3.png

アニメーション#

図形要素のさまざまな属性を変更し、チャートを再描画することで、簡単なアニメーション効果を実現できます。以下の例では: ❶まず、50ミリ秒のタイマーtimerを作成し、add_callback()を呼び出してタイマーイベントを追加します。最初のパラメータはタイマーイベントが発生したときに呼び出される関数で、2番目のパラメータはこの関数に渡されるオブジェクトです。チャート内の曲線のデータを変更する必要があるため、Line2Dオブジェクトlineupdate_data()に渡します。❷Line2D.set_ydata()を呼び出して曲線のYデータを設定します。❸Figure.canvas.draw()を呼び出してチャート全体を再描画します。

Tip

JupyterLab で matplotlib のインタラクティブなグラフを表示するには、以下の手順を実行します。

まず、ipympl をインストールします。

pip install ipympl

Notebook のセル内で以下のマジックコマンドを実行します。

%matplotlib ipympl
%matplotlib ipympl
from matplotlib import pyplot as plt
import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0, 10, 1000)
(line,) = ax.plot(x, np.sin(x), lw=2)


def update_data(line):
    x[:] += 0.1
    line.set_ydata(np.sin(x))  #❷
    fig.canvas.draw()  #❸


timer = fig.canvas.new_timer(interval=50)  #❶
timer.add_callback(update_data, line)
timer.start()

次のコードでタイマーを停止させます。

timer.stop()

キャッシュで高速に再描画#

しかし、Figure.canvas.draw()の描画速度は遅いため、再描画速度を向上させるために、チャート内の静的な要素を高速に再描画し、動的な効果を持つ要素のみを更新することができます。以下の例では:

❶図形要素を作成する際に、animated属性をTrueに設定します。❷Figure.canva.draw()を呼び出してチャート全体を再描画する際に、animatedTrueのオブジェクトは無視されます。❸この時点で、すべての静的な要素が描画され、Figure.canvas.copy_from_bbox()を呼び出して、サブプロットオブジェクトに対応する領域の画像情報をbackgroundに保存します。サブプロットオブジェクトの位置とサイズは、そのbbox属性から取得できます。

タイマーイベント処理関数では、❹まずFigure.canvas.restore_region()を呼び出して保存された画像情報を復元します。これは、すべての動的な要素を消去し、すべての静的な要素を再描画することに相当します。❺曲線のY軸データを更新した後、サブプロットオブジェクトのdraw_artist()を呼び出して、Canvasオブジェクト内に曲線を描画します。この時点で、Canvasオブジェクト内には完全なチャートの画像が含まれています。❻Figure.canvas.blit()を呼び出して、Canvas内の指定された領域の内容を画面に描画します。

fig, ax = plt.subplots()
x = np.linspace(0, 10, 1000)
(line,) = ax.plot(x, np.sin(x), lw=2, animated=True)  #❶

fig.canvas.draw()  #❷
background = fig.canvas.copy_from_bbox(ax.bbox)  #❸


def update_data(line):
    x[:] += 0.1
    line.set_ydata(np.sin(x))
    fig.canvas.restore_region(background)  #❹
    ax.draw_artist(line)  #❺
    fig.canvas.blit(ax.bbox)  #❻


timer = fig.canvas.new_timer(interval=50)
timer.add_callback(update_data, line)
timer.start()
timer.stop()

animationモジュール#

前述の2つの簡単な例を通じて、matplotlibでアニメーションを作成する原理を理解しましたが、実際の使用では、animationモジュールを使用してアニメーション効果を作成することが一般的です。例えば、FuncAnimationオブジェクトは、定期的にユーザー定義の関数を呼び出してチャート内の要素を更新します。

❶曲線オブジェクトを作成する際に、animatedパラメータをTrueに設定します。❷アニメーションコールバック関数update_line()内で、すべてのアニメーション要素のデータを設定します。この関数には現在の表示フレーム数がパラメータとして渡され、ここではフレーム数を使用して波形の位相を変更し、すべてのアニメーション要素を含むシーケンスを返します。❸FuncAnimationオブジェクトを作成し、update_line()を定期的に呼び出します。intervalパラメータは1秒あたりのフレーム数で、blitTrueの場合、キャッシュを使用して各フレームの描画を高速化します。framesパラメータは最大フレーム数を設定し、update_line()のフレーム数パラメータは0から99の間で循環します。

%matplotlib inline
from matplotlib.animation import FuncAnimation

plt.ioff()
fig, ax = plt.subplots()

x = np.linspace(0, 4 * np.pi, 200)
y = np.sin(x)
(line,) = ax.plot(x, y, lw=2, animated=True)  #❶


def update_line(i):
    y = np.sin(x + i * 2 * np.pi / 100)
    line.set_ydata(y)
    return [line]  #❷


ani = FuncAnimation(fig, update_line, blit=True, interval=25, frames=100)  #❸

アニメーションを動画ファイルとして保存するには、以下の方法を呼び出します:

Warning

matplotlibは、システムにインストールされているビデオ圧縮ソフトウェア(例:ffmpeg.exe)を使用してビデオファイルを生成します。ビデオ圧縮ソフトウェアの実行ファイルのパスがPATH環境変数に含まれていることを確認してください。

ani.save("sin_wave.mp4", fps=25)

次は作成したアニメーションです。

from IPython.display import Video

Video("sin_wave.mp4", embed=True)