【Matplotlib】ArtistAnimationを使って棒グラフや円グラフをアニメーション描画する


投稿日 2023年2月1日 >> 更新日 2023年2月28日

概要

棒グラフや円グラフをアニメーション描画していきます。

「gif」や「mp4」としてファイルに保存するにはArtistAnimationクラスやFuncAnimationクラスを使用する必要があります。各拡張子のファイルとして保存するにはmatplotlibの他に別途インストールしておくツールが必要なのでよろしければこちら「gifファイルとmp4ファイルの保存」をご参照ください。

この記事ではArtistAnimationクラスを使ってアニメーション描画していきます。

この記事を見ることによって、グラフの種類に問わずアニメーション描画を行うことができます。

開発環境&使用ライブラリ

開発環境
Windows Subsystem for Linux 1
Python 3.6.9
pip 21.3.1
ffmpeg
使用ライブラリ ライセンス
matplotlib==3.1.1 PSF
Pillow==6.1.0 HPND

棒グラフのアニメーション

以下の記事で実装している棒グラフを元に、ArtistAnimationクラスでアニメーション描画してからファイルに保存します。

ArtistAnimationクラスを使用しない単純な棒グラフのリアルタイム描画のコードは以下です。

import matplotlib.pyplot as plt

%matplotlib


country = ['JPN', 'USA']
population = [131, 301]

# 各値を10飛ばして取得(アニメーションする際の時間短縮として)
jpn_10 = list(range(0, population[0], 10))
usa_10 = list(range(0, population[1], 10))
# 各変数の長さの誤差×[jpn_10の最高値] = 足りない分の値の穴埋め
jpn_10 = jpn_10 + [130] * abs(len(jpn_10) - len(usa_10))

fontsize = 15

plt.figure(figsize=(10, 5))

for j, u in zip(jpn_10, usa_10):
    plt.cla() # 描画されたグラフをクリアにする
    plt.bar(country, [j, u], width=0.3, color=['b', 'r'])
    plt.title("Population comparison between Japan and the United States", fontsize=fontsize)
    plt.text(country[0], j+5, "{} Million".format(j), fontsize=fontsize)
    plt.text(country[1], u+5, "{} Million".format(u), fontsize=fontsize)
    plt.xlabel("Country", fontsize=fontsize)
    plt.ylabel("Population", fontsize=fontsize)
    plt.xlim(-1, 2)
    plt.ylim(0, 330)
    plt.grid()
    plt.pause(0.01) # 一時グラフを停止する(引数には間隔を指定)

# plt.close()

以下はArtistAnimationクラスを使用した棒グラフのアニメーション描画のコードです。

import matplotlib.pyplot as plt

from matplotlib.animation import ArtistAnimation
from matplotlib.animation import PillowWriter, FFMpegWriter

# jupyter notebook環境の場合
%matplotlib


country = ['JPN', 'USA']
population = [131, 301]

# 各値を10飛ばして取得(アニメーションする際の時間短縮として)
jpn_10 = list(range(0, population[0], 10))
usa_10 = list(range(0, population[1], 10))
# 各変数の長さの誤差×[jpn_10の最高値] = 足りない分の値の穴埋め
jpn_10 = jpn_10 + [130] * abs(len(jpn_10) - len(usa_10))

fig = plt.figure(figsize=(10, 5))
fontsize = 15

# 以下は変化しないのでグローバルで定義
plt.title("Population comparison between Japan and the United States", fontsize=fontsize)
plt.xlabel("Country", fontsize=fontsize)
plt.ylabel("Population", fontsize=fontsize)
plt.xlim(-1, 2)
plt.ylim(0, 330)
plt.grid()

# 各オブジェクトが格納されたリストを追加するための空のリストを用意
artist_list = []

for j, u in zip(jpn_10, usa_10):

    # jpバー、usバーインスタンス変数を取得
    bar1, bar2 = plt.bar(country, [j, u], width=0.3, color=['b', 'r'])
    # 他の方法でobjを取得するには以下
    # bars_ojb = plt.bar(country, [j, u], width=0.3, color=['b', 'r'])
    # bars_list = list(bars_obj)

    # デフォルト値として設定
    jp_text = plt.text(0, 0, None, fontsize=fontsize)
    us_text = plt.text(0, 0, None, fontsize=fontsize)

    # mplのテキストオブジェクトを更新
    jp_text.set_position((country[0], j+5))
    jp_text.set_text("{} Millon".format(j))
    us_text.set_position((country[1], u+5))
    us_text.set_text("{} Millon".format(u))

    # 各値のオブジェクトをリストに格納
    # 格納する並びは適当
    objects_list = [bar1, jp_text, bar2, us_text]
    # bars_listを格納する場合
    # objects_list = bars_list + [jp_text, us_text]

    # リストをリストに追加していく
    artist_list.append(objects_list)


