カテゴリー

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

go

【plotly&グラフ内グラフ】plotlyでグラフ内に別グラフやグラフの一部拡大を描画

2021年8月1日

こんな人にオススメ

以前、matplotlib.pyplotpltでグラフ内に他のグラフを作成するという記事があったけど、plotlyでグラフ内グラフって描けるの?

plotlyの方が汎用性が高いから描きたいんだけど。

ということで、以前作成したpltでのグラフ内グラフをplotlyを使用して作成する。plotlyの方がカスタムのしがいがあるから便利。以前のpltの記事は以下。

【plt&グラフ内グラフ】matplotlibでグラフ内に別グラフやグラフの一部拡大を描画

続きを見る

ただし、グラフ内グラフを使用する機会があるかどうかで言えばあまりない印象。執筆者はpltではグラフ内グラフを作成したことがあるが、plotlyではない。グラフ内グラフのイメージは以下の感じ。

python環境は以下。

  • Python 3.9.4
  • plotly 4.14.3
  • plotly-orca 3.4.2
  • numpy 1.20.3

運営者のメガネです。YouTubeTwitterInstagramも運営中。自己紹介お問い合わせページあります。

運営者メガネ

下準備

import numpy as np
import plotly.colors
import plotly.graph_objs as go
import plotly.io as pio

x = np.arange(0, 10, 0.1)
y1 = x ** 1
y11 = x ** 1.1
y15 = x ** 1.5

# プロットデータをまとめる
ys = {'y1': y1, 'y11': y11, 'y15': y15, }
# グラフラベルを予めまとめる
labels = {
    'y1': 'y=x',
    'y11': 'y=x<sup>1.1</sup>',
    'y15': 'y=x<sup>1.5</sup>',
}

まずは下準備としてのimport関連。以下に示すpltの時と同じデータを使用。予めプロットとラベルはdictに入れて一括管理する。

$\begin{aligned} y1\ &=\ x^1 \\ y11\ &=\ y^{1.1} \\ y15\ &=\ x^{1.5} \end{aligned}$

pltplotlyでの上付き文字とかの違い

# plotly
labels = {
    'y1': 'y=x',
    'y11': 'y=x<sup>1.1</sup>',
    'y15': 'y=x<sup>1.5</sup>',
}

# plt
labels = {
    'y1': 'y=x',
    'y11': 'y=x$^{1.1}$',
    'y15': 'y=x$^{1.5}$',
}

pltの時と異なっているのがこのラベル部分。累乗部分の表記が異なっている。というのも、pltの場合は$\LaTeX$の書式で数式を書くが、plotlyの場合はHTMLコードで書く。上下付き文字の対応表は以下。調べればたくさん出る。

項目 plt plotly
上付き文字 ^{2}({}内が1文字だけなら{}は省略可能) <sup>2</sup>
下付き文字 _{2}({}内が1文字だけなら{}は省略可能) <sub>2</sub>

グラフ内グラフを作成


まずは簡単にグラフ内グラフを作成する。キーワードはdomain。これまで何回もplotlyのReferenceは読んできたがその中にあった引数の1つがこのdomain。これまでノータッチだったがここで登場。

全体のコードは以下。go.Layoutの引数templateでレイアウトのテンプレートを指定することができる。また、グラフ内グラフの軸名はx2, y2という名称にしているのでLayoutの引数もxaxis2, yaxis2のようになる。デフォルトはx1, y1

