カテゴリー

go

【plotly&fill】goで領域を塗りつぶし

2021年5月4日

こんな人にオススメ

plotlyで塗りつぶしをしたいけど、referenceに書いてある内容がよくわからん!

しかも塗りつぶしたらあらゆるところが塗られてしまう!助けて!

ということで、今回はplotlyを使用して領域の塗りつぶしを行う。何かしらの範囲を明示したい時などに塗りつぶしは便利だ。なお、以下の記事でpltによるグラフの塗りつぶしも行った。

(9 < x y2)の条件に合った塗りつぶし
【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
スポンサーリンク
スポンサーリンク

運営者のメガネです。TwitterInstagramも運営してます。

自己紹介はこちらから、お問い合わせはこちら。

運営者メガネ

作成したコード全文

下準備

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_templateplotlyの自作テンプレートで以下の記事で解説している。これを使用することで簡単にキレイなグラフレイアウトを作成できる。

【随時更新 備忘録】plotlyのグラフ即席作成コード

続きを見る

使用するデータは、xは単なる0から29までの数値で、yは適当に作成した値とした。また、後から使いやすいようにy1, y2ysとして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' or None: 塗りつぶさない
  • '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. 塗りつぶしの開始を表すデータ(実際に塗りつぶしをするデータ)

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, 5y2で言えば2, 3, 4, 5y1で言えば4, 6, 8, 8に該当する。そして、forの中の18, 192, 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に入れられる。これを使ってplotlyfillをしてみる。

成功するplotlyfill

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 ≥ y1y2 = y1を許容した版。そうするとx = 8, 9が2つの分断された領域をつなぐ役割を果たすので結果的に分断された領域が1つになる。

これはこれで対処する必要がありそう。どうするかは各々が決めたらいいが、ここではこれでひとまず終了とする。なんで同じ色なんやってなったらつながれているということが当たり前だけど驚き。

もっと良い方法ありそうなんだけどな

今回はplotlyを使用した、領域の塗りつぶしに関して紹介した。実際にコードを作成すると思うが、かなり面倒な処理が必要となる。

今回はデータ数が30なので大したことはないが、データ数が増えるとその分、分割された領域が増えるのでそれらに1つ1つ対処しないといけない。

もっといい方法があるのかもしれないが、とりあえず現状はこんな感じ。ダルい。

【plotly&buttons】ボタンを押すとプロット・背景の色が変わる変なグラフ

続きを見る

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

続きを見る

【plotly&mapbox】感染者数と死者数の散布図コロナマップを作成

続きを見る

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

続きを見る

関連コンテンツ

スポンサーリンク

Amazonのお買い物で損したない人へ

1回のチャージ金額通常会員プライム会員
¥90,000〜2.0%2.5%
¥40,000〜1.5%2.0%
¥20,000〜1.0%1.5%
¥5,000〜0.5%1.0%

Amazonギフト券にチャージすることでお得にお買い物できる。通常のAmazon会員なら最大2.0%、プライム会員なら2.5%還元なのでバカにならない。

ゲットしたポイントは通常のAmazonでのお買い物に使えるからお得だ。一度チャージしてしまえば、好きなタイミングでお買いものできる。

なお、有効期限は10年だから安心だ。いつでも気軽にAmazonでお買い物できる。

Amazonチャージはここから出来るで

もっとお得なAmazon Prime会員はこちらから

30日間無料登録

執筆者も便利に使わせてもらってる

スポンサーリンク

  • この記事を書いた人

メガネ

独学でpythonを学び天文学系の大学院を修了。 ガジェット好きでMac×Android使い。色んなスマホやイヤホンを購入したいけどお金がなさすぎて困窮中。 元々、人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂ったために今も人生めちゃくちゃ楽しい。 pythonとガジェットをメインにブログを書いていますので、興味を持たれましたらちょこちょこ訪問してくだされば幸いです🥰。 自己紹介→変わって楽しいの繰り返し

-go
-, , ,