カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

go

【Plotly&animation】Plotlyのgoでアニメーションを作成

2021年10月16日

こんな人にオススメ

plotlyでアニメーションができるらしいけどどうやるの?

Plotly公式の「Intro to Animations in Python」を見てもよくわからん。

ということで、今回はplotly.graph_objects、通称goを使用してアニメーション機能を持つグラフを作成する。アニメーションってのはグラフ上のボタンを押すと自動でプロット内容が変わるグラフのこと。

goじゃなくてplotly.express、通称pxを使用する方法もあるけど、正直こっちの方が作成は簡単。その代わりにグラフのカスタム性は低い。

pxで描いたアニメーショングラフについては以下参照。

【px&animation】plotly.expressでアニメーションのグラフを作成

続きを見る

アニメーション機能付きのグラフは作成が面倒なことが多いけど、本記事では各部分を分解して解説するので、少しでも理解していただけると幸いだ。

なお、アニメーション機能を使うにはPlotlyのボタンを機能を使う。ボタン機能については以下の記事参照。

【plotly&ボタン】plotlyのupdatemenusにbuttonsを追加

続きを見る

Python環境は以下。

  • Python 3.10.1
  • pandas 1.3.5
  • plotly 5.4.0
  • plotly-orca 3.4.2

運営者のメガネです。YouTubeTwitterInstagram、自己紹介はこちら、お問い合わせはこちらから。

運営者メガネ

作成したコード全文

下準備

import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio
import plotly.express as px

まずは下準備のimport関連。pandasはデータを読み込む際に使用。pandasじゃなくてもいいけど、pandasが一番データの処理が楽だと思う。

今回はgoを使用するけど、pxでの作成方法も軽く解説するのでpximport。pioはグラフ保存用に使用する。詳しくは以下の記事参照。

plotlyでグラフを静止画として保存する際に出るポップアップ
【plotly&orca】plotlyで静止画保存(orca)

続きを見る

再生ボタンだけのアニメーショングラフ


まずはPlotly公式の「Intro to Animations in Python」で作成されている「Simple Play Button」のグラフから。このグラフには再生ボタンの「Play」が設置されている。

公式のコードにコメントを追記したのが以下だが、ちょっとわかりにくい。ということで、次の項目でこのコードを分解して解説する。

なお、公式のグラフのままだとフォントサイズが小さいので、fig作成後にfig.update_layoutでフォントサイズを変更している。fig.updateシリーズについては以下の記事参照。

【plotly&fig作成と更新】add_traceやupdate_layoutの使い方

続きを見る

# 再生ボタンだけのアニメーション

fig = go.Figure(
    # データの作成
    data=[go.Scatter(x=[0, 1], y=[0, 1])],
    # レイアウトの設定
    layout=go.Layout(
        # 横軸・縦軸の範囲設定
        xaxis=dict(range=[0, 5], autorange=False),
        yaxis=dict(range=[0, 5], autorange=False),
        # グラフタイトル
        title="Start Title",
        # 再生用のボタン作成
        updatemenus=[dict(
            type="buttons",  # ボタンの形式
            buttons=[dict(label="Play",  # ボタンのラベル
                          method="animate",  # ボタンの役割はアニメーション
                          # ボタンを押して変更する内容(今回はなし)
                          args=[None])])]
    ),
    # アニメーションの各フレームの設定
    frames=[go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
            go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
            # 最後のフレームが終わればグラフタイトルを変更
            go.Frame(data=[go.Scatter(x=[3, 4], y=[3, 4])],
                     layout=go.Layout(title_text="End Title"))]
)
# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
# グラフの表示
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_play"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

Simple Play Buttonのコードを分解


先程のコードを書く部分に分解してグラフを作成。コードを分解しただけだから同じグラフができる。