def graph_add(domain: list, save_name: str, tmplt='plotly',
              xanchor='y2', yanchor='x2', mirror='allticks'):
    """大元のグラフにグラフ内グラフを作成

    Parameters
    ----------
    domain : list
        グラフ内グラフの相対座標
    save_name : str
        ファイルの保存名の一部
    tmplt : str, optional
        グラフの見た目のテンプレート, by default 'plotly'
    xanchor : str, optional
        グラフ内グラフの横軸の基準, by default 'y2'
    yanchor : str, optional
        グラフ内グラフの縦軸の基準, by default 'x2'
    mirror : str, optional
        グラフの枠線を表示するか否か, by default 'allticks'
    """

    d1 = go.Scatter(
        x=x, y=y1, name=labels['y1'],
    )

    d11 = go.Scatter(
        x=x, y=y11, name=labels['y11'],
        # グラフをどの軸に置くかを決める
        xaxis='x2', yaxis='y2',
    )

    d15 = go.Scatter(
        x=x, y=y15, name=labels['y15'],
        xaxis='x2', yaxis='y2',
    )
    plot = [d1, d11, d15]

    # グラフ内グラフを置く相対的x, yの位置
    xdomain = domain[0:2]
    ydomain = domain[2:4]

    layout = go.Layout(
        template=tmplt,
        xaxis=dict(
            # グラフ内グラフを見やすくするためにグラフ枠を追加
            linecolor='black', mirror=mirror,
        ),
        yaxis=dict(
            # グラフ内グラフを見やすくするためにグラフ枠を追加
            linecolor='black', mirror=mirror,
        ),
        xaxis2=dict(
            linecolor='black', mirror=mirror,
            domain=xdomain,
            # グラフ内グラフの横軸を置く位置をy1にするかy2にするか
            anchor=xanchor,
        ),
        yaxis2=dict(
            linecolor='black', mirror=mirror,
            domain=ydomain,
            # グラフ内グラフの縦軸を置く位置をx1にするかx2にするか
            anchor=yanchor,
        ),
    )
    fig = go.Figure(data=plot, layout=layout)
    fig.show()

    file = f"insert_{save_name}"
    pio.write_html(fig, f"{file}.html")
    pio.write_image(fig, f"{file}.png")

なお、グラフ内グラフを置く位置は以下の位置。domain[(xの初めの位置), (xの終わりの位置), (yの初めの位置), (yの終わりの位置)]

# グラフ内グラフを置く位置
x0, width, y0, height = 0.55, 0.3, 0.1, 0.4

デフォルトのグラフ

# デフォルトのグラフ
# domainは[x0, xend, y0, yend]で指定
graph_add(
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='default',
)

まずはデフォルトのレイアウトでグラフを作成。domainは相対座標として指定するので0から1の間しか受け付けない。この範囲外だとエラーになる。

デフォルトのレイアウトは'plotly'というレイアウト名のようで背景が青っぽい紫っぽい色だ。できるグラフは以下。


テンプレートがOFF

# レイアウトのテンプレートがOFFのグラフ
graph_add(
    tmplt='none',
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='none',
)

テンプレートを司るtmplt引数を'none'にすることでレイアウトのテンプレートをなしにすることができる。リセットすると白を基調とした色合いになる。


グラフ枠線がない場合

# グラフ枠線がない場合
graph_add(
    tmplt='none',
    domain=[x0, x0 + width, y0, y0 + height],
    mirror=False,
    save_name='mirror_False',
)

で、Layoutの引数xaxis2, yaxis2にはmirrorという引数がある。今回のgraph_add関数ではデフォルトでallticksにしている。allticksにするとグラフの枠がつくんだが、これをFalseにすると枠がなくなる。

テンプレートが大元のグラフでもグラフ内グラフでも同じなので、グラフ内グラフがどこからどこまでなのかが分かりにくい。ということで枠線をつけている。


グラフ内グラフのx基準をy1に変更

# グラフ内グラフの横軸の基準をy2ではなくy1に変更
graph_add(
    tmplt='none', xanchor='y1',
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='xanchor_y1',
)

また、引数xanchor, yanchorをそれぞれ'y2', 'x2'にしているけど、これはグラフ内グラフの軸の基準位置をどこにするかを示すもの。仮にxanchory1にしてみると軸ラベルがx1と合体してしまう。

xanchorに指定するのがy2なのでややこしいが、イメージはどの軸の位置に置くかってことだと思っている。ここではx2軸はy1に置いたからx1軸と軸の位置が一致したイメージ。多分。


グラフ内基準のyの基準をx1に変更

# グラフ内グラフの縦軸の基準をx2ではなくx1に変更
graph_add(
    tmplt='none', yanchor='x1',
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='yanchor_x1',
)

反対にyanchorを'x1'にするとさっきのyバージョンでy2軸がy1軸に一致する。


グラフ内グラフの基準座標を0にする

# グラフ内グラフの基準を0にする
graph_add(
    tmplt='none',
    domain=[0, 0 + width, 0, 0 + height],
    save_name=0,
)

