plotly ボタンとスライダーを使用したグラフの作成

# やりたいこと

- グラフ内にボタンを作成して、ボタンを押すことでプロットの表示/非表示,タイトルなどのレイアウトを操作する
- 表示したプロットに対してスライダーで操作可能にする

<img width="600" alt="button_slider_plotly.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/6861fe02-927a-4833-f2df-4174b8e61b15.gif">

# 環境
- Mac OS
- python 3.8.5
- plotly 4.12.0

# pip

```terminal:
pip install plotly
pip install numpy
```

# ボタンのおさらい

<img width="500" alt="button_plotly.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/d985c86f-57b9-4126-6688-f050711917bf.gif">

```python:
import numpy as np
import plotly.offline as offline
import plotly.graph_objs as go
import plotly

# 線の色取得
colors = plotly.colors.DEFAULT_PLOTLY_COLORS

x = np.linspace(-5, 5, 11)
data = []
for i in range(1, 4):
    trace = go.Scatter(
        x=x,
        y=x ** i,
        name="y{}".format(i),
        mode="lines",
        line=dict(color=colors[i], width=6),
        visible=False,
        showlegend=True
    )

    data.append(trace)
# 初期状態としてdataの0番目を見えるようにする
data[0].visible = True

"""
ボタン作成個所
キーの説明
active: 初期状態で押されているボタンを指定、buttonsキー値のリストインデックスを入力
type: ドロップダウンかボタンか ( "dropdown" | "buttons" ), default="dropdown"
buttons: ボタンの設定
"""
buttons = []
for ii in range(len(data)):
    visible = [False] * len(data)  # len(data)=3だから, visible=[False, False, False]
    visible[ii] = True  # ii番目だけTrueにする, ex) [True, False, False]
    button = dict(label=data[ii].name,
                  method="update",  # レイアウトを更新する為"update"
                  args=[dict(visible=visible),  # Trueにした個所のtraceのみ見える
                        dict(title="button_plotly_{}".format(data[ii].name),  # title更新
                             yaxis=dict(title="yaxis_{}".format(data[ii].name), showspikes=True))])  # y軸ラベル更新
    buttons.append(button)

updatemenus = [dict(active=0, type="buttons", buttons=buttons)]

layout = go.Layout(
    title="button_plotly_y1",
    xaxis=dict(title="xaxis", showspikes=True, domain=[0.05, 1.0]),
    yaxis=dict(title="yaxis_y1", showspikes=True),
    font=dict(size=16),
    updatemenus=updatemenus,
    showlegend=True)

fig = dict(data=data, layout=layout)
offline.plot(fig, auto_open=True, include_plotlyjs="cdn", filename=r"./button_plotly.html")

```

以下がボタンを作成している個所です。
`updatemenus = [dict(active=0, type="buttons", "buttons"=buttons)]`の`"buttons"=buttons`がボタンを押した時の情報（表示/非表示など）を作成している個所です。

`buttons`はリストで、中にはレイアウト情報の辞書(下記では`button`変数)が格納されています。

```python:
"""
ボタン作成個所
キーの説明
active: 初期状態で押されているボタンを指定、buttonsキー値のリストインデックスを入力
type: ドロップダウンかボタンか ( "dropdown" | "buttons" ), default="dropdown"
buttons: ボタンの設定
"""
buttons = []
for ii in range(len(data)):
    visible = [False] * len(data)  # len(data)=3だから, visible=[False, False, False]
    visible[ii] = True  # ii番目だけTrueにする, ex) [True, False, False]
    button = dict(label=data[ii].name,
                  method="update",  # レイアウトを更新する為"update"
                  args=[dict(visible=visible),  # Trueにした個所のtraceのみ見える
                        dict(title="button_plotly_{}".format(data[ii].name),  # title更新
                             yaxis=dict(title="yaxis_{}".format(data[ii].name), showspikes=True))])  # y軸ラベル更新
    buttons.append(button)

updatemenus = [dict(active=0, type="buttons", buttons=buttons)]


```

今回はボタンでプロットの表示/非表示、タイトル、y軸ラベルの切り替えをするように書いています。

```python:
visible=[False] * len(data)
visble[ii] = True
```
はどのプロットを表示するかを設定しています。

例えば、`data=[trace1, trace2, trace3]`で`visible=[True, False, False]`の時、trace1のみ表示されます。

