こんな人にオススメ
plolty
で垂直な塗りつぶしがしたい!
プロットとか四角の図形で塗りつぶすとプロット領域をズラした時に塗りつぶした部分がずれちゃう!
ということで、今回はplotly
で画面固定の塗りつぶしを行う。画像として保存する際には塗りつぶしはプロットの上から四角形でも貼っておけば上から下までキレイに塗りつぶせる。
しかし、plotly
の場合は保存した後からもグリグリ動かすことが簡単なので、このように四角を置いただけでは、グラフをズラせば四角もズレてしまう。
plt
の場合はグラフのこの位置に固定ってのができるが、plotly
でもこれは可能。今回はこの固定について解説する。
python環境は以下。
- Python 3.9.6
- numpy 1.21.1
- matplotlib 3.4.2
- plotly 5.1.0
- plotly-orca 3.4.2
なお、今回使用するadd_vrect
とadd_hrect
はplotly
のバージョンが4.12以上でないと使用できないので、この点については注意が必要。
作成したコード全文
下準備
import sys import numpy as np import pandas as pd 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のグラフ即席作成コード
続きを見る
また、使用するデータは横軸は0から29までの配列、縦軸は-10から+10までのランダム値を3種類とした。乱数はseed
で固定し、コードを回すごとに値が変更されないようにした。
x = np.arange(30) data = {} for num in range(3): np.random.seed(num) y = np.random.randint(-10, 10, (30,)) data[f"data{num}"] = y print(data) # {'data0': array([ 2, 5, -10, -7, -7, -3, -1, 9, 8, -6, -4, 2, -9, # -4, -3, 4, 7, -5, 3, -2, -1, 9, 6, 9, -5, 5, # 5, -10, 8, -7]), 'data1': array([ -5, 1, 2, -2, -1, 1, -5, 5, -10, 6, -9, 2, -3, # 3, -4, 8, -5, 8, 1, 0, 4, 8, -6, -1, 7, -10, # 3, -1, -1, -3]), 'data2': array([-2, 5, 3, -2, 1, 8, 1, -2, -3, -8, 7, 1, 5, -5, -3, -7, -4, # -6, 0, 1, 9, -3, -4, 0, -9, -7, -5, -6, 4, -4])}
プロットデータ作成用の関数
def set_data(x, y, name): d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, ) return d
複数回、同じコードを書くのはかなり面倒なので予め関数化して使い回す。ここではプロットデータを作成するための関数を作成。今回は色とかをこだわらずにシンプルな線と点のデータとする。
関数を使用してplolty
を楽に書く方法については以下参照。
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
レイアウト作成用の関数
def set_layout(): layout = go.Layout( template=template.plotly_layout(), xaxis=dict(range=(-1, 31),), yaxis=dict(range=(-11, 11),), ) return layout
レイアウトも関数化して楽をする。こちらも特にこだわらずに、自作テンプレートの読み込みと横軸・縦軸の範囲指定のみ。グラフタイトルや軸ラベルもつけていない。つけてもいいけど。
グラフ保存用の関数
def save(fig, save_name, config): 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")
グラフ保存用の関数も作成。保存形式はなんでもいいけど、htmlとpngにしている。また、保存時にもconfig
を設定することで、保存後のhtmlファイルでもconfig
を使用することができる。
plotly
のconfig
とは、グラフ上部にあるツールバーのようなもの。詳しくは以下参照。
-
-
【plotly&config】グラフのツールバーを編集する
続きを見る
塗りつぶし対象のグラフ
まずは塗りつぶしをしたいデータをグラフ化。今回は乱数を使用したランダム値なのでギザギザなデータとなっている。基本的には上で書いた関数を使用してグラフ化するのみ。簡単。
fig.show()
のタイミングでもconfig
を使用することで、上のグラフのようにツールバーに多くのツールが表示されるようになる。好みで減らすこともできる。
def graph(save_name): plot = [] for name, y in data.items(): d = set_data(x=x, y=y, name=name) plot.append(d) # レイアウトの作成 layout = set_layout() # グラフ描画 fig = go.Figure(data=plot, layout=layout) # グラフの描画 config = template.plotly_config() fig.show(config=config) # グラフ保存 save(fig=fig, save_name=save_name, config=config) graph(save_name='original')
垂直・水平塗りつぶしの関数
# 垂直塗りつぶしの設定 def vrect(x0, x1, fillcolor='green', opacity=1, width=0, color=None, layer='below'): dct = dict( x0=x0, x1=x1, # 塗りつぶしの開始位置と終了位置 fillcolor=fillcolor, opacity=opacity, # 塗りつぶしの色と透明度 line=dict(width=width, color=color,), # 塗りつぶしの枠線の太さと色 layer=layer, # プロット点の上もしくは下で塗りつぶし ) return dct
ということで、本題の塗りつぶしについて。plotly
の塗りつぶしは基本は絶対的なもので、グラフを移動させると塗りつぶしも移動する。通常のプロット点のような感じ。
一応、shapes
を使って、四角を相対的な位置として配置することで塗りつぶしをすることもできるが、plotly
のバージョン4.12からはもっと簡単に塗りつぶしが描けるようになる。それがvrect
。
上の関数はあくまでも引数をまとめたもので、後にこれを展開して使用する。引数としてはかなりシンプルで、よく使いそうなものは以下。
x0
,x1
: 塗りつぶしを開始する横軸の値、終了する横軸の値fillcolor
: 塗りつぶしの色opacity
: 塗りつぶしと枠線の透明度line
: 塗りつぶしの枠線の設定。今までのプロットと同じ指定の仕方layar
: プロット点の上に塗りつぶしを置くか、下に置くか
この塗りつぶしはプロット領域の一番下から一番上までを塗りつぶすこと前提なので、y
の値は指定しなくていい。勝手に上下ピッタリに塗りつぶしてくれる。
逆に水平に塗りつぶししたいときは、hrect
を使用する。
def hrect(y0, y1, fillcolor='green', opacity=1, width=0, color=None, layer='below'): dct = dict( y0=y0, y1=y1, # 塗りつぶしの開始位置と終了位置 fillcolor=fillcolor, opacity=opacity, # 塗りつぶしの色と透明度 line=dict(width=width, color=color,), # 塗りつぶしの枠線の太さと色 layer=layer, # プロット点の上もしくは下で塗りつぶし ) return dct
こちらもvrect
と同じように、あくまでも引数を書いてあるだけ。後ほどグラフに要素として追加する際に展開して使用する。
垂直・水平の塗りつぶしのグラフ
ということで、目的の垂直・水平に塗りつぶすグラフを作成。普段通りのプロットに図形として塗りつぶしを追加するだけだからかなり簡単。
垂直に塗りつぶすグラフ
関数としては塗りつぶしたい領域を可変長引数args
で複数使用可能とした。今回は枠線あり+プロットの下と枠線なし+プロットの上に塗りつぶしをする2種類を使用することに。
塗りつぶしの色や透明度はそれぞれで合わせたので、対照実験的な感じで見ることができるだろう。なお、塗りつぶしは図形として入れるのでマウスオーバーは効かない。
# 垂直の塗りつぶし def fill_vertical(*args, save_name): # プロットデータの作成 plot = [] for name, y in data.items(): d = set_data(x=x, y=y, name=name) plot.append(d) # レイアウトの作成 layout = set_layout() # グラフ描画 fig = go.Figure(data=plot, layout=layout) # 垂直の塗りつぶしを追加 for kwarg in args: fill = vrect(**kwarg) # args内の各dictを展開してvrect関数の引数に fig.add_vrect(**fill) # vrect関数の返り値を展開して塗りつぶしに # グラフの描画 config = template.plotly_config() fig.show(config=config) # グラフの保存 save(fig=fig, save_name=save_name, config=config) fill_vertical( dict(x0=1, x1=8, opacity=0.5, width=5, color='blue', layer='below',), dict(x0=16, x1=25, opacity=0.5, width=0, color=None, layer='above',), save_name='vertical_fill', )
塗りつぶし用に作成した引数のdict
をargs
に使用して、args
をfor
で回す。そうすると引数にしたdict
が1つずつ出力されるので、これをvrect
で展開して塗りつぶしの設定を行う。
あとはvrect
を再度展開してfig
に追加したらいい。わざわざ2回展開しなくても工夫すれば1回の展開で塗りつぶしを作成。個人的には分けた方が問題を切り分けやすいと判断。
fig.add_vrect
はグラフ上に垂直な塗りつぶしを追加するという意味。プロット点やレイアウトのようにfig = go.Figure
で指定することができないっぽいので、仕方なくこのように指定。
水平に塗りつぶすグラフ
水平に塗りつぶすグラフの同じように作成可能。塗りつぶしに使用する引数x0
, x1
がy0
, y1
に変更になって、値を調節するだけ。塗りつぶしに使用する関数はvrect
からhrect
に変更。
# 水平の塗りつぶし def fill_horizontal(*args, save_name): # プロットデータの作成 plot = [] for name, y in data.items(): d = set_data(x=x, y=y, name=name) plot.append(d) # レイアウトの作成 layout = set_layout() # グラフ描画 fig = go.Figure(data=plot, layout=layout) # 垂直の塗りつぶしを追加 for kwarg in args: fill = hrect(**kwarg) # 使用する関数はvrectからhrectに変更 fig.add_hrect(**fill) # 塗りつぶしの図形もhrectに # グラフの描画 config = template.plotly_config() fig.show(config=config) # グラフの保存 save(fig=fig, save_name=save_name, config=config) fill_horizontal( dict(y0=1, y1=8, opacity=0.5, width=5, color='blue', layer='below',), dict(y0=-6, y1=-1, opacity=0.5, width=0, color='blue', layer='above',), save_name='horizontal_fill', )
条件に合う領域だけを塗りつぶすための関数
これまでは、ユーザー側が塗りつぶしたい部分を指定して塗りつぶしを行なってきた。しかし、あるじょうけんにあてはまる部分だけを塗りつぶしたい場合はどうすればいいだろうか。
ということで、ここではある条件下で自動で塗りつぶしの位置を判定・塗りつぶしをする関数を作成する。
条件に合う領域を判定
# データフレームに変換 df = pd.DataFrame(data) print(df) # data0 data1 data2 # 0 2 -5 -2 # 1 5 1 5 # 2 -10 2 3 # 3 -7 -2 -2 # 4 -7 -1 1 # 5 -3 1 8 # 6 -1 -5 1 # 7 9 5 -2 # 8 8 -10 -3 # 9 -6 6 -8 # 10 -4 -9 7 # 11 2 2 1 # 12 -9 -3 5 # 13 -4 3 -5 # 14 -3 -4 -3 # 15 4 8 -7 # 16 7 -5 -4 # 17 -5 8 -6 # 18 3 1 0 # 19 -2 0 1 # 20 -1 4 9 # 21 9 8 -3 # 22 6 -6 -4 # 23 9 -1 0 # 24 -5 7 -9 # 25 5 -10 -7 # 26 5 3 -5 # 27 -10 -1 -6 # 28 8 -1 4 # 29 -7 -3 -4
今回は複数データを使用するということで、予めpandas
のデータフレームに変換しておく。このデータフレームを使うことで、簡単に条件に合うかどうかの判定をすることができる。
条件に合う部分を判定するために以下のめメソッドを使用すると簡単。
gt
: 超過ge
: 以上lt
: 未満le
: 以下eq
: 等しいne
: 等しくない
具体的な使い方は以下。pandas
のシリーズに上のメソッドと数値を入れることで抽出することができる。
# 条件に合うようにデータを制限 def limit_data(conditon, value, header='data0'): if conditon == 'gt': # より大きい(<) series_limit = df[header].gt(value) elif conditon == 'ge': # 以上(≤) series_limit = df[header].ge(value) elif conditon == 'lt': # より小さい(>) series_limit = df[header].lt(value) elif conditon == 'le': # 以下(≥) series_limit = df[header].le(value) elif conditon == 'eq': # 等しい(=) series_limit = df[header].eq(value) elif conditon == 'ne': # 等しくない(≒) series_limit = df[header].ne(value) else: # 上記以外 raise ValueError('conditionにはgt, ge, lt, le, eq, neのいずれか') return series_limit
具体的な使い方は以下。ここではdata0
のデータのうち、5を超過する値がTrue
になる。
print(df['data0'].gt(5)) # 0 False # 1 False # 2 False # 3 False # 4 False # 5 False # 6 False # 7 True # 8 True # 9 False # 10 False # 11 False # 12 False # 13 False # 14 False # 15 False # 16 True # 17 False # 18 False # 19 False # 20 False # 21 True # 22 True # 23 True # 24 False # 25 False # 26 False # 27 False # 28 True # 29 False # Name: data0, dtype: bool
条件に合う連続したデータ点の始まりと終わりを取得
# 連続して条件に合うデータの最初と最後のインデックスを、各領域ごとにまとめて取得 def get_sequence(series): # 連続して条件に合うデータの最初と最後のインデックスを入れる sequences = [[]] # 連続の初めか終わりかを判断する用の変数 # 0: 条件に当てはまるインデックスの開始、1: 条件に当てはまるインデックスの終了の合図 flag = 0 # 真偽値の差分を取るとTrueからFalse, FalseからTrueの変わり目だけTrueに diff = series.diff() # 初めの値からTrueなら差分の0番目のNaNをTrueにする if series[0]: diff[0] = True for num, i in enumerate(diff): # 差分がTrueからFalseもしくはFalseからTrueの変わり目の時 if i and ~np.isnan(i): if flag: # flag=1(条件に当てはまるインデックスが終わる) # flag=1インデックスの終了なので、条件に当てはまるのは1つ前 sequences[-1].append(num - 1) flag = 0 # 連続数状態解除 sequences.append([]) else: # flag=0(条件に当てはまるインデックスが始まる) sequences[-1].append(num) flag = 1 # 連続数状態に # 連続数の状態で最後のデータに来た時は強制的に最後のデータで閉じる if flag: sequences[-1].append(num) # 空のlistを削除 sequences = list(filter(lambda x: x, sequences)) return sequences
add_vrect
, add_hrect
はそれぞれ開始位置と終了位置を指定する必要がある。しかし、.gt
などで条件を適用した場合は開始・終了位置だけではなくその途中の値もTrue
になる。
ということで、get_sequence
関数では各True
の塊の開始インデックスと終了インデックスを、塊ごとにlistに入れる。最終的な出力のイメージは以下。
[[7, 8], [16, 16], [21, 23], [28, 28]]
この場合だと、初めは7番目から8番目が条件に当てはまる。次は16番目から16番目、すなわち16番目のデータのみ条件に当てはまり、その次は21番目から23番目のデータ、そして最後が28番目のデータ。
get_sequence
関数の処理手順は以下。
- 最終結果(上の
[[7, 8]
で始まる配列)になる配列sequence
を定義 - 条件に当てはまる領域の開始位置か終了位置かを判定する変数
flag
を定義 - 配列の差分を取る
- 同じ真偽値の差分なら
False
に、異なる真偽値の差分ならTrue
に
- 同じ真偽値の差分なら
- 0番目が条件に当てはまっていたら、差分の0番目を
True
に- 差分の0番目はNaNとなる
- これは0番目と1つ前のデータ(存在しない)の差分だから
- 差分(真偽値)を
for
で回し、True
の時のみ次の処理を行うTrue
ということは条件に当てはまる・らないの切り替えの時- この切り替えが当てはまるの開始か終了かを判定するのが
flag
- 条件に当てはまる開始インデックスの時は
flag=0
なのでelse
へ - 開始位置のインデックスを変数
sequence
格納し、flag=1
に変更 - 次に
if i
に入るのは条件の当てはまりが終わる時 - その時は
flag=1
なので、if flag
に入る if flag
に入るループの1つ前が条件に当てはまるループの最後- その時のループ
num
から1引いたnum - 1
をsequence
に格納 - 上記を繰り返す
- もし、条件に当てはまる状態で差分のループが終われば、条件に当てはまる状態で閉める
- 最後のインデックス
num
をsequence
の-1
番目に追加すればいい
- 最後のインデックス
- 最後の
num
でピッタリ終わった場合は空のlist
が残るので、最後に削除- 空の
list
はif
がFalse
になるので、filter
関数で弾ける
- 空の
という具合。かなりややこしいので、各人で色々と試してみてほしい。多分、エラーは起きないと思うが、エラーが起きたら申し訳ない。
試しでgt
とdiff
を使ったコードを出力してみる。確かに差分の初めはNaN
になるしgt
の条件の切り替え部分では差分がTrue
になる。
limited = df['data0'].gt(5) limited_zip = zip(limited, limited.diff()) print("num, original, diff") for num, (original, diff) in enumerate(limited_zip): print(f"{num}, {original}, {diff}") # num, original, diff # 0, False, nan # 1, False, False # 2, False, False # 3, False, False # 4, False, False # 5, False, False # 6, False, False # 7, True, True # 8, True, False # 9, False, True # 10, False, False # 11, False, False # 12, False, False # 13, False, False # 14, False, False # 15, False, False # 16, True, True # 17, False, True # 18, False, False # 19, False, False # 20, False, False # 21, True, True # 22, True, False # 23, True, False # 24, False, True # 25, False, False # 26, False, False # 27, False, False # 28, True, True # 29, False, True
条件に合う領域だけを塗りつぶし
# 条件に合うデータ位置のみ塗りつぶしをしたグラフを作成 def select_fill(conditon, value, save_name, header='data0'): # 条件に合うデータをTrueに、合わないデータをFalseにする series_limit = limit_data(conditon=conditon, value=value, header=header,) # 連続して条件に合うデータの最初と最後のインデックスをlistに sequences = get_sequence(series_limit) # 塗りつぶすデータのインデックスから塗りつぶしの設定を作成 ans = [] for x0, x1 in sequences: fill = dict( x0=x0, x1=x1, opacity=0.5, width=5, color='blue', layer='below', ) ans.append(fill) # 条件に合う領域だけ垂直塗りつぶし fill_vertical(*ans, save_name=save_name,)
ということで、最後にこれまでの関数を組み合わせ、条件に当てはまる領域のみを垂直に塗りつぶしできる関数を作成する。コードは上のもので、予め関数化しているからかなり簡単に書ける。
data0
の中で5
を超過する範囲だけ塗りつぶし
まずはdata0
の中で5
を超過する範囲だけを塗りつぶす。条件に合うデータが1点しかなかった場合は面ではなく線として塗りつぶされる。
# data0で5より大きい値を持つ領域だけ塗りつぶし select_fill(conditon='gt', value=5, header='data0', save_name='gt0_5')
data0
の中で5
以上の範囲だけ塗りつぶし
条件を超過から以上に変更したので、data0
の値が5
のデータも塗りつぶされる。
# data1で-1未満の値を持つ領域だけ塗りつぶし select_fill(conditon='ge', value=5, header='data0', save_name='ge0_5')
data1
の中で-1
を超過する範囲だけ塗りつぶし
お次はdata1
の中で-1
を超過する領域のみを塗りつぶすというもの。-1
より大きいのでかなりあるのではと思いきや、以外にもギザギザしすぎていて1点ものが多数。
# data1で-1未満の値を持つ領域だけ塗りつぶし select_fill(conditon='gt', value=-1, header='data1', save_name='gt1_-1')
data1
で3
未満の領域だけ塗りつぶし
最後はdata1
で3
未満の値を持つ領域だけ塗りつぶし。逆に3未満とすると結構小さい値が多いから当てはまる領域が多い印象。ランダム値だけど、これだけデータ数が少ないと偏りが目立ちやすいんだろう。
発展途上なplotly
今回はplotly
で垂直・水平な塗りつぶしをする方法について解説した。塗りつぶし自体は簡単にできるが、条件に合わせて塗りつぶす際には結構面倒だった。まあ、もしかしたら簡単にできるかもしれないが。
add_vrect
, add_hrect
はバージョン4.12から登場した機能であるが、このバージョンのリリースは2020年でかなり最近のアップデートでできるようになった。
matplotlib
では当たり前にできることだが、plotly
では最近までできなかった。このように、もっと便利になることがこれから増えていくのが楽しみだ。
関連記事
-
-
【plt vs plotly】matplotlibとgoでグラフの比較
続きを見る
-
-
【plotly&バブルチャート】plotlyで各国の収入と平均寿命をバブルチャートで描く
続きを見る
-
-
【plotly&orca】plotlyで静止画保存(orca)
続きを見る
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る