では、グラフ内グラフの基礎的なことの最後として、グラフ内グラフの相対位置の基準を示しておく。pltの場合ではグラフ内グラフの作成方法にax.inset_axesfig.add_axesがあった。

ax.inset_axesはプロット領域の左端、すなわちx1, y1軸を基準にしていた。一方でfig.add_axesはグラフ全体の左下を基準としていた。

今回のplotlydomainではax.inset_axesと同じようにプロット領域の左端を基準にしているようだ。


グラフ内グラフを2つにする


グラフ内グラフを複数個作成することも可能。シンプルにdomaindomain2domain3のように、接尾辞をつけると良い。

def graph_add3(domain2: list, domain3: list, save_name: str,
               xanchor2='y2', yanchor2='x2', xanchor3='y3', yanchor3='x3',
               mirror='allticks'):

    d1 = go.Scatter(
        x=x, y=y1, name=labels['y1'],
    )

    d11 = go.Scatter(
        x=x, y=y11, name=labels['y11'],
        xaxis='x2', yaxis='y2',
    )

    # y15はy3のグラフ内グラフにする
    d15 = go.Scatter(
        x=x, y=y15, name=labels['y15'],
        xaxis='x3', yaxis='y3',
    )
    plot = [d1, d11, d15]

    xdomain2, ydomain2 = domain2[0:2], domain2[2:4]
    xdomain3, ydomain3 = domain3[0:2], domain3[2:4]

    layout = go.Layout(
        template='none',
        xaxis=dict(
            linecolor='black', mirror=mirror,
        ),
        yaxis=dict(
            linecolor='black', mirror=mirror,
        ),
        xaxis2=dict(
            linecolor='black', mirror=mirror,
            domain=xdomain2,
            anchor=xanchor2,
        ),
        yaxis2=dict(
            linecolor='black', mirror=mirror,
            domain=ydomain2,
            anchor=yanchor2,
        ),
        xaxis3=dict(
            linecolor='black', mirror=mirror,
            domain=xdomain3,
            anchor=xanchor3,
        ),
        yaxis3=dict(
            linecolor='black', mirror=mirror,
            domain=ydomain3,
            anchor=yanchor3,
        )
    )
    fig = go.Figure(data=plot, layout=layout)
    fig.show()

    file = f"insert_{save_name}"
    pio.write_html(fig, f"{file}.html")
    pio.write_image(fig, f"{file}.png")

グラフ内グラフの座標と起動コードは以下。

# グラフ内グラフを置く位置
width, height = 0.3, 0.4
x0_1, y0_1 = 0.55, 0.1
x0_2, y0_2 = 0.15, 0.5

# レイアウトのテンプレートがOFFのグラフ
graph_add3(
    domain2=[x0_1, x0_1 + width, y0_1, y0_1 + height],
    domain3=[x0_2, x0_2 + width, y0_2, y0_2 + height],
    save_name='add3',
)

グラフ内の一部を拡大したい

ということで準備は終了。ここからはグラフ内の一部を拡大するグラフ内グラフを作成する。基本的にはグラフ内グラフに大元のグラフと同じプロットを作成し、範囲を限定すればいいというもの。だが少しだけ注意点があるからそこは気をつける必要がある。

そもそもなんでグラフの一部を拡大表示したいかというと、下のグラフのように全体のプロットを表示してしまうと四角で囲った左下の部分の構造が見えない。一方で左下に焦点を持ってくると全体が見えない。

これを解決するのがグラフ内グラフで拡大するというもの。もちろんsubplotで別途グラフを用意してもいいけど、グラフの中にあったほうが見やすい。


# 調べたいgraph範囲も描画
def graph_range():
    plot = []
    # yをプロット
    for num, (name, y) in enumerate(ys.items()):
        color = default_color[num]
        d = go.Scatter(x=x, y=y, name=name)
        plot.append(d)

    # 拡大したい部分を四角で描く
    # (-0.2, -0.5)より、横に+2縦に+3した四角
    x0, y0 = -0.2, -0.5
    xend, yend = x0 + 2, y0 + 3
    d = go.Scatter(
        mode='lines',
        x=[x0, xend, xend, x0, x0],
        y=[y0, y0, yend, yend, y0],
        line_color='black',
    )
    plot.append(d)

    layout = go.Layout(
        template='none',
    )
    fig = go.Figure(data=plot, layout=layout)
    fig.show()

    file = 'insert_range'
    pio.write_html(fig, f"{file}.html")
    pio.write_image(fig, f"{file}.png")

