こんな人にオススメ
以前、matplotlib.pyplot
、plt
でグラフ内に他のグラフを作成するという記事があったけど、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
下準備
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}$
plt
とplotly
での上付き文字とかの違い
# 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'
にしているけど、これはグラフ内グラフの軸の基準位置をどこにするかを示すもの。仮にxanchor
をy1
にしてみると軸ラベルが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_axes
とfig.add_axes
があった。
ax.inset_axes
はプロット領域の左端、すなわちx1
, y1
軸を基準にしていた。一方でfig.add_axes
はグラフ全体の左下を基準としていた。
今回のplotly
のdomain
ではax.inset_axes
と同じようにプロット領域の左端を基準にしているようだ。
グラフ内グラフを2つにする
グラフ内グラフを複数個作成することも可能。シンプルにdomain
にdomain2
やdomain3
のように、接尾辞をつけると良い。
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, # 拡大したい領域の縦軸の絶対位置と高さ )
このコードだと大元のグラフを動かしても、グラフ内グラフを移動させても初期表示だけはうまくいく。
Layout
のshapes
で線を追加
線はgo.Scatter
でも描けるんだけど、Layout
のshapes
で図形として線を追加することも可能。やってることは基本的には同じ。ただホバーが効かなくなるので注意。
# グラフ内グラフと四角を線で結ぶことに成功 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.Layout
のshapes
に以下のコードを入れたらいい。
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を追加
続きを見る