ani = ArtistAnimation(
    fig=fig, # 図
    artists=artist_list, # 出来上がったグラフのリストを渡す
    interval=100, # 速さを設定(ミリ秒)
)

# gifファイルとして保存する場合
ani.save("animation_bar.gif", writer=PillowWriter())
# mp4ファイルとして保存する場合
#ani.save("animation_bar.mp4", writer=FFMpegWriter())

「plt.bar(country, [j, u], width=0.3, color=['b', 'r'])」のタイプは「BarContainer object of 2 artists」となっていて、2つの作品(値)が含まれているオブジェクトとなっています。

公式ドキュメント(BarContainer)での解説では、タプルとして扱う事ができるとあります。

よって「plt.bar()」に格納されている値は「(ja_obj, us_obj)」という事になるので、上記コードのように変数を2つ用意するか、1つの変数に代入し後の処理をスムーズに行う為にタプルからリストに変換しています。

「plt.text(0, 0, None, fontsize=fontsize)」ではテキストに印字された値を初期化しています。

「plt.text(country[0], j+5, "{} Million".format(j), fontsize=fontsize)」のように繰り返し値を更新してしまうとグラフにテキストが印字され続けてしまうので、一旦クリアにしています。

for j, u in zip(jpn_10, usa_10):
    ...
    # デフォルト値として設定
    jp_text = plt.text(0, 0, None, fontsize=fontsize)

    # mplのテキストオブジェクトを更新
    jp_text.set_position((country[0], j+5))
    jp_text.set_text("{} Millon".format(j))
    ...

初期化されたオブジェクトから「Text.set_position()」や「Text.set_text()」メソッドを呼び出して値を更新しています。

「 objects_list」変数には「 [bar1, jp_text, bar2, us_text]」のようなシーケンスで各matplotlibオブジェクトを格納していますが、並び順に関しては特に決まっていません。

「ArtistAnimation(fig=fig, artists=artist_list, interval=100)」のパラメータ「artists」にはリストのリストを与える必要があるので、「[[obj1, obj2, obj3]]」となっていればグラフがプロットされます。

「artists」に渡されたリストのリストはこちらのソースコードを見ることによってどのように処理が行われているか理解できるかと思います。

リストのリストが処理されているコードを一部抜粋しているのが以下です。

class ArtistAnimation(TimedAnimation):
    ...    
    ...
    def _init_draw(self):
        # Make all the artists involved in *any* frame invisible
        figs = set()
        for f in self.new_frame_seq():
            for artist in f:
                artist.set_visible(False)
                artist.set_animated(self._blit)
                # Assemble a list of unique figures that need flushing
                if artist.get_figure() not in figs:
                    figs.add(artist.get_figure())
...

円グラフのアニメーション

円グラフのシンプルなリアルタイム描画は以下となります。

import matplotlib.pyplot as plt

# jupyter notebook環境の場合
%matplotlib

country = ['JPN', 'USA']
population = [131, 301]

# 各値を10飛ばして取得(アニメーションする際の時間短縮として)
jpn_10 = list(range(0, population[0], 10))
usa_10 = list(range(0, population[1], 10))
# 各変数の長さの誤差×[jpn_10の最高値] = 足りない分の値の穴埋め
jpn_10 = jpn_10 + [130] * abs(len(jpn_10) - len(usa_10))

fig = plt.figure(figsize=(10, 5))

fontsize = 15

for j, u in zip(jpn_10, usa_10):
    plt.cla() # 描画されたグラフをクリアにする
    plt.pie([j, u], labels=country, autopct='%1.1f%%', colors=['dodgerblue', 'r'], wedgeprops={'width': 0.6, 'linewidth': 4, 'edgecolor': 'w'})
    plt.title("graph circle")
    plt.suptitle("Population comparison between Japan and the United States", fontsize=fontsize)
    plt.pause(0.1) # 一時グラフを停止する(引数には間隔を指定)

# plt.close()

ArtistAnimationクラスを使用した円グラフのアニメーション描画のコードです。

country = ['JPN', 'USA']
population = [131, 301]

# 各値を10飛ばして取得(アニメーションする際の時間短縮として)
jpn_10 = list(range(0, population[0], 10))
usa_10 = list(range(0, population[1], 10))
# 各変数の長さの誤差×[jpn_10の最高値] = 足りない分の値の穴埋め
jpn_10 = jpn_10 + [130] * abs(len(jpn_10) - len(usa_10))