graph_range()

plotlyのデフォルトの色を取得

# plotlyのプロットのデフォルトの色
default_color = plotly.colors.DEFAULT_PLOTLY_COLORS
print(default_color)
# ['rgb(31, 119, 180)', 'rgb(255, 127, 14)', 'rgb(44, 160, 44)', 'rgb(214, 39, 40)', 'rgb(148, 103, 189)', 'rgb(140, 86, 75)', 'rgb(227, 119, 194)', 'rgb(127, 127, 127)', 'rgb(188, 189, 34)', 'rgb(23, 190, 207)']
print(type(default_color))
# <class 'list'>

グラフを作成する前の下準備として、plotlyのデフォルトの色を取得しておく。というのも後にグラフ化するが、plotlyでグラフ内グラフを作成すると同じグラフでも色が変わる。

なので、デフォルトの色のセットを取得して、あたかも同じグラフでは同じグラフになるようにする。デフォルトの色はlist形式で格納されているようだ。

グラフ内グラフに大元と同じグラフをプロット


まずはグラフ内グラフに大元と同じグラフを作成。色は同じプロット同士では同じ色になるようにした。また、グラフ内グラフにも凡例が適用されるので、グラフ内グラフの凡例は表示しないようにした。

なお、pltでは凡例は大元とグラフ内で別々だったがplotlyでは同じ位置に凡例が表示される。ここでは非表示にしているが。

def graph_inset(domain: list, save_name: str, colors=default_color):

    plot = []
    for axis in range(1, 2 + 1):
        xaxis, yaxis = f"x{axis}", f"y{axis}"
        # 全凡例が表示になるからグラフ内グラフの凡例は非表示にする
        showlegend = [True, False][axis - 1]

        for num, name in enumerate(ys):

            if colors:  # 入力値がdefault_color
                # 別々の色になるので同じグラフで色を合わせる
                color = default_color[num]
            else:  # 入力値がNone
                # 色は全グラフでバラバラ
                color = None

            d = go.Scatter(
                x=x, y=ys[name], name=labels[name],
                line_color=color,
                xaxis=xaxis, yaxis=yaxis,
                showlegend=showlegend,
            )

            plot.append(d)

    xdomain, ydomain = domain[0:2], domain[2:4]

    layout = go.Layout(
        template='none',
        xaxis=dict(linecolor='black', mirror='allticks',),
        yaxis=dict(linecolor='black', mirror='allticks',),
        xaxis2=dict(
            linecolor='black', mirror='allticks',
            domain=xdomain, anchor='y2',
        ),
        yaxis2=dict(
            linecolor='black', mirror='allticks',
            domain=ydomain, anchor='x2',
        )
    )
    fig = go.Figure(data=plot, layout=layout)
    fig.show()

    file = f"insert_{save_name}"
    pio.write_html(fig, f"{file}.html")
    pio.write_image(fig, f"{file}.png")

プロットする位置はpltの時と同じ位置にする。多少のずれはあるかもしれないがイメージ。

x0, width, y0, height = 0.05, 0.4, 0.35, 0.4
graph_inset(
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='inset',
)

ここで色をデフォルトの色から指定なしのNoneにすると、全プロットで色がバラバラになる。正確にはdefault_colorの色が繰り返されるんだけど、今回のプロットの数で言えばバラバラになる。


graph_inset(
    domain=[x0, x0 + width, y0, y0 + height],
    save_name='inset_None',
    colors=None,
)

グラフ内グラフの表示範囲を調節


先程のグラフだと、グラフ内グラフの表示範囲が大元のグラフと同じだからここでは表示範囲を調節する。といっても単純にxaxis2とyaxis2に引数としてrangeを追加すればいいだけ。

これで当初のグラフを拡大して大元のグラフに表示するという目的が達成できた。

拡大部分を明示したいのにできない(失敗案紹介)

