Streamlitでのページ遷移とポップアップボタンの実装

１次、２次、、と深く潜るタイプのページ遷移のテクニックが見つからなかったので自分なりに実装してみました。

# ページ遷移

Streamlitを使っていると、

>![Untitled Diagram.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2459843/34e5a6e6-b38b-1d96-c3d9-83758a16040c.jpeg)

この一連のページ遷移を実装したい場面が出てきます。
これを実現します。

# ポップアップボタン

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="xxXzoQZ" data-user="gudapy" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/gudapy/pen/xxXzoQZ">
  Untitled</a> by py guda (<a href="https://codepen.io/gudapy">@gudapy</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

一度切りの押しボタンです。一定時間後に消えるポップアップを表示したり、処理のトリガーとなります。ポップアップメッセージ自体は`st.spinner`を使うと出来ます。しかしページ遷移との組み合わせにすると、ページ遷移後にボタン操作などを行った場合、『ページの状態がクリアされてしまう』『ポップアップメッセージが消えない』、といった問題がいくつか出てきます。ページ遷移後の状態を保持しつつポップアップ（ボタン）を実装する、これを実現します。


# 肝となるStreamlit API
`st.session_state()`
`st.empty()`
`st.spinner()`


# ノートブック
>Streamlitでのページ遷移とポップアップボタン

https://colab.research.google.com/drive/1VBkwkd-lE9KVhBovzfUyLIkDMXaWIvMj


# 動作
ページ移動からの、ページ遷移＋ポップアップの一連の動作です。
逐次処理順に番号`[n]`を振りました。
>GIF動画です

![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2459843/0d0af590-c4f4-a8e8-2bf8-36d02da930e8.gif)

# 使い方
`メニューバー`から、

>`ランタイム`　**>**　`すべてのセルを実行`

https://qiita.com/gudapys/items/5d8a24292e6d5729ca45

そもそも`Google Colaboratory`の使い方は、という方はこちら。

https://qiita.com/gudapys/items/24a04b01438ea7e01678


# ソース
ベースはこれです。

https://qiita.com/gudapys/items/cae2730f093879659d1f

### app.py

```python:
import streamlit as st
import time
```
>`pop()`と`pop_btn()`に`time`が必要です。

```python:
# config
st.set_page_config(
    page_title="Streamlitでのページ遷移とポップアップボタン",
    layout="wide",
    initial_sidebar_state="expanded"
)
```
>ページのタイトルや幅を設定できます。

>>st.set_page_config
https://docs.streamlit.io/library/api-reference/utilities/st.set_page_config

```python:
# init
def init():
    if "init" not in st.session_state:
        st.session_state.init=True
        reset_session()
        count()
        return True
    else:
        return False

# st.sidebar.selectboxの切り替わりを感知
def tab_session():
    if not st.session_state.now_tab==st.session_state.tab:
        reset_session()
    st.session_state.now_tab=st.session_state.tab

def layer_session(layer=0):
    st.session_state.layer=layer

def reset_session():
    st.session_state.now_tab=None
    layer_session()
```
>主に`ページ遷移`処理部分を管理しています。
表示ページの途中でst.sidebar.selectboxが切り替わった場合にlayerをリセットして対応しています。
>>st.session_state
https://docs.streamlit.io/library/api-reference/session-state

```python:
# extends
def ck():
    if "ck" not in st.session_state:
        st.session_state.ck=-1
    st.session_state.ck+=1
    return st.session_state.ck

def count():
    if "count" not in st.session_state:
        st.session_state.count=0
    st.session_state.count+=1
```
>webアプリとしての処理部分です。例えばここに`APIを叩く処理`など好きな処理を追加していきます。

```python:
# UI components
def deco_horizontal(func):
    def wrapper(*args, **kwargs):
        st.write("---")
        func(*args, **kwargs)
        st.write("---")
    return wrapper

@deco_horizontal
def back_btn():
    st.button(f"戻る。[{ck()}]",on_click=reset_session)

def btn(label="ボタン",key=None,onclick=None,done=None):
    if st.button(label,key=key,on_click=onclick) and done:
        done()

def pop_btn(label="pop",key=None, layer=0, onclick=lambda:None, done=None, description=None):
    placeholder=st.empty()
    with placeholder.container():
        if description:
            st.write(description)
        res=st.button(label,key=key,on_click=lambda:[placeholder.empty(),layer_session(layer),onclick()])
    if res:
        if done:
            with placeholder:
                done()
                placeholder.empty()

def pop(msg, done=None, interval=1):
    with st.spinner(msg):
        time.sleep(interval)
    if done:
        done()
```
>ポップアップボタンと必要最低限のUI部品です。
ポップアップ後の処理とポップアップボタンそのものが残らないように`placeholder.empty()`によってクリアしています。
`done`には`一度表示させて消したい`処理を登録します。
`onclick`には`表示に関わりのない`処理を登録します。
`onclick`に`st.write`などを登録すると（逐次処理的に）ページ上部に表示されてしまいます。
>>st.empty
https://docs.streamlit.io/library/api-reference/layout/st.empty

```python:
# contents
def index():
    st.write("ここは **Indexページ** です。")

def sample_content(i):
    st.write(f"ここは **ページ {i}** です。[{ck()}]")
    pop_btn(
        label=f"POPボタン",
        layer=2,
        onclick=lambda:[count(),print(f"ck[{ck()}]")],
        done=lambda:[
            pop(f"POPボタンが押されました[{ck()}]"),
            st.success(f"成功！[{ck()}]"),
            time.sleep(1)
        ]
    )
```
>ページのコンテンツ部分です。
ボタンが押された直後にカウントするよう`onclick`にcount()を登録しました。
ボタンが押された後の表示処理として`done`に逐次処理を登録しています。
`st.columns`や`st.sidebar`によって出力場所を変えることもできます。
>>st.columns
https://docs.streamlit.io/library/api-reference/layout/st.columns
st.sidebar
https://docs.streamlit.io/library/api-reference/layout/st.sidebar

```python:
# body
init()
st.session_state.ck=0
    
st.session_state.tab = st.sidebar.selectbox("選択してください。", ["Index","List"])
tab_session()# TAB切り替えの管理
# delay
time.sleep(0.1)
#
_tab=st.session_state.tab
_layer=st.session_state.layer
if _tab=="Index":
    if _layer==0 or _layer==1:
        layer_session(1)
        index()
elif _tab=="List":
    if _layer==0 or _layer==1:
        layer_session(1)
        sample_content(1)
    elif _layer==2:
        st.info(f"POPによるページ遷移をしました。[{ck()}]")
        sample_content(1)
        back_btn()
```
>各コンテンツには`st.sidebar`でアクセスします。
`st.sidebar`でなくとも、条件分岐で表示させることができれば、どの方法でも良いと思います。
今どこのページにいるかは`st.sesion_state.tab`と`st.session_state.layer`で判別しています。
`st.session_state.tab`は各コンテンツ。
`st.session_state.layer`は深層で分岐することによってコンテンツ内遷移をしています。

>>st.sidebar
https://docs.streamlit.io/library/api-reference/layout/st.sidebar

# おわりに
汎用的に使えて動作もしますが、やり方はこれであっているのかはわかりません。
他にもっと良い方法があるのかもしれません。

# 参考

https://docs.streamlit.io/library/api-reference/session-state

https://docs.streamlit.io/library/api-reference/status/st.spinner

https://docs.streamlit.io/library/api-reference/layout/st.empty

https://www.w3schools.com/howto/howto_css_loader.asp