グラフの作成手順は以下の10ステップ。

  1. データの作成(data
  2. プロットに変換(plot
  3. ボタンの挙動変更(buttons
  4. buttonsをレイアウトに反映させる設定(updatemenus
  5. レイアウトの設定(layout
  6. アニメーションフレーム作成(frames
  7. グラフ作成(fig
  8. フォントサイズ変更(追記分、fig.update_layout
  9. グラフ表示(fig.show()
  10. グラフ保存(prefix以下)

複雑に見えるけど、分解してみるとシンプル。データ作成はgo.Scatterで散布図を設定、ボタン作成では単にボタンをレイアウトに設定するだけ。

レイアウトではグラフの表示範囲をrangeで指定、autorangeで自動調節を無効にしている。 autorange=Trueにするとアニメーションの各フレームごとにグラフ範囲が変更される可能性がある。

【plotly&go.Scatter】plotlyの散布図グラフの描き方

続きを見る

# さっきのグラフを分解する

data = go.Scatter(x=[0, 1], y=[0, 1])  # 初期表示データ
plot = [data]  # listにする

# ボタンの挙動
buttons = dict(
    label='Play',  # ボタンのラベル
    method='animate',  # ボタンの形式
    # ボタンを押した時のプロット・レイアウトの変更点(今回はなし)
    args=[None]
)

# レイアウトにボタンを配置
updatemenus = dict(
    type='buttons',  # ボタンの形式はボタン式に
    buttons=[buttons]  # ボタンの設定はここに
)

# レイアウトの設定
layout = go.Layout(
    # 横軸・縦軸の表示範囲
    # autorange=Trueにすると各フレームで自動で表示範囲が調整されてしまう
    xaxis=dict(range=[0, 5], autorange=False),
    yaxis=dict(range=[0, 5], autorange=False),
    title='Start Title',  # 初期からフレーム終了までのグラフタイトル
    updatemenus=[updatemenus],  # ボタンのレイアウト設定はここに
)

# アニメーションの各フレームの設定
frames = [
    # (x1, y1)=(1, 1), (x2, y2)=(2, 2)の直線
    go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
    # (x1, y1)=(1, 1), (x2, y2)=(4, 4)の直線
    go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
    # (x1, y1)=(3, 3), (x2, y2)=(4, 4)の直線
    go.Frame(
        data=[go.Scatter(x=[3, 4], y=[3, 4])],
        # アニメーション終了でグラフタイトルを変更
        layout=go.Layout(title_text='End Title'),
    )
]

fig = go.Figure(
    data=plot,  # データ部分
    layout=layout,  # レイアウト部分
    frames=frames,  # アニメーション部分
)
# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_play_divide"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

go.Frameでフレームを作成

# アニメーションの各フレームの設定
frames = [
    # (x1, y1)=(1, 1), (x2, y2)=(2, 2)の直線
    go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
    # (x1, y1)=(1, 1), (x2, y2)=(4, 4)の直線
    go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
    # (x1, y1)=(3, 3), (x2, y2)=(4, 4)の直線
    go.Frame(
        data=[go.Scatter(x=[3, 4], y=[3, 4])],
        # アニメーション終了でグラフタイトルを変更
        layout=go.Layout(title_text='End Title'),
    )
]

で、重要なのが変数framesのアニメーションの各フレームの設定。list形式で各フレームで表現したいグラフの内容やレイアウトの設定を格納する。今回は3種類。

go.Frameの引数dataでデータ内容を、layoutでレイアウト内容を設定可能。framesを設定できたら、最後にこれをgo.Figureの引数framesに入れるだけ。

なお、今回は最後のフレームを処理する際にtitle_text='End Title'でグラフタイトルを変更している。layoutを指定しない場合はレイアウトの変更はない。

初期プロットもアニメーション化


気づいた方もいるかもしれないが、さっきのアニメーショングラフだと、初期プロットは「Play」ボタンを押しても表示されない。あくまでもgo.Frameで指定したプロットのみ表示される。

本記事に載せているグラフはサイトを開いた時点で自動的にアニメーションが動いてしまうからわからんが、Plotly公式の「Intro to Animations in Python」の「Simple Play Button」だと初期プロットの存在が確認できる。

初期プロットもアニメーションに組み込みたいって場合は、framesgo.Frameに初期プロットの設定を追記したらいい。ただし、初期プロット→フレームの順番になるから、アニメーションが一瞬遅くなる。

# 初期グラフもアニメーションに

fig = go.Figure(
    # データの作成
    data=[go.Scatter(x=[0, 1], y=[0, 1])],
    # レイアウトの設定
    layout=go.Layout(
        # 横軸・縦軸の範囲設定
        xaxis=dict(range=[0, 5], autorange=False),
        yaxis=dict(range=[0, 5], autorange=False),
        # グラフタイトル
        title="Start Title",
        # 再生用のボタン作成
        updatemenus=[dict(
            type="buttons",  # ボタンの形式
            buttons=[dict(label="Play",  # ボタンのラベル
                          method="animate",  # ボタンの役割はアニメーション
                          # ボタンを押して変更する内容(今回はなし)
                          args=[None])])]
    ),
    # アニメーションの各フレームの設定
    frames=[
        # 初期グラフ分をアニメーションに組み込む
        go.Frame(data=[go.Scatter(x=[0, 1], y=[0, 1])]),
        go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
        go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
        # 最後のフレームが終わればグラフタイトルを変更
        go.Frame(data=[go.Scatter(x=[3, 4], y=[3, 4])],
                 layout=go.Layout(title_text="End Title"))]
)    # アニメーションの各フレームの設定
    frames=[
        # 初期グラフ分をアニメーションに組み込む
        go.Frame(
            data=[go.Scatter(x=[0, 1], y=[0, 1])],
            layout=go.Layout(title_text='Start Title')),
        go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
        go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
        # 最後のフレームが終わればグラフタイトルを変更
        go.Frame(data=[go.Scatter(x=[3, 4], y=[3, 4])],
                 layout=go.Layout(title_text='End Title'))]
)
# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
# グラフの表示
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_play_initial"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

pxでもアニメーショングラフを作成


ここまでgoを使ってアニメーショングラフを作成したが、pxを使っても作成可能。詳しくは以下の記事にて解説しているが、pxの方が楽に作成できる。

【px&animation】plotly.expressでアニメーションのグラフを作成

続きを見る

pxの場合は単にデータを配列で指定するのではなく、pandasのデータフレームを使用するのが一般的であり簡単。ということで、ここでもpandasのデータフレームを使うことにする。

なお、作成されたグラフには自動で再生/一時停止のボタンやスライダーが表示される。取り付ける手間がないのが魅力的。

# pxでもアニメーションを作成

data = {
    'x': [
        0, 1,  # フレーム1
        1, 2,  # フレーム2
        1, 4,  # フレーム3
        3, 4,  # フレーム4
    ],
    'y': [
        0, 1,  # フレーム1
        1, 2,  # フレーム2
        1, 4,  # フレーム3
        3, 4,  # フレーム4
    ],
    'frame': [
        0, 0,  # フレーム1
        1, 1,  # フレーム2
        2, 2,  # フレーム3
        3, 3,  # フレーム4
    ],
}

# 同じframe列の値が1つのフレームのプロット
df = pd.DataFrame(data)
print(df)
#    x  y  frame
# 0  0  0       0
# 1  1  1       0
# 2  1  1       1
# 3  2  2       1
# 4  1  1       2
# 5  4  4       2
# 6  3  3       3
# 7  4  4       3

# グラフの作成
fig = px.line(
    markers=True,  # マーカーの追加
    data_frame=df, x='x', y='y',
    animation_frame='frame',  # フレームを区別する列名
    range_x=[0, 5],  # 横軸の表示範囲
    range_y=[0, 5],  # 縦軸の表示範囲
)
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_px"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

durationeasingで遅延と動作を変更


ボタンの引数argsで、各フレームの遅延とフレーム間の動き方を変更することが可能。詳しくは以下の記事で解説している。

【plotly&animation】動き方のeasingと遅延のduration

続きを見る

上のグラフでは各フレーム間の遅延を長くして動作をゆっくりに、さらにフレーム間の動き方をbounce-in-outでバウンスするように変更した。

durationeasingについてはPlotly公式でもあまり解説されていないので、手探りで情報をかき集めるしかないのがネックな点だ。

# easingとdurationを変更

data = go.Scatter(x=[0, 1], y=[0, 1])  # 初期表示データ
plot = [data]  # listにする

# ボタンの挙動
buttons = dict(
    label='Play',  # ボタンのラベル
    method='animate',  # ボタンの形式
    # ボタンを押した時のプロット・レイアウトの変更点
    args=[
        None,  # プロットの内容は変更なし
        # フレームの遅延と再描画のオフ
        {'frame': {'duration': 1500, 'redraw': False},
         'fromcurrent': True,  # 現在のフレームから再生
         # フレーム間の繊維の設定
         'transition': {
            'duration': 300,  # フレーム間の遅延
            'easing': 'bounce-in-out'  # フレーム間の移動方法
        }}
    ]
)

# レイアウトにボタンを配置
updatemenus = dict(
    type='buttons',  # ボタンの形式はボタン式に
    buttons=[buttons]  # ボタンの設定はここに
)

# レイアウトの設定
layout = go.Layout(
    # 横軸・縦軸の表示範囲
    # autorange=Trueにすると各フレームで自動で表示範囲が調整されてしまう
    xaxis=dict(range=[0, 5], autorange=False),
    yaxis=dict(range=[0, 5], autorange=False),
    title='Start Title',  # 初期からフレーム終了までのグラフタイトル
    updatemenus=[updatemenus],  # ボタンのレイアウト設定はここに
)

# アニメーションの各フレームの設定
frames = [
    # (x1, y1)=(1, 1), (x2, y2)=(2, 2)の直線
    go.Frame(data=[go.Scatter(x=[1, 2], y=[1, 2])]),
    # (x1, y1)=(1, 1), (x2, y2)=(4, 4)の直線
    go.Frame(data=[go.Scatter(x=[1, 4], y=[1, 4])]),
    # (x1, y1)=(3, 3), (x2, y2)=(4, 4)の直線
    go.Frame(
        data=[go.Scatter(x=[3, 4], y=[3, 4])],
        # アニメーション終了でグラフタイトルを変更
        layout=go.Layout(title_text='End Title'),
    )
]

fig = go.Figure(
    data=plot,  # データ部分
    layout=layout,  # レイアウト部分
    frames=frames,  # アニメーション部分
)
# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_args"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

各フレームでレイアウトを変更


今度は各フレームでレイアウトを変更してみた。やっていることは簡単で、framesの各go.Framelayoutの内容を変更するだけ。

ここではグラフタイトルやグラフテーマ(テンプレート)、軸ラベルを変更した。グラフテーマ(テンプレート)については以下の記事で解説している。

【plotly&グラフテーマ】plotly既存のthemasをグラフ化

続きを見る

注意点は一度設定したdata, layoutの内容は上書きしない限り残り続けるということ。2つ目のフレームではxaxis_title='second frame'で横軸ラベルを追加しているが、一度表示されると再度Playしても消えることはない。

一方で、title_text='first frame’template='ggplot2’は最後のフレームでそれぞれtitle_text='End Title’template='plotly’に変更しているので表示が変わる。

なので、途中で変更した設定を消したい(非表示にしたい)場合はその旨を設定する必要がある。

# 各フレームでレイアウトを変更

data = go.Scatter(x=[0, 1], y=[0, 1])  # 初期表示データ
plot = [data]  # listにする

# ボタンの挙動
buttons = dict(
    label='Play',  # ボタンのラベル
    method='animate',  # ボタンの形式
    # ボタンを押した時のプロット・レイアウトの変更点(今回はなし)
    args=[None]
)

# レイアウトにボタンを配置
updatemenus = dict(
    type='buttons',  # ボタンの形式はボタン式に
    buttons=[buttons]  # ボタンの設定はここに
)

# レイアウトの設定
layout = go.Layout(
    # 横軸・縦軸の表示範囲
    # autorange=Trueにすると各フレームで自動で表示範囲が調整されてしまう
    xaxis=dict(range=[0, 5], autorange=False),
    yaxis=dict(range=[0, 5], autorange=False),
    title='Start Title',  # 初期からフレーム終了までのグラフタイトル
    updatemenus=[updatemenus],  # ボタンのレイアウト設定はここに
)

# アニメーションの各フレームの設定
frames = [
    # (x1, y1)=(1, 1), (x2, y2)=(2, 2)の直線
    go.Frame(
        data=[go.Scatter(x=[1, 2], y=[1, 2])],
        layout=go.Layout(
            title_text='first frame',  # グラフタイトル
            template='ggplot2',  # グラフのテンプレート(テーマ)
        )
    ),
    # (x1, y1)=(1, 1), (x2, y2)=(4, 4)の直線
    go.Frame(
        data=[go.Scatter(x=[1, 4], y=[1, 4])],
        layout=go.Layout(xaxis_title='second frame')  # 横軸ラベル
    ),
    # (x1, y1)=(3, 3), (x2, y2)=(4, 4)の直線
    go.Frame(
        data=[go.Scatter(x=[3, 4], y=[3, 4])],
        # アニメーション終了でグラフタイトルを変更
        layout=go.Layout(
            title_text='End Title',
            template='plotly',  # グラフのテンプレート(テーマ)
        ),
    )
]

fig = go.Figure(
    data=plot,  # データ部分
    layout=layout,  # レイアウト部分
    frames=frames,  # アニメーション部分
)
# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_layout"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

アニメーショングラフにスライダーとボタンを追加


最後はPlotly公式の「Intro to Animations in Python」の「Using a Slider and Buttons」で作成されているグラフについて。

このグラフはアニメーションってだけじゃなくて、「Play」「Pause」ボタンにスライダーも追加されている。pxだと簡単に追加できたが、goの場合はかなり大変。

以下にPlotly公式のコードにコメントなどを追加したコードを示す。かなり長いのでコードを確認する際には注意。

なお、上のグラフを作成するためのステップは大まかには以下。

  1. データの読み込み
  2. アニメーションのフレームと大陸名の定義
  3. fig設定用のdict作成
  4. レイアウト設定
  5. ボタンの作成
  6. スライダーの作成
  7. 初期表示用のプロット作成
  8. アニメーションのフレーム作成
  9. グラフ表示と保存

以下より各部分の解説を行う。

使用するデータ

url = "<https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv>"
dataset = pd.read_csv(url)
print(dataset)
#           country  year         pop continent  lifeExp   gdpPercap
# 0     Afghanistan  1952   8425333.0      Asia   28.801  779.445314
# 1     Afghanistan  1957   9240934.0      Asia   30.332  820.853030
# 2     Afghanistan  1962  10267083.0      Asia   31.997  853.100710
# 3     Afghanistan  1967  11537966.0      Asia   34.020  836.197138
# 4     Afghanistan  1972  13079460.0      Asia   36.088  739.981106
# ...           ...   ...         ...       ...      ...         ...
# 1699     Zimbabwe  1987   9216418.0    Africa   62.351  706.157306
# 1700     Zimbabwe  1992  10704340.0    Africa   60.377  693.420786
# 1701     Zimbabwe  1997  11404948.0    Africa   46.809  792.449960
# 1702     Zimbabwe  2002  11926563.0    Africa   39.989  672.038623
# 1703     Zimbabwe  2007  12311143.0    Africa   43.487  469.709298

# [1704 rows x 6 columns]

まずは使用するデータについて。使用するのはGapminderの国名・年・人口・大陸・寿命・GDPのデータ。Gapminderは「FACTFULNESS」で有名な財団。

このブログでも以下の記事でデータを使用している。このデータをpandaspd.read_csvで読み込んだ。

bubblechart2017_pop
【plotly&バブルチャート】plotlyで各国の収入と平均寿命をバブルチャートで描く

続きを見る

ubblechart_animation1820_2020_50_latest
【plotly&アニメーション】plotlyで各国の収入と平均寿命の時代変化をバブルチャート&アニメーションで描く

続きを見る

アニメーションフレームの年と大陸を定義

# アニメーションのフレームとなるのは年
years = ["1952", "1962", "1967", "1972", "1977",
         "1982", "1987", "1992", "1997", "2002", "2007"]

# 各大陸名を入れたlistを作成
continents = []
for continent in dataset["continent"]:
    if continent not in continents:
        continents.append(continent)
print(continents)
# ['Asia', 'Europe', 'Africa', 'Americas', 'Oceania']

アニメーションのフレームの基準となる年を変数yearsで定義。データフレームから取ってきてもいい気がする。

また、変数continentsで各大陸名称を取ってきている。continentsの型はlist。なお、以下のように.uniqueを使うことでも取り出せる。この時は型が<class 'numpy.ndarray'>となる。

print(dataset['continent'].unique())
# ['Asia' 'Europe' 'Africa' 'Americas' 'Oceania']

figure作成用のdictを定義

# figure用のdictを作成
fig_dict = {
    'data': [],
    'layout': {},
    'frames': []
}

# レイアウトを編集
# 横軸の表示範囲をと軸ラベルを設定
fig_dict['layout']['xaxis'] = {'range': [30, 85], 'title': 'Life Expectancy'}
# 縦軸ラベルを設定し、縦軸をlogに
fig_dict['layout']['yaxis'] = {'title': 'GDP per Capita', 'type': 'log'}
# マウスホバー時にはカーソルに一番近い点を表示
fig_dict['layout']['hovermode'] = 'closest'

続いてはfigを作成するためのdictを定義。後に解説するが、この変数fig_dictgo.Figurefigに変換する。 dictから作成する方が使い勝手がいいからdictを使用すると思う。

dictの中身はデータのdata、レイアウトのlayout、アニメーションのフレームのframesの3つ。そして、dataframeslistにして各データ・各フレームを格納。レイアウトはそのままkeysvaluesで設定。

また、上記コードの最後の部分でレイアウトの設定を変更している。軸ラベルやhovermodeは好みだが、軸の表示範囲は設定しておかないと、表示範囲がプロットの最小・最大値に応じて自動で変わって見づらい。

# レイアウトを編集
# 横軸の表示範囲をと軸ラベルを設定
fig_dict['layout']['xaxis'] = {'range': [30, 85], 'title': 'Life Expectancy'}
# 縦軸ラベルを設定し、縦軸をlogに
fig_dict['layout']['yaxis'] = {'title': 'GDP per Capita', 'type': 'log'}
# マウスホバー時にはカーソルに一番近い点を表示
fig_dict['layout']['hovermode'] = 'closest'

この設定で変更しているのは以下の3種類。

  • xaxis: 表示範囲と軸ラベル
  • yaxis: 軸ラベルと軸の種類
  • hovermode: マウスオーバー時の挙動

ボタンの設定

# ボタンの設定
fig_dict['layout']['updatemenus'] = [
    {
        'buttons': [
            # 再生用ボタンを作成
            {
                'args': [
                    None,  # プロットの内容は変更なし
                    # フレームの遅延と再描画のオフ
                    {'frame': {'duration': 500, 'redraw': False},
                     'fromcurrent': True,  # 現在のフレームから再生
                     # フレーム間の繊維の設定
                     'transition': {
                         'duration': 300,  # フレーム間の遅延
                        'easing': 'quadratic-in-out'  # フレーム間の移動方法
                    }}
                ],
                'label': 'Play',  # ボタン名
                'method': 'animate'  # ボタンの挙動の種類
            },
            # 停止用ボタンを作成
            {
                'args': [
                    [None],  # プロットの内容は変更なし
                    # フレームの遅延と再描画のオフ
                    {'frame': {'duration': 0, 'redraw': False},
                     'mode': 'immediate',
                     # フレーム間の繊維の設定
                     'transition': {'duration': 0}}  # フレーム間の遅延
                ],
                'label': 'Pause',  # ボタン名
                'method': 'animate'  # ボタンの挙動の種類
            }
        ],
        'direction': 'left',  # 再生・一時停止ボタンを並べる向き
        'pad': {'r': 10, 't': 87},  # プロット領域からの余白の量
        'showactive': False,  # 再生・一時停止ボタンを押した時にハイライトしない
        'type': 'buttons',  # ボタンの種類は独立したボタン
        'x': 0.1,  # ボタンを設置するxの値
        'xanchor': 'right',  # xの位置の基準
        'y': 0,  # ボタンを設置するyの値
        'yanchor': 'top'  # yの位置の基準
    }
]

アニメーショングラフの肝となるボタンの設定。作成するボタンは再生の「Play」と停止の「Pause」ボタン。fig_dict['layout']['updatemenus']listの中で以下の9種類の引数を設定している。

  • buttons: ボタンの内容設定
  • direction: ボタンの向き
  • pad: グラフのプロット部分とボタンの間隔(余白)
  • showactive: 再生/一時停止の時にボタンをハイライトするか否か
  • type: ボタンの形式
  • x: ボタンの横位置
  • xanchor: ボタンの横位置の基準
  • y: ボタンの縦位置
  • yanchor: ボタンの縦位置の基準

2から9についてはコメントのままで理解しやすいと思う。padx, yについては各人で値を変更してみて挙動を確認するのもいいだろう。

1についてはややこしいので以下で取り出して解説する。buttonsの中にはさらにdictが2つあり、それぞれがPlayPauseに対応。

dictargsがボタンの内容、labelがボタンのラベル、そしてmethodがボタンの種類に対応する。

# 再生用ボタンを作成
            {
                'args': [
                    None,  # プロットの内容は変更なし
                    # フレームの遅延と再描画のオフ
                    {'frame': {'duration': 500, 'redraw': False},
                     'fromcurrent': True,  # 現在のフレームから再生
                     # フレーム間の繊維の設定
                     'transition': {
                         'duration': 300,  # フレーム間の遅延
                        'easing': 'quadratic-in-out'  # フレーム間の移動方法
                    }}
                ],
                'label': 'Play',  # ボタン名
                'method': 'animate'  # ボタンの挙動の種類
            },
            # 停止用ボタンを作成
            {
                'args': [
                    [None],  # プロットの内容は変更なし
                    # フレームの遅延と再描画のオフ
                    {'frame': {'duration': 0, 'redraw': False},
                     'mode': 'immediate',
                     # フレーム間の繊維の設定
                     'transition': {'duration': 0}}  # フレーム間の遅延
                ],
                'label': 'Pause',  # ボタン名
                'method': 'animate'  # ボタンの挙動の種類
            }

argsの内容については既に上で解説したが、初めのNoneはプロットの内容は変更しないってことを意味する。

ただ、PlayではNoneNoneのままでPause[None]であることに注意。Pause[None]Noneにしても問題がないが、PlayNone[None]にするとアニメーションが動かない。

Noneの下のdictがレイアウトやアニメーションの設定で、ここではアニメーション間の遅延やフレーム間の遷移の方法を設定している。

スライダーの設定

# スライダーの設定
sliders_dict = {
    'active': 0,  # 初期表示の際にアクティブにする位置
    'yanchor': 'top',  # yの位置の基準
    'xanchor': 'left',  # xの位置の基準
    # 各スライダーのフレームごとのラベル(今回は年)の設定
    'currentvalue': {
        'font': {'size': 20},  # フォントサイズ
        'prefix': 'Year:',  # 接頭辞
        'visible': True,  # ラベルを表示する
        'xanchor': 'right'  # xの位置の基準
    },
    # スライダーの遷移の遅延と遷移の動き方(プロットとは独立して設定)
    'transition': {'duration': 300, 'easing': 'cubic-in-out'},
    'pad': {'b': 10, 't': 50},  # プロット領域からの余白の量
    'len': 0.9,  # スライダーの長さ
    'x': 0.1,  # スライダーを設置するxの値
    'y': 0,  # スライダーを設置するyの値
    'steps': []  # 各スライダーのフレームの設定はここに
}

続いてはスライダーの設定。ここではスライダーの各種設定とスライダー内容を入れるためのlist(’steps’)を設定。詳しいスライダーの解説については以下の記事参照。

【plotly&スライダー】plotlyのslidersにスライダーを追加

続きを見る

スライダーの設定もfigの場合と同じようにsliders_dictdictを定義して内容を設定、その後にレイアウトのスライダーの項目に入れることでスライダーの設定を行う。

ここでの設定内容は以下。

  • active: 初期表示でスライダーを置くインデックス
  • yanchor: スライダーの縦位置の基準
  • xanchor: スライダーの横位置の基準
  • currentvalue: 各スライダーのフレームの設定
  • transition: 各スライダーのフレームの遷移の設定
  • pad: グラフのプロット部分とスライダーの間隔(余白)
  • len: スライダーの長さ
  • x: スライダーの横位置
  • y: スライダーの縦位置
  • steps: 各スライダーのフレームの設定

最後のstepsに各スライダーのフレームの内容(どのデータをプロットするのかなど)を設定する。ここの設定は後述。アニメーションのフレーム作成と同時に行う。

初期表示用のプロット作成

# 初期表示用のプロットの作成
year = 1952
for continent in continents:
    # 1952年のデータを抽出
    dataset_by_year = dataset[dataset['year'] == year]
    # 1952年のデータから各大陸のデータを抽出
    dataset_by_year_and_cont = dataset_by_year[
        dataset_by_year['continent'] == continent]

    # 初期表示用のプロットを作成
    data_dict = {
        'x': list(dataset_by_year_and_cont['lifeExp']),  # xの値
        'y': list(dataset_by_year_and_cont['gdpPercap']),  # yの値
        'mode': 'markers',  # プロットはマーカーのみ
        'text': list(dataset_by_year_and_cont['country']),  # ホバーに国名を追加

        'marker': {
            'sizemode': 'area',  # マーカーサイズの反映のさせ方
            'sizeref': 200000,  # マーカーサイズをスケーリングする引数
            'size': list(dataset_by_year_and_cont['pop'])  # マーカーサイズ
        },
        'name': continent  # プロットの名称
    }
    # figのデータに格納
    fig_dict['data'].append(data_dict)

続いては初期表示用のプロット作成。アニメーションプロットでは、初期表示用のプロットとアニメーション中のプロットで分けないといけない。

今回は初期表示は一番古い1952年のデータを使いたいので、pandasのデータフレームdatasetからdataset[dataset['year'] == year]で1952年のデータフレーム内容を抽出。

1952年だけのデータフレームdataset_by_yearからさらにdataset_by_year['continent'] == continent]で各大陸のデータを抽出している。dataset_by_year、Oceaniaの場合のdataset_by_year_and_contは以下。

print(dataset_by_year)
#                  country  year         pop continent  lifeExp    gdpPercap
# 0            Afghanistan  1952   8425333.0      Asia   28.801   779.445314
# 12               Albania  1952   1282697.0    Europe   55.230  1601.056136
# 24               Algeria  1952   9279525.0    Africa   43.077  2449.008185
# 36                Angola  1952   4232095.0    Africa   30.015  3520.610273
# 48             Argentina  1952  17876956.0  Americas   62.485  5911.315053
# ...                  ...   ...         ...       ...      ...          ...
# 1644             Vietnam  1952  26246839.0      Asia   40.412   605.066492
# 1656  West Bank and Gaza  1952   1030585.0      Asia   43.160  1515.592329
# 1668         Yemen, Rep.  1952   4963829.0      Asia   32.548   781.717576
# 1680              Zambia  1952   2672000.0    Africa   42.038  1147.388831
# 1692            Zimbabwe  1952   3080907.0    Africa   48.451   406.884115

# [142 rows x 6 columns]

print(dataset_by_year_and_cont)  # Oceaniaの場合
# Oceania           country  year        pop continent  lifeExp    gdpPercap
# 60      Australia  1952  8691212.0   Oceania    69.12  10039.59564
# 1092  New Zealand  1952  1994794.0   Oceania    69.39  10556.57566

data_dictsliders_dictなどと同じように、 figに入れるために一時的にdictにしている。

で、ここが面白いところだが、data_dictxyを設定するだけで自動でgo.Scatterが設定される。わざわざgo.Scatterと書いて散布図を設定する必要がない。

go.Scatterpxでの散布図px.scatterについては以下の記事で詳しく解説している。

【plotly&go.Scatter】plotlyの散布図グラフの描き方

続きを見る

【plotly&px.scatter】pxでの散布図の描き方

続きを見る

    # 初期表示用のプロットを作成
    data_dict = {
        'x': list(dataset_by_year_and_cont['lifeExp']),  # xの値
        'y': list(dataset_by_year_and_cont['gdpPercap']),  # yの値
        'mode': 'markers',  # プロットはマーカーのみ
        'text': list(dataset_by_year_and_cont['country']),  # ホバーに国名を追加

        'marker': {
            'sizemode': 'area',  # マーカーサイズの反映のさせ方
            'sizeref': 200000,  # マーカーサイズをスケーリングする引数
            'size': list(dataset_by_year_and_cont['pop'])  # マーカーサイズ
        },
        'name': continent  # プロットの名称
    }
    # figのデータに格納
    fig_dict['data'].append(data_dict)

設定内容はx, yで横軸・縦軸の値、modeでプロットの種類、textでマウスオーバー時の追加表示項目、markerでマーカーの設定、最後にnameで各プロット(ここでは大陸名)の名前を設定している。

data_dictの設定が終わったら、fig_dictdata部分であるfig_dict[’data’]に追加する。

アニメーションのフレームの設定

# アニメーションのフレーム作成
for year in years:
    # 各年のフレームを入れるためのdict
    frame = {'data': [], 'name': str(year)}
    for continent in continents:
        # years, yearはstrだからdatasetで検索する際にはintに変換
        dataset_by_year = dataset[dataset['year'] == int(year)]
        # 各大陸のデータを抽出
        dataset_by_year_and_cont = dataset_by_year[
            dataset_by_year['continent'] == continent]

        # データの作成
        data_dict = {
            'x': list(dataset_by_year_and_cont['lifeExp']),
            'y': list(dataset_by_year_and_cont['gdpPercap']),
            'mode': 'markers',
            'text': list(dataset_by_year_and_cont['country']),
            'marker': {
                'sizemode': 'area',
                'sizeref': 200000,
                'size': list(dataset_by_year_and_cont['pop'])
            },
            'name': continent
        }
        # frameのデータに各都市・各大陸のデータを格納
        frame['data'].append(data_dict)

    # figのframeに各年・全大陸のframeを追加
    fig_dict['frames'].append(frame)
    # スライダーの作成
    slider_step = {'args': [
        # スライダーとアニメーションを紐づけるのはyear
        [year],  # listにしないとスライダーが動かない
        # frameの設定
        {'frame': {'duration': 300, 'redraw': False},
         'mode': 'immediate',
         'transition': {'duration': 300}}
    ],
        'label': year,  # スライダーのラベル
        'method': 'animate'}
    # 作成したスライダーのstepに格納
    sliders_dict['steps'].append(slider_step)

# figのlayoutのslider設定に作成したスライダー内容を格納
fig_dict['layout']['sliders'] = [sliders_dict]

メインであるアニメーションのフレームの設定。各フレームは年で分けられているのでyearsをforで回している。で、各年のフレームのデータの設定と年の情報を変数frameで定義している。

年毎にfor continentで大陸をさらにforで回す。dataset_by_yearからframe['data'].append(data_dict)までは初期表示プロット作成の時と同じ。単に1952年が各年も含めて作成されているだけ。

ここでの追加事項はframe['data'].append(data_dict)の後のfig_dict['frames'].append(frame)から後の部分。フレーム内容の設定と各フレームでのスライダーの設定。

# figのframeに各年・全大陸のframeを追加
    fig_dict['frames'].append(frame)
    # スライダーの作成
    slider_step = {'args': [
        # スライダーとアニメーションを紐づけるのはyear
        [year],  # listにしないとスライダーが動かない
        # frameの設定
        {'frame': {'duration': 300, 'redraw': False},
         'mode': 'immediate',
         'transition': {'duration': 300}}
    ],
        'label': year,  # スライダーのラベル
        'method': 'animate'}
    # 作成したスライダーのstepに格納
    sliders_dict['steps'].append(slider_step)

# figのlayoutのslider設定に作成したスライダー内容を格納
fig_dict['layout']['sliders'] = [sliders_dict]

fig_dict['frames'].append(frame)はシンプルにfig_dictのフレーム部分である fig_dict['frames']に変数frameを追加。

slider_stepはスライダー設定の時に変数sliders_dictで空のlist’steps’を作成したが、ここに入れる内容の設定。スライダーの各設定については以下の記事で解説している。

【plotly&スライダー】plotlyのslidersにスライダーを追加

続きを見る

ざっくり解説すると、以下の設定を行なっている。

  • args: スライダーの設定
  • label: スライダーに表示するラベル
  • method: スライダーの種類

argsについて注意することが、スライダーの[year]とアニメーションのフレームframe = {'data': [], 'name': str(year)}nameが同じでないといけない点。

ここの値が異なっている場合はアニメーションは動くがスライダーは停止したままだったり、そもそもアニメーションが動かない可能性がある。

もしアニメーションがうまくいかないならこの2点を確認すると良いだろう。

figへの変換と表示・保存

# fig_dictをgo.Figureに変換
fig = go.Figure(fig_dict)

# グラフ全体とホバーのフォントサイズ変更(追記)
fig.update_layout(font_size=20, hoverlabel_font_size=20)
fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_slider_buttons"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

最後は作成したfig_dictgo.Figureに変換し、追記分のフォントサイズ変更、そしてグラフの表示と保存。グラフの保存については以下の記事参照。

plotlyでグラフを静止画として保存する際に出るポップアップ
【plotly&orca】plotlyで静止画保存(orca)

続きを見る

長いコードは分解する


ということで作成されるグラフが上のグラフになるんだが、ここまで来るのにかなり苦労する。goだと各設定の自由度が高い分、それぞれ設定しないといけない。

一方でpxの場合は以下の記事でも書いたように、同様のグラフを簡単に作成可能。以下の記事で解説したpxの方法で作成したグラフは次に紹介する。

【px&animation】plotly.expressでアニメーションのグラフを作成

続きを見る

pxでGapminderのグラフを作成

上で紹介したpxでのアニメーションのグラフ作成方法を使って、今回作成したgoのグラフを再現してみた。多少細かい部分は異なるが、これはレイアウトの設定次第でどうにでもなる。

pxでの作成だとデータフレームが予め出来ていればグラフ化自体のコードはかなり短く済む。サクッと簡単にグラフができるのがpxの魅力。

なお、pxの場合は設定出来る引数が少ないため、軸ラベルなどは別途fig.update_layoutなどで設定する必要がある。

【plotly&fig作成と更新】add_traceやupdate_layoutの使い方

続きを見る

# pxでもアニメーショングラフを作成

df = px.data.gapminder()
print(df)
#           country continent  year  ...   gdpPercap  iso_alpha  iso_num
# 0     Afghanistan      Asia  1952  ...  779.445314        AFG        4
# 1     Afghanistan      Asia  1957  ...  820.853030        AFG        4
# 2     Afghanistan      Asia  1962  ...  853.100710        AFG        4
# 3     Afghanistan      Asia  1967  ...  836.197138        AFG        4
# 4     Afghanistan      Asia  1972  ...  739.981106        AFG        4
# ...           ...       ...   ...  ...         ...        ...      ...
# 1699     Zimbabwe    Africa  1987  ...  706.157306        ZWE      716
# 1700     Zimbabwe    Africa  1992  ...  693.420786        ZWE      716
# 1701     Zimbabwe    Africa  1997  ...  792.449960        ZWE      716
# 1702     Zimbabwe    Africa  2002  ...  672.038623        ZWE      716
# 1703     Zimbabwe    Africa  2007  ...  469.709298        ZWE      716

# [1704 rows x 8 columns]

fig = px.scatter(
    df,  # 使用するデータフレーム
    x='lifeExp', y='gdpPercap',  # 横軸・縦軸とする列名
    animation_frame='year',  # アニメーションにする列名
    animation_group='country',  # アニメーション時に同じグループにする列名
    size='pop', color='continent',  # サイズと色分けの基準とする列名
    hover_name='country',  # ホバーに表示する情報
    log_y=True,  # 縦軸をlogに
    size_max=55,  # sizeの最大値
    range_x=[30, 85],  # 横軸の表示範囲
)

# 横軸・縦軸ラベルの変更
fig.update_layout(xaxis_title='Life Expectancy', yaxis_title='GDP per Capita')
# グラフ全体とホバーのフォントサイズ変更
fig.update_layout(font_size=20, hoverlabel_font_size=20)
# fig.show()

# グラフ保存
prefix = 'go-animation'  # 保存ファイル名の接頭辞
save_name = f"{prefix}_px_animation"
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html")
pio.write_image(fig, f"{save_name}.png")

複雑なgoのアニメーション

ということで、今回はPlotlygoを使ったアニメーショングラフの作成方法について解説した。

goでのアニメーショングラフは、再生ボタン作成だけならそこまで難しくない。しかし一時停止ボタンやスライダーをつけるとなるとかなり面倒。

したがって、アニメーションについてはgoではなくpxを使うのが楽だろうという本末転倒な結論で本記事を締める。

関連記事

自作したplotlyのマーカーのシンボル一覧
【plotly&マーカー】plotlyのマーカーのシンボル

続きを見る

【plotly&config】グラフのツールバーを編集する

続きを見る

【plotly&ボタン】plotlyのupdatemenusにbuttonsを追加

続きを見る

【plotly&ボタン】plotlyのupdatemenusに2回押し対応のbuttonsを追加

続きを見る

【plotly&スライダー】plotlyのslidersにスライダーを追加

続きを見る

bubblechart2017_pop
【plotly&バブルチャート】plotlyで各国の収入と平均寿命をバブルチャートで描く

続きを見る

【plotly&animation】動き方のeasingと遅延のduration

続きを見る

【px&バブルチャート】plotly.expressで各国の収入と平均寿命の時代変化をアニメーションで

続きを見る

【plotly&rangeslider/rangeselector】レンジスライダーとレンジセレクターで時系列を見やすく

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

完全ワイヤレスイヤホン(TWS)

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-go
-, , ,