# 図の中に2つのサブプロット作る
fig = plt.figure(figsize=(10, 5))

fontsize = 15

# 円グラフ用の定義
plt.title("graph circle")

plt.suptitle("Population comparison between Japan and the United States", fontsize=fontsize)

# 各オブジェクトが格納されたリストを追加するための空のリストを用意
artist_list = []

for j, u in zip(jpn_10, usa_10):

    # 戻り値[jpnの半径, usaの半径], [jpnのテキストobj, usaのテキストobj], [jpnのテキストobj, usaのテキストobj]
    pie_obj, pie_text_num, pie_text_per = plt.pie(
        [j, u],
        labels=country,
        autopct='%1.1f%%',
        colors=['dodgerblue', 'r'],
        wedgeprops={'width': 0.6, 'linewidth': 4, 'edgecolor': 'w'}
    )
    # 戻り値[[jpnの半径, usaの半径], [jpnのテキストobj, usaのテキストobj], [jpnのテキストobj, usaのテキストobj]]
    # pies = plt.pie([j, u], labels=country, autopct='%1.1f%%', colors=['dodgerblue', 'r'], wedgeprops={'width': 0.6, 'linewidth': 4, 'edgecolor': 'w'})

    # 各オブジェクトが格納されたリストを1つのリストにまとめる
    objects_list = pie_obj + pie_text_num + pie_text_per
    # objects_list = pies[0] + pies[1] + pies[2]

    # リストをリストに追加していく
    artist_list.append(objects_list)

ani_2 = ArtistAnimation(
    fig=fig, # 図
    artists=artist_list, # 出来上がったグラフのリストを渡す
    interval=100, # 速さを設定(ミリ秒)
)

# gifファイルとして保存する場合
ani_2.save("ani_pie.gif", writer=PillowWriter())
# mp4ファイルとして保存する場合
#ani_2.save("ani_pie.mp4", writer=FFMpegWriter())

「plt.pie()」のタイプはタプル型となっており、複数のリストが格納されたオブジェクトです。上記では戻り値として3つの変数に渡しています(「pie_obj, pie_text_num, pie_text_per」)。

各変数の「pie_obj, pie_text_num, pie_text_per」はリストとなっているので、「objects_list」に1つのリストとしてまとめいます。

ArtistAnimationクラスの構造上、リストのリストを渡す必要があるので1サイクルで抽出されたオブジェクトを1つのリストにまとめて、そのリストを「artist_list」に追加しています。

グラフの種類によって戻り値が異なるのでリストに追記していく際は注意が必要です。

複数グラフ(棒・円)のアニメーション

ArtistAnimationクラスを使用した棒グラフと円グラフのアニメーション描画の実装は以下となります。

country = ['JPN', 'USA']
population = [131, 301]

# 各値を10飛ばして取得(アニメーションする際の時間短縮として)
jpn_10 = list(range(0, population[0], 10))
usa_10 = list(range(0, population[1], 10))
# 各変数の長さの誤差×[jpn_10の最高値] = 足りない分の値の穴埋め
jpn_10 = jpn_10 + [130] * abs(len(jpn_10) - len(usa_10))

# 図の中に2つのサブプロット作る
fig, axs = plt.subplots(1, 2, figsize=(10, 5))

fontsize = 15

# 棒フラフ用の定義
axs[0].set_xlabel("Country", fontsize=fontsize)
axs[0].set_ylabel("Population", fontsize=fontsize)
axs[0].set_xlim(-1, 2)
axs[0].set_ylim(0, 330)
axs[0].grid()
axs[0].set_title("graph bar")

# 円グラフ用の定義
axs[1].set_title("graph circle")

# plt.suptitle("Population comparison between Japan and the United States", fontsize=fontsize)

# 各オブジェクトが格納されたリストを追加するための空のリストを用意
artist_list = []

