こんな人にオススメ
plotly
で塗りつぶしをしたいけど、referenceに書いてある内容がよくわからん!
しかも塗りつぶしたらあらゆるところが塗られてしまう!助けて!
ということで、今回はplotly
を使用して領域の塗りつぶしを行う。何かしらの範囲を明示したい時などに塗りつぶしは便利だ。なお、以下の記事でplt
によるグラフの塗りつぶしも行った。
-
-
【plt&fill_between】matplotlibで領域を塗りつぶし
続きを見る
plt
の場合は2プロット作成するだけで簡単に塗りつぶしをすることができたが、plotly
の場合は構造が複雑で簡単には塗りつぶせない。失敗例も示すので参考にしていただきたい。
plotlyのfillをうまく描くには、塗りつぶしたい領域ごとに塗りつぶしを行う。一括でここからここまでのこの条件に当てはまる部分を塗りつぶすというのができない。かなり不便。
python 環境は以下。
- Python 3.9.6
- numpy 1.21.1
- matplotlib 3.4.2
- plotly 5.1.0
- plotly-orca 3.4.2
作成したコード全文
下準備
import sys import numpy as np import matplotlib.cm as cm import plotly import plotly.graph_objects as go import plotly.io as pio sys.path.append('../../') import plotly_layout_template as template
まずは下準備としてのimport
関連。plotly_layout_template
はplotly
の自作テンプレートで以下の記事で解説している。これを使用することで簡単にキレイなグラフレイアウトを作成できる。
-
-
【随時更新 備忘録】plotlyのグラフ即席作成コード
続きを見る
使用するデータは、x
は単なる0から29までの数値で、y
は適当に作成した値とした。また、後から使いやすいようにy1
, y2
をys
としてdict
でまとめ、それぞれのプロットの色も予め決めておいた。
x = np.arange(30) y1 = np.array([ 1, 2, 4, 6, 8, 8, 7, 3, 3, 1, 1, 3, 3, 2, 4, 2, 3, 4, 5, 4, 3, 4, 6, 7, 8, 9, 8, 7, 8, 9 ]) y2 = np.array([ 4, 6, 2, 3, 4, 5, 7, 8, 3, 1, 7, 8, 7, 7, 6, 7, 8, 7, 4, 3, 7, 8, 9, 8, 3, 8, 9, 9, 8, 7 ]) # y1, y2をまとめる ys = {'y1': y1, 'y2': y2} # y1, y2の色を決める colors = {'y1': 'green', 'y2': 'blue'}
プロットデータを作成する関数
def scatter(x, y, name, color, mode='lines+markers', symbol='circle', dash='solid', fill=None, fillcolor=None, hoveron='points'): d = go.Scatter( mode=mode, x=x, y=y, name=name, marker=dict(symbol=symbol, size=10), line=dict(dash=dash, color=color, width=3), fill=fill, fillcolor=fillcolor, hoveron=hoveron, hovertemplate='x: %{x} <br>' + f"{name}: " + "%{y} <br>" ) return d
グラフを描く際に、毎回同じ内容のコードを書くのはダルい。ということで予め関数化して楽をする。上のコードはプロットデータを作成する関数。引数一覧は以下。
x
,y
: 横軸・縦軸のデータ配列name
: プロットの名称mode
: プロットの種類。デフォルトは線と点symbol
: プロット点のマーカー。デフォルトは円dash
: プロットの線種。デフォルトは実線fill
: 塗りつぶす際の基準。デフォルトは塗りつぶさないfillcolor
: 塗りつぶしの色。デフォルトは色なしhoveron
: マウスオーバーに反応する範囲。デフォルトはプロット点
plotly
で関数化して楽にコードを作成する方法については以下参照。
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
塗りつぶす側の設定
引数fill
について、デフォルトはNone
だったが、以下の引数を選択することができる。
'none'
orNone
: 塗りつぶさない'tozeroy'
:y=0
を対象に塗りつぶし(横軸に対して塗りつぶし)'tozerox'
:x=0
を対象に塗りつぶし(縦軸に対して塗りつぶし)'tonexty'
: 指定したプロットの直前のプロットからy
方向に塗りつぶし'tonextx'
: 指定したプロットの直前のプロットからx
方向に塗りつぶし'toself'
: 他のプロットを使用せず、そのプロットで中身を塗りつぶし'tonext'
: 指定したプロットの直前のプロットに対して、x
,y
の両方向に塗りつぶし
後に書くが、今回はy2 < y1
の条件に当てはまる部分を塗りつぶしたい。y
の値で判定するので使用するのはtonexty
。
レイアウト作成用の関数
def set_layout(title): layout = go.Layout( template=template.plotly_layout(), title=dict(text=title), xaxis=dict(range=[-1, 30],), yaxis=dict(range=[0, 10],), legend=dict( y=0, yanchor='bottom', # 凡例の順番が勝手に逆方向になるので戻す traceorder='normal' ), ) return layout
レイアウトも関数化して楽をする。関数の引数title
でグラフのタイトルを設定。go.Layout
の引数template
に自作テンプレートを入れてグラフレイアウトを設定。あとは横軸・縦軸の範囲を設定。
また、legend
で凡例の設定をするが、どうやらfill
をすると凡例の順番が逆になるらしい。なので、traceorder
で凡例の順番をnormal
、すなわち上から下へと設定する。
グラフ保存用の関数
def save(fig, save_name, config): save_name = f"fill_{save_name}" pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca' pio.write_html(fig, f"{save_name}.html", config=config,) pio.write_image(fig, f"{save_name}.png")
グラフ保存に関しても関数化しておく。関数の中身は短いがそれでも1行で済むし、変更する際もこの部分を変更するだけでいいので、変更し忘れもなくなる。
保存するファイル形式はhtmlとpngで、htmlに関しては自作テンプレートのconfig
を使用することで、保存した後のhtmlファイルでもconfig
を適用することができる。
塗りつぶししたいグラフ
そもそも塗りつぶししたいグラフが上。y1
, y2
を使用した。緑線がy1
で青線がy2
、そして、今回はy2 < y1
の条件にあるプロットを赤で表した。点で表すとわかりにくかったので線で表現。
今回は上のグラフのうち、赤に該当する部分、すなわちy2 < y1
の領域を塗りつぶす。上のグラフのコードは以下。
def graph(): plot = [] # y1 y2を設定 for name, val in ys.items(): d = scatter(x=x, y=val, name=name, color=colors[name]) plot.append(d) # y2 < y1の条件に合うy1のプロット condition = y2 < y1 d = scatter( x=x[condition], y=y1[condition], symbol='square', name='y2 < y1', color='red', dash='dash', ) plot.append(d) # グラフレイアウトの設定 layout = set_layout(title='original') config = template.plotly_config() # グラフのconfig fig = go.Figure(data=plot, layout=layout) fig.show(config=config,) # 作成したグラフを保存 save_name = 'original' save(fig=fig, save_name=save_name, config=config,) graph()
塗りつぶしに失敗したグラフ
def ng_graph(title: str, arr_x, arr_y, save_name: str): # 塗りつぶししたいグラフの条件 condition = y2 < y1 plot = [] # y1 y2を設定 for name, val in ys.items(): d = scatter(x=x, y=val, name=name, color=colors[name]) plot.append(d) # y2 < y1の条件に合うy1のプロット d = scatter( x=x[condition], y=y1[condition], symbol='square', name='y2 < y1', color='violet', dash='dot', ) plot.append(d) # y2 < y1を塗りつぶし d = scatter( x=arr_x, y=arr_y, name='fill', symbol='star', color='green', dash='dash', fill='tonexty', fillcolor='rgba(10, 255, 100, 0.6)', hoveron='points+fills', ) plot.append(d) # グラフレイアウトの設定 layout = set_layout(title=title) config = template.plotly_config() # グラフのconfig fig = go.Figure(data=plot, layout=layout) fig.show(config=config,) # 作成したグラフを保存 save(fig=fig, save_name=save_name, config=config,)
まずは塗りつぶしに失敗したグラフをお見せする。シンプルにこうすればいけるじゃんってやつだと失敗する。使用するグラフのコードは上。plotlyの塗りつぶしグラフ
には2つのデータが必要。
- 塗りつぶしの終了を表すデータ
- 塗りつぶしの開始を表すデータ(実際に塗りつぶしをするデータ)
1つ目の塗りつぶしの終了を表すデータは、2つ目の実際に塗りつぶしをするデータをどこまで塗るのかを示すもの。今回は先程のグラフの赤破線のy2 < y1
を満たすプロットを終了とする。
ということはスタート位置となる2つ目のデータはy2
ということになる。すぐにy2
を選択してもいいが、ここでは色々なパターンを先に試しておく。
(y2 < y1)
vs y1
まずはy1
をスタート位置にしてみる。当たり前だがy2
よりy1
の方が大きい部分を塗りつぶさないといけないから、y1
をスタート位置にしてしまうとうまくいかない。
実際に試してみると、y2 < y1
が満たされる領域で、途中でギャップがある部分を塗りつぶしてしまっている。これだと何が何だかわからない。
# y1からy2 < y1の部分まで塗りつぶし ng_graph(title='(y2 < y1) vs y1', arr_x=x, arr_y=y1, save_name='y1',)
(y2 < y1)
vs y2
y2 < y1
を満たすということは、塗りつぶす領域ではy1
よりもy2
の方が値が低いということ。ならy2
をスタート位置にすればいいじゃないかと思って実行してみるとうまくいかない。
実は塗りつぶしの終了位置と開始位置の値の大小は関係ない。なので、とにかく挟まれた領域を全て塗りつぶしたということになる。また、x=0
付近の初めの方ではx=2
の時のy1
の値で塗りつぶされている。
# y2からy2 < y1の部分まで塗りつぶし ng_graph(title='(y2 < y1) vs y2', arr_x=x, arr_y=y2, save_name='y2',)
(y2 < y1)
vs y1[y2 < y1]
余計な部分まで塗りつぶされてしまうのであれば、元から必要な領域だけを使用して塗りつぶしをしたらいい。ということで、条件y2 < y1
に当てはまるy1
のみを使用してグラフを作成した。
もちろんy1
を使用しているのでうまくいくわけではない。しかし、シンプルにy1
を使用した時に比べて避けな部分の色付けがなくなっている。これは勝機。
# y1の内y1がy2より大きい箇所からy2 < y1の部分まで塗りつぶし ng_graph( title='(y2 < y1) vs y1[y2 < y1]', arr_x=x[y2 < y1], arr_y=y1[y2 < y1], save_name='y1_y2y1', )
(y2 < y1)
vs y2[y2 < y1]
y2
からy2 < y1
の条件まで塗りつぶしをするといけるかというと、そうではない。なんと、離れたデータ部分もまるまる塗りつぶされる。これは初めのy1
と同じような現象。
すなわち、plotly
の塗りつぶしは一括でうまくいくというものではない。個別で指定する必要がある。
# y2の内y1がy2より大きい箇所からy2 < y1の部分まで塗りつぶし ng_graph( title='(y2 < y1) vs y2[y2 < y1]', arr_x=x[y2 < y1], arr_y=y2[y2 < y1], save_name='y2_y2y1', )
条件に当てはまる連続した領域をlist
に格納する関数
def get_true(condition): x_true = [[]] # conditionの条件がTrueになるインデックスでのxの値 y1_true = [[]] # conditionの条件がTrueになるインデックスでのy1の値 y2_true = [[]] # conditionの条件がTrueになるインデックスでのy2の値 bool = False for num, cnd in enumerate(condition): # confitionの条件がTrueになるインデックスのとき # そのインデックスに該当するx, y1, y2の値をlistに入れる if cnd: y1_true[-1].append(y1[num]) y2_true[-1].append(y2[num]) x_true[-1].append(x[num]) bool = True # conditionの条件がFalseになるインデックスのとき else: # bool = Trueは1つ前のループで条件がTrueだったインデックス # Trueの連なりが途切れたので、次のTrueの備えてlistを追加 if bool: y1_true.append([]) y2_true.append([]) x_true.append([]) bool = False # boolを元に戻しておく # boolがFalseの時は連続して条件がFalseのインデックスだから何もしない return x_true, y1_true, y2_true
ということで、上の調査で一括での塗りつぶしはギャップ部分まで色をつけてしまうということがわかった。なら、ギャップを無くせばいい、すなわちデータの飛びを無くせばいい。
上のコードは、引数condition
に入れた条件がTrue
なら値を格納、False
なら何もしないという関数。で、True
の直後にFalse
になった場合はFalse
になった値からギャップが生じたことになるので新たにlist
を追加。
要するに、条件に当てはまる部分のうち、連続している領域はひとまとめにして、離れている領域同士はlistを別のものにするということ。具体例を次に示す。
True
の領域を判定する関数のテスト
def check_true(condition, condition_name): # インデックス、y2の値、y1の値、(y2 < y1) & (x < 19)の結果 print('index, y2, y1, y2 < y1') for num, (element2, element1, tf) in enumerate(zip(y2, y1, condition)): print(f"{num: >5}", end=', ') print(f"{element2: >2}", end=', ') print(f"{element1: >2}", end=', ') # !sでboolをstrに変換してboolで出力。変換しないと0, 1が出力 # str(tf)でもboolで出力可能 print(f"{tf!s: >7}") print() # conditionをy1, y2に適用 print(f"y1[{condition_name}]: {y1[condition]}") print(f"y2[{condition_name}]: {y2[condition]}") print() # 連続したTrueがlistに入っているのか確認 zips = zip(('x_true', 'y1_true', 'y2_lst'), get_true(condition=condition)) for name, ans in zips: print(name, ans) print() print()
y2 < y1
の真偽値からTrue
のみを抽出。そのTrue
の時のy1
, y2
の値のうち、連続した部分がlist
入っているかを確かめる。実際に今回のy2 < y1
の場合だと以下のような出力となる。
# 2-5, 18-19, 24-25, 29番目がTrue check_true(condition=y2 < y1, condition_name='y2 < y1') # index, y2, y1, y2 < y1 # 0, 4, 1, False # 1, 6, 2, False # 2, 2, 4, True # 3, 3, 6, True # 4, 4, 8, True # 5, 5, 8, True # 6, 7, 7, False # 7, 8, 3, False # 8, 3, 3, False # 9, 1, 1, False # 10, 7, 1, False # 11, 8, 3, False # 12, 7, 3, False # 13, 7, 2, False # 14, 6, 4, False # 15, 7, 2, False # 16, 8, 3, False # 17, 7, 4, False # 18, 4, 5, True # 19, 3, 4, True # 20, 7, 3, False # 21, 8, 4, False # 22, 9, 6, False # 23, 8, 7, False # 24, 3, 8, True # 25, 8, 9, True # 26, 9, 8, False # 27, 9, 7, False # 28, 8, 8, False # 29, 7, 9, True # y1[y2 < y1]: [4 6 8 8 5 4 8 9 9] # y2[y2 < y1]: [2 3 4 5 4 3 3 8 7] # x_true [[2, 3, 4, 5], [18, 19], [24, 25], [29]] # y1_true [[4, 6, 8, 8], [5, 4], [8, 9], [9]] # y2_lst [[2, 3, 4, 5], [4, 3], [3, 8], [7]]
この例だと2-5, 18-19, 24-25, 29番目がy2 < y1
の条件に対してTrue
なので、x
, y1
, y2
もこれらの領域ごとにデータをlist
に入っている。
要するに、for
の中の2, 3, 4, 5
がy2
で言えば2
, 3
, 4
, 5
、y1
で言えば4
, 6
, 8
, 8
に該当する。そして、for
の中の18
, 19
は2
, 3
, 4
, 5
から離れているので別のlist
になっている。
別の例でも試してみる。次は2つの条件を組み合わせる。条件はy2 < y1
に加えてx < 19
とした。すなわち横軸が小さい中でy2 < y1
判定を行う。
# 2-5, 18番目がTrue check_true( condition=(y2 < y1) & (x < 19), condition_name='(y2 < y1) & (x < 19)', ) # index, y2, y1, y2 < y1 # 0, 4, 1, False # 1, 6, 2, False # 2, 2, 4, True # 3, 3, 6, True # 4, 4, 8, True # 5, 5, 8, True # 6, 7, 7, False # 7, 8, 3, False # 8, 3, 3, False # 9, 1, 1, False # 10, 7, 1, False # 11, 8, 3, False # 12, 7, 3, False # 13, 7, 2, False # 14, 6, 4, False # 15, 7, 2, False # 16, 8, 3, False # 17, 7, 4, False # 18, 4, 5, True # 19, 3, 4, False # 20, 7, 3, False # 21, 8, 4, False # 22, 9, 6, False # 23, 8, 7, False # 24, 3, 8, False # 25, 8, 9, False # 26, 9, 8, False # 27, 9, 7, False # 28, 8, 8, False # 29, 7, 9, False # y1[(y2 < y1) & (x < 19)]: [4 6 8 8 5] # y2[(y2 < y1) & (x < 19)]: [2 3 4 5 4] # x_true [[2, 3, 4, 5], [18], []] # y1_true [[4, 6, 8, 8], [5], []] # y2_lst [[2, 3, 4, 5], [4], []]
この条件だと18番目がTrue
であるが、1点しかないので領域として成立しない。かつ途中の点なので次のループが始まってしまう。
ということで、塗りつぶしには該当せず空のlist
となる。2点以上あると塗りつぶせるので空のlist
にはならない。
これを使うと条件に当てはまる領域を独立してlist
に入れられる。これを使ってplotly
のfill
をしてみる。
成功するplotly
のfill
def ok_graph(condition, title: str, save_name: str): # conditionの条件がTrueの連続したx, y1, y2の値を計算 x_true, y1_true, y2_true = get_true(condition) plot = [] # y1 y2を設定 for name, val in ys.items(): d = scatter(x=x, y=val, name=name, color=colors[name], ) plot.append(d) # 条件に当てはまる領域をそれぞれ塗りつぶし zips = zip(x_true, y1_true, y2_true) for num, (elementx, element1, element2) in enumerate(zips): # 塗りつぶし領域の色を作成 rgb = cm.jet_r(num / len(x_true)) rgb = plotly.colors.convert_to_RGB_255(rgb) color = f"rgba{rgb + (1,)}" fillcolor = f"rgba{rgb + (0.4,)}" # conditionの条件に合うy1のプロット d = scatter( x=elementx, y=element1, name=f"condition #{num}", symbol='square', color=color, dash='dot', ) plot.append(d) # conditionに当てはまる部分を塗りつぶし d = scatter( x=elementx, y=element2, name=f"fill #{num}", symbol='star', color=color, dash='dash', fill='tonexty', fillcolor=fillcolor, hoveron='points+fills', ) plot.append(d) # グラフレイアウトの設定 layout = set_layout(title=title) config = template.plotly_config() # グラフのconfig fig = go.Figure(data=plot, layout=layout) fig.show(config=config,) # 作成したグラフを保存 save_name = f"plotly_fill_{save_name}" save(fig=fig, save_name=save_name, config=config,)
ということで、条件に合う領域を独立して選択し、その領域ごとに塗りつぶしをする。基本的には初めに書いたNGコードと同じだが、領域を独立させたのとその領域ごとにfill
したのが異なる。
以下に、色んな条件下での塗りつぶしを試してみる。
y2 < y1
まずは目的の条件であるy2 < y1
。これに該当するのは4領域であるが、最後の領域は1点しかないため塗りつぶしではなく点となる。したがって、実際に塗りつぶしになるのは3領域。
3領域がちゃんと塗りつぶせているのかというと、ちゃんとy2 < y1
の領域が塗りつぶされているのがわかる。また、領域をそれぞれ独立で作成したので塗りつぶしの色をそれぞれで変更することもできる。
ok_graph( condition=y2 < y1, title='y2 < y1', save_name='y2_less_y1', )
y2 ≤ y1
次はy2 ≤ y1
、すなわちy2 = y1
の点も含める。すると、一番右の1点だけだった部分が塗りつぶしとなる。このほか、左から2番目にy2 = y1
の領域ができるので塗りつぶしではないが色がつく。
y2
, y1
がクロスする点がちょうどy2 = y1
の条件に当てはまるデータ点だった場合、塗りつぶしがちゃんと閉じるのでキレイに見える。
ok_graph( condition=y2 <= y1, title='y2 ≤ y1', save_name='y2_less_equal_y1', )
y2 > y1
お次はy2 < y1
の不等号を逆転させたy2 > y1
。要するにy2
の方が大きい状態。条件が変わるだけでグラフの考え方は同じ。なのでx = 7
ではy2 = y1
になるが>
だと含まれないので塗りつぶしがなくなる。
ok_graph( condition=y2 > y1, title='y2 > y1', save_name='y2_grater_y1', )
y2 ≥ y1
最後はy2 ≥ y1
。y2 = y1
を許容した版。そうするとx = 8, 9
が2つの分断された領域をつなぐ役割を果たすので結果的に分断された領域が1つになる。
これはこれで対処する必要がありそう。どうするかは各々が決めたらいいが、ここではこれでひとまず終了とする。なんで同じ色なんやってなったらつながれているということが当たり前だけど驚き。
もっと良い方法ありそうなんだけどな
今回はplotly
を使用した、領域の塗りつぶしに関して紹介した。実際にコードを作成すると思うが、かなり面倒な処理が必要となる。
今回はデータ数が30なので大したことはないが、データ数が増えるとその分、分割された領域が増えるのでそれらに1つ1つ対処しないといけない。
もっといい方法があるのかもしれないが、とりあえず現状はこんな感じ。ダルい。
-
-
【plotly&buttons】ボタンを押すとプロット・背景の色が変わる変なグラフ
続きを見る
-
-
【plt vs plotly】matplotlibとgoでグラフの比較
続きを見る
-
-
【plotly&mapbox】感染者数と死者数の散布図コロナマップを作成
続きを見る
-
-
【plotly&グラフ内グラフ】plotlyでグラフ内に別グラフやグラフの一部拡大を描画
続きを見る