最後に`go.Layout(updatemenus=updatemenus)`でグラフにボタンができます。

詳細
[plotly ボタン付きグラフの作成 - Qiita](https://qiita.com/shu32/items/5469ea4704bd96b4dab7)

# スライダーのおさらい

<img width="500" alt="slider_plotly.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/0db7ee0d-0045-a5ad-3eae-0dcbcf1b8324.gif">


```python:
import numpy as np
import plotly.offline as offline
import plotly.graph_objs as go

# sin, cos波のtraceをを別々に保存
sin_data, cos_data = [], []
x = np.linspace(0, 10, 101)

for step in np.linspace(0, 5, 51):
    y = np.sin(step * x)
    y2 = np.cos(step * x)

    sin_trace = go.Scatter(
        x=x,
        y=y,
        name="sin {:.1f}Hz".format(step),
        line=dict(color="red", width=3),
        visible=False, )

    cos_trace = go.Scatter(
        x=x,
        y=y2,
        name="cos {:.1f}Hz".format(step),
        line=dict(color="blue", width=3),
        visible=False, )

    sin_data.append(sin_trace)
    cos_data.append(cos_trace)

# sin, cos共にデフォルトでindex=10のトレースを表示する
sin_data[10].visible = True
cos_data[10].visible = True

data = sin_data + cos_data

steps = []
"""
sin_data, cos_dataの表示/非表示を別々に設定した後、結合してvisibleキーに追加
例えばスライダーが2の時、
sin_visible = [False, False, True, False,...]
cos_visible = [False, False, True, False,...]
にした後2つを結合することで、sin, cos共にindex=2のtraceを表示できるようにする
"""
for s in range(len(sin_data)):
    # sin_data, cos_data共に一度全て非表示にする
    sin_visible, cos_visible = [False] * len(sin_data), [False] * len(cos_data)
    # それぞれs番目だけ見えるようにする
    sin_visible[s], cos_visible[s] = True, True
    step = dict(method="update",
                args=[{"visible": sin_visible + cos_visible},
                      {"title": "Simple slider step: {}".format(s)}]
                )
    steps.append(step)

sliders = [dict(active=10, currentvalue=dict(prefix="Frequency: "), pad=dict(t=50), steps=steps)]

layout = go.Layout(
    title="Simple slider step: 10",
    xaxis=dict(title="x"),
    yaxis=dict(title="y"),
    font=dict(size=16),
    hovermode='x unified',
    hoverlabel=dict(font_size=16),
    sliders=sliders,
    showlegend=True)

fig = dict(data=data, layout=layout)

offline.plot(fig, include_plotlyjs="cdn", filename="plots.html")

```

下記でスライダーを作成しています

```python:
steps = []
"""
sin_data, cos_dataの表示/非表示を別々に設定した後、結合してvisibleキーに追加
例えばスライダーが2の時、
sin_visible = [False, False, True, False,...]
cos_visible = [False, False, True, False,...]
にした後2つを結合することで、sin, cos共にindex=2のtraceを表示できるようにする
"""
for s in range(len(sin_data)):
    # sin_data, cos_data共に一度全て非表示にする
    sin_visible, cos_visible = [False] * len(sin_data), [False] * len(cos_data)
    # それぞれs番目だけ見えるようにする
    sin_visible[s], cos_visible[s] = True, True
    step = dict(method="update",
                args=[{"visible": sin_visible + cos_visible},
                      {"title": "Simple slider step: {}".format(s)}]
                )
    steps.append(step)

sliders = [dict(active=10, currentvalue=dict(prefix="Frequency: "), pad=dict(t=50), steps=steps)]
```

やっていることはボタンとあまり変わっていません。
`sliders = [dict(active=10, currentvalue=dict(prefix="Frequency: "), pad=dict(t=50), steps=steps)]`の`steps=steps`がスライダーを動かした時の情報（表示/非表示など）を作成している個所です。

`steps`はリストで、中にはレイアウト情報の辞書（上記では`step`変数）が格納されています。

最後に`go.Layout(sliders=sliders)`でグラフにスライダーができます。

詳細
[plotly スライダーを使ったグラフの応用 - Qiita](https://qiita.com/shu32/items/f9ba57babb072363b466)

# サンプルグラフ

<img width="500" alt="button_slider_plotly.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/6861fe02-927a-4833-f2df-4174b8e61b15.gif">


３つのボタンで

- sin, cos両方(ALL)
- sinのみ(SIN)
- cosのみ(COS)

の表示を切り替え後、見えているプロットに対してスライダーで周波数を変更します。

スライダーはALL, SIN, COSの３つを作成しておき、ボタンでレイアウトを更新する際にスライダーも入れ替えます。

```python:
import copy
import numpy as np
import plotly.offline as offline
import plotly.graph_objs as go

# sin, cos波のtraceをを別々に保存
sin_data, cos_data = [], []
x = np.linspace(0, 10, 101)

for step in np.linspace(0, 5, 51):
    y = np.sin(step * x)
    y2 = np.cos(step * x)

    sin_trace = go.Scatter(
        x=x,
        y=y,
        name="sin {:.1f}Hz".format(step),
        line=dict(color="red", width=3),
        visible=False, )

    cos_trace = go.Scatter(
        x=x,
        y=y2,
        name="cos {:.1f}Hz".format(step),
        line=dict(color="blue", width=3),
        visible=False, )

    sin_data.append(sin_trace)
    cos_data.append(cos_trace)

sin_data[10].visible = True
cos_data[10].visible = True

data = sin_data + cos_data

steps = {"ALL": [], "SIN": [], "COS": []}
"""
visible情報のリストは計４つ
sin_visible_false: sin波のvisibleが全てFalse 
cos_visible_false: cos波のvisibleが全てFalse 
sin_visible_true: sin波のindex=sのvisibleがTrue
cos_visible_true: cos波のindex=sのvisibleがTrue

表示したくないものはFalseにすればいいから、
sin, cos両方表示するときのvisible情報: sin_visible_true + cos_visible_true
sinのみ表示するときのvisible情報: sin_visible_true + cos_visible_false
cosのみ表示するときのvisible情報: sin_visible_false + cos_visible_true
"""
for s in range(len(sin_data)):
    # sin_data, cos_data共に一度全て非表示にする
    sin_visible_false, cos_visible_false = [False] * len(sin_data), [False] * len(cos_data)
    # sin_visible_false, cos_visible_falseのコピー作成、コピーのindex=sに対してTrueにする
    sin_visible_true, cos_visible_true = copy.copy(sin_visible_false), copy.copy(cos_visible_false)
    sin_visible_true[s], cos_visible_true[s] = True, True

    step_all = dict(method="update",
                    args=[{"visible": sin_visible_true + cos_visible_true},
                          {"title": "slider_button_ALL step: {}".format(s)}]
                    )
    step_sin = dict(method="update",
                    args=[{"visible": sin_visible_true + cos_visible_false},
                          {"title": "slider_button_SIN step: {}".format(s)}]
                    )
    
    step_cos = dict(method="update",
                    args=[{"visible": sin_visible_false + cos_visible_true},
                          {"title": "slider_button_COS step: {}".format(s)}]
                    )

    steps["ALL"].append(step_all)
    steps["SIN"].append(step_sin)
    steps["COS"].append(step_cos)

sliders = {}
for key, step in steps.items():
    slider = [dict(active=10, currentvalue=dict(prefix="Frequency: "), pad=dict(t=50), steps=step)]
    sliders[key] = slider

buttons = []
for key, slider in sliders.items():
    slider_active = slider[0]["active"]
    slider_visible = slider[0]["steps"][slider_active]["args"][0]["visible"]
    button = dict(label=key,
                  method="update",
                  args=[dict(visible=slider_visible),
                        dict(title="slider_button_{} step: {}".format(key, slider_active),
                             yaxis=dict(title="y {}".format(key)),
                             sliders=slider)])
    buttons.append(button)

updatemenus = [dict(active=0, type="buttons", buttons=buttons)]

layout = go.Layout(
    title="slider_button_ALL step: 10",
    xaxis=dict(title="x", domain=[0.05, 1]),
    yaxis=dict(title="y ALL"),
    font=dict(size=16),
    hovermode='x unified',
    hoverlabel=dict(font_size=16),
    sliders=sliders["ALL"],
    updatemenus=updatemenus,
    showlegend=True)

fig = dict(data=data, layout=layout)

offline.plot(fig, auto_open=True, include_plotlyjs="cdn", filename=r"./slider_button_plotly.html",
             config={'modeBarButtonsToAdd': ['drawline', 'drawopenpath', 'drawclosedpath', 'drawcircle', 'drawrect',
                                             'eraseshape']})

```

スライダーを動かした時のプロット表示を作成しています。

```python:
steps = {"ALL": [], "SIN": [], "COS": []}
"""
visible情報のリストは計４つ
sin_visible_false: sin波のvisibleが全てFalse 
cos_visible_false: cos波のvisibleが全てFalse 
sin_visible_true: sin波のindex=sのvisibleがTrue
cos_visible_true: cos波のindex=sのvisibleがTrue

表示したくないものはFalseにすればいいから、
sin, cos両方表示するときのvisible情報: sin_visible_true + cos_visible_true
sinのみ表示するときのvisible情報: sin_visible_true + cos_visible_false
cosのみ表示するときのvisible情報: sin_visible_false + cos_visible_true
"""
for s in range(len(sin_data)):
    # sin_data, cos_data共に一度全て非表示にする
    sin_visible_false, cos_visible_false = [False] * len(sin_data), [False] * len(cos_data)
    # sin_visible_false, cos_visible_falseのコピー作成、コピーのindex=sに対してTrueにする
    sin_visible_true, cos_visible_true = copy.copy(sin_visible_false), copy.copy(cos_visible_false)
    sin_visible_true[s], cos_visible_true[s] = True, True

    step_all = dict(method="update",
                    args=[{"visible": sin_visible_true + cos_visible_true},
                          {"title": "slider_button_ALL step: {}".format(s)}]
                    )
    step_sin = dict(method="update",
                    args=[{"visible": sin_visible_true + cos_visible_false},
                          {"title": "slider_button_SIN step: {}".format(s)}]
                    )

    step_cos = dict(method="update",
                    args=[{"visible": sin_visible_false + cos_visible_true},
                          {"title": "slider_button_COS step: {}".format(s)}]
                    )

    steps["ALL"].append(step_all)
    steps["SIN"].append(step_sin)
    steps["COS"].append(step_cos)

sliders = {}
for key, step in steps.items():
    slider = [dict(active=10, currentvalue=dict(prefix="Frequency: "), pad=dict(t=50), steps=step)]
    sliders[key] = slider

```
slidersの中身は`{"ALL":ALLの時のスライダー, "SIN":SINの時のスライダー,...}`となっています

```python:
buttons = []
for key, slider in sliders.items():
    slider_active = slider[0]["active"]
    slider_visible = slider[0]["steps"][slider_active]["args"][0]["visible"]
    button = dict(label=key,
                  method="update",
                  args=[dict(visible=slider_visible),
                        dict(title="slider_button_{} step: {}".format(key, slider_active),
                             yaxis=dict(title="y {}".format(key)),
                             sliders=slider)])
    buttons.append(button)

updatemenus = [dict(active=0, type="buttons", buttons=buttons)]

```

ボタンを作成しています。
`slider_visible = slider[0]["steps"][slider_active]["args"][0]["visible"]`はスライダーの初期位置のvisible情報を取得しています。

上のスクリプトの場合スライダーの初期位置を10としているので、ボタンを切り替えた直後の表示もそれに合わせないとスライダーの位置と表示が合わない為です。

```python:
layout = go.Layout(
    title="slider_button_ALL step: 10",
    xaxis=dict(title="x", domain=[0.05, 1]),
    yaxis=dict(title="y ALL"),
    font=dict(size=16),
    hovermode='x unified',
    hoverlabel=dict(font_size=16),
    sliders=sliders["ALL"],
    updatemenus=updatemenus,
    showlegend=True)

fig = dict(data=data, layout=layout)

offline.plot(fig, auto_open=True, include_plotlyjs="cdn", filename=r"./slider_button_plotly.html",
             config={'modeBarButtonsToAdd': ['drawline', 'drawopenpath', 'drawclosedpath', 'drawcircle', 'drawrect',
                                             'eraseshape']})

```

レイアウトを決めて出力しています。
デフォルトはALLを表示するようにしています。

config={...}は右上のお絵かきボタンなどを追加しています。

# 参考
公式
[Custom Buttons | Python | Plotly](https://plotly.com/python/custom-buttons/)
[layout.updatemenus | Python | Plotly](https://plotly.com/python/reference/layout/updatemenus/)
[Plotly Python Graphing Library | Python | Plotly](https://plotly.com/python/)