for j, u in zip(jpn_10, usa_10):
    """
    棒グラフのサブプロット
    """
    # jpバー、usバーインスタンス変数を取得
    bar1, bar2 = axs[0].bar(country, [j, u], width=0.3, color=['dodgerblue', 'r'])

    # デフォルト値として設定
    jp_text = axs[0].text(0, 0, None, fontsize=fontsize)
    us_text = axs[0].text(0, 0, None, fontsize=fontsize)

    # mplのテキストオブジェクトを更新
    jp_text.set_position((country[0], j+5))
    jp_text.set_text("{} Millon".format(j))
    us_text.set_position((country[1], u+5))
    us_text.set_text("{} Millon".format(u))

    """
    円グラフ用のサブプロット
    """
    # [jpnの半径, usaの半径], [jpnのテキストobj, usaのテキストobj], [jpnのテキストobj, usaのテキストobj]
    pie_obj, pie_text_num, pie_text_per = axs[1].pie(
        [j, u], labels=country,
        autopct='%1.1f%%',
        colors=['dodgerblue', 'r'],
        wedgeprops={'width': 0.6, 'linewidth': 4, 'edgecolor': 'w'}
    )

    # 各値のオブジェクトをリストに格納
    # 格納する並びは適当
    objects_list = [bar1, jp_text, bar2, us_text] + pie_obj + pie_text_num + pie_text_per

    # リストをリストに追加していく
    artist_list.append(objects_list)

ani_3 = ArtistAnimation(
    fig=fig, # 図
    artists=artist_list, # 出来上がったグラフのリストを渡す
    interval=100, # 速さを設定(ミリ秒)
)

# gifファイルとして保存する場合
ani_3.save("ani_bar_pie.gif", writer=PillowWriter())
# mp4ファイルとして保存する場合
#ani_3.save("ani_bar_pie.mp4", writer=FFMpegWriter())

「objects_list」には各変数リストを1つにまとめています。

print(objects_list)
print(len(objects_list))
# [<matplotlib.patches.Rectangle object at 0x7f6f8b1dcc50>, Text(JPN, 135, '130 Millon'), <matplotlib.patches.Rectangle object at 0x7f6f8b1dcef0>, Text(USA, 305, '300 Millon'), <matplotlib.patches.Wedge object at 0x7f6f8b168550>, <matplotlib.patches.Wedge object at 0x7f6f8b168c18>, Text(0.6400448207491376, 0.8946187050538372, 'JPN'), Text(-0.6400449045093831, -0.8946186451285122, 'USA'), Text(0.3491153567722568, 0.4879738391202748, '30.2%'), Text(-0.3491154024596635, -0.4879738064337339, '69.8%')]
# 10

ArtistAnimationクラスにはリストのリストを与えることにより、二重構造のイテレーションによってプロットされます。

# class ArtistAnimation(TimedAnimation):クラスで行われている処理の疑似コード
for index, f in enumerate(artist_list, 1):
    print(index, f)
    for index, artist in enumerate(f, 1):
        print(index, 'type', type(artist))
        print(index, artist)
    print()
# 1 [<matplotlib.patches.Rectangle object at 0x7f6f8b408d68>, Text(JPN, 5, '0 Millon'), <matplotlib.patches.Rectangle object at 0x7f6f8b408fd0>, Text(USA, 5, '0 Millon'), <matplotlib.patches.Wedge object at 0x7f6f8b415668>, <matplotlib.patches.Wedge object at 0x7f6f8b415d68>, Text(1.1, 0.0, 'JPN'), Text(1.1, 0.0, 'USA'), Text(0.6, 0.0, '0.0%'), Text(0.6, 0.0, '0.0%')]
# 1 type <class 'matplotlib.patches.Rectangle'>
# 1 Rectangle(xy=(-0.15, 0), width=0.3, height=0, angle=0)
# 2 type <class 'matplotlib.text.Text'>
# 2 Text(JPN, 5, '0 Millon')
# 3 type <class 'matplotlib.patches.Rectangle'>
# 3 Rectangle(xy=(0.85, 0), width=0.3, height=0, angle=0)
# 4 type <class 'matplotlib.text.Text'>
# 4 Text(USA, 5, '0 Millon')
# 5 type <class 'matplotlib.patches.Wedge'>
# 5 Wedge(center=(0, 0), r=1, theta1=0, theta2=0, width=0.6)
# 6 type <class 'matplotlib.patches.Wedge'>
# 6 Wedge(center=(0, 0), r=1, theta1=0, theta2=0, width=0.6)
# 7 type <class 'matplotlib.text.Text'>
# 7 Text(1.1, 0.0, 'JPN')
# 8 type <class 'matplotlib.text.Text'>
# 8 Text(1.1, 0.0, 'USA')
# 9 type <class 'matplotlib.text.Text'>
# 9 Text(0.6, 0.0, '0.0%')
# 10 type <class 'matplotlib.text.Text'>
# 10 Text(0.6, 0.0, '0.0%')
# ...

上記のようにリスト内のリストからイテレーションしてプロットしてるので、グラフオブジェクト内の値がどのようになっているのか確認する必要があります。

それでは以上となります。

最後までご覧いただきありがとうございました。