で、拡大できたから解決となるんだけど、pltの時はこれに加えてさらにプロットの拡大部分の明示ができた。具体的には以下のグラフ。グラフのどこの部分を拡大したのかは一緒だけど、加えて拡大領域とグラフ内グラフに線が引かれている。

これをどうにかしてplotlyで実装できないかを模索したけど無理そう。いやいやこの章の初めのグラフではできてるやんと声が聞こえてくるが、そうではない。

pltだとグラフを移動させると青色で示した拡大場所を明示するための線がついてくる。しかしplotlyではこれを実装できなかった。単に知識不足とか検索不足とかかもしれないが。

今のところ考えている解決策が「明示する線の始点固定」なんだけどやり方がわからないので今回は初期表示だけ線を見せるようにした。

そもそもグラフ内グラフは動かせない


上に書いたように、明示する線がうまくいかないから拡大領域をうまく書けない。ならグラフ内グラフを動かせばいいんじゃないのとなるがそれもうまくいかない。

というのもdomainは相対位置だし、そもそも動かすことを前提にしていないのか、そんなオプションはなかった。

# そもそもグラフを動かすと大元のグラフだけ動いてしまう
# domainで指定したグラフは0-1の相対的な位置での固定しかできない
graph_inset(
    domain=[0, 0 + 0.4, 0, 0 + 0.4],
    save_name='inset_0',
)

go.Scatterで場所明示用の線を追加


ということで付け焼き刃的な感じで初期表示だけいい具合にしてみる。まずはgo.Scatterで線を描く。これは今まで通り線のプロットを作成すればいい。ついでに四角もgo.Scatterで作成してみた。

# グラフ内グラフと四角を線で結ぶことに成功
graph_scatter_line(
    domain=[0.05, 0.05 + 0.4, 0.35, 0.35 + 0.4], save_name='scatter_zoom1',
    xrange=(-0.5, 10),  # 大元のグラフの横軸表示範囲
    yrange=(-1, 35),  # 大元のグラフの縦軸表示範囲
    x0=-0.2, width=2,  # 拡大したい領域の横軸の絶対位置と幅
    y0=-0.5, height=3,  # 拡大したい領域の縦軸の絶対位置と高さ
)

このコードだと大元のグラフを動かしても、グラフ内グラフを移動させても初期表示だけはうまくいく。

Layoutshapesで線を追加


線はgo.Scatterでも描けるんだけど、Layoutshapesで図形として線を追加することも可能。やってることは基本的には同じ。ただホバーが効かなくなるので注意。

# グラフ内グラフと四角を線で結ぶことに成功
graph_shape_line(
    domain=[0.05, 0.05 + 0.4, 0.35, 0.35 + 0.4], save_name='shape_zoom1',
    xrange=(-0.5, 10),
    yrange=(-1, 35),
    x0=-0.2, width=2,
    y0=-0.5, height=3,
)

拡大部分もshapesで作成


最後についでっちゃついでだけど、四角領域も図形で作成した。go.Layoutshapesに以下のコードを入れたらいい。

rect = dict(
    type='rect',
    x0=x0,
    x1=x0 + width,
    y0=y0,
    y1=y0 + height,
    line_color='black',
    fillcolor='rgba(238, 130, 238, 0.3)',
    layer='below'  # layerをbelowにするとプロットの下に描かれる
)

plotlyはまだまだ発展途上

今回はplotlyを使用してグラフ内グラフを描画する方法について解説した。pltと同様のグラフを作成できたものの、できないこともあって多少不便。plotlyはまだまだ発展途上なのかなと思った。

でもその発展途上な部分を自らのてでどうやって補っていくのかが1つのカギになるんじゃないだろうか。ということでまた面白いplotlyのグラフを作成できればいいな。

関連記事

【plt&グラフ内グラフ】matplotlibでグラフ内に別グラフやグラフの一部拡大を描画

続きを見る

【plt vs plotly】matplotlibとgoでグラフの比較

続きを見る

【python&imshow】plt.imshowで2次元配列をマップ化

続きを見る

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

続きを見る

ガジェット

2023/11/11

【デスクツアー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/11/4

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

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

ベストバイ

2023/10/29

【ベストバイ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(31万円) x Galaxy Z Fold5(25万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa、 Instagram@megatenpa

-go
-, ,