【Streamlit】マウスオーバーで画面更新する

Streamlit は標準でマウスオーバーによるページ更新に対応していないですが、streamlit.components.v1.html で JavaScript を埋め込むだけで対応できました (参考文献 [1] を参考にしました)。

### 経緯

Streamlit でルービックキューブをつくったのですが、各ボタンによってどこのブロックがどちら向きに動くのかわかりにくかったので、マウスオーバーでガイドを表示したくなりました。そこで、以下のようにマウスオーバーでピンク色のガイドを表示するようにしました。

![rubikscube_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177688/25860d78-0258-606a-5e4d-e21c6975c812.gif)

上の Streamlit アプリケーションのスクリプトは以下にあります → 削除しました (1/4)。

※ 面倒になったので「上面からみて全体を90度回転」と「上面を90度回転」のボタンにしかガイドは出ません。

### 説明

画面更新に関係する部分だけ抜粋すると以下です。ボタンのマウスオーバー/マウスアウトでダミーボタンを押しています。

```py
import streamlit as st
import streamlit.components.v1 as components

# 略

# ルービックキューブは session_state で管理
if 'rc' not in st.session_state:
    st.session_state.rc = RubiksCube()

col1, col2 = st.columns(2)
with col1:
    # ルービックキューブの描画
    fig, ax = plt.subplots(figsize=(4, 4))
    ax_add_patch_(ax, st.session_state.rc.state)  # みえている3面の描画
    ax_add_patch_help_(ax, st.session_state.rc.state, st.session_state.rc.help)  # ガイドの描画
    st.pyplot(fig)
with col2:
    # ボタン (一部省略)
    st.button('上面からみて全体を90度回転', on_click=st.session_state.rc.rotate_, args=('rt', ))
    st.button('上面を90度回転', on_click=st.session_state.rc.rotate_, args=('rts', ))
    # ダミーボタン
    st.button('help_上面からみて全体を90度回転', on_click=st.session_state.rc.help_, args=('rt', ))
    st.button('help_上面を90度回転', on_click=st.session_state.rc.help_, args=('rts', ))
    st.button('help_cancel', on_click=st.session_state.rc.help_cancel)

js = """
<script>
const doc = window.parent.document;
buttons = Array.from(doc.querySelectorAll('button[kind=secondary]'));
const btn_rt = buttons.find(el => el.innerText === '上面からみて全体を90度回転');
const btn_rts = buttons.find(el => el.innerText === '上面を90度回転');
const btn_help_rt = buttons.find(el => el.innerText === 'help_上面からみて全体を90度回転');
const btn_help_rts = buttons.find(el => el.innerText === 'help_上面を90度回転');
const btn_help_cancel = buttons.find(el => el.innerText === 'help_cancel');
/* ダミーボタンを隠す */
btn_help_rt.style.display = 'none';
btn_help_rts.style.display = 'none';
btn_help_cancel.style.display = 'none';
/* mouseover, mouseout イベント */
btn_rt.addEventListener('mouseover', function(event) { btn_help_rt.click(); });
btn_rts.addEventListener('mouseover', function(event) { btn_help_rts.click(); });
btn_rt.addEventListener('mouseout', function(event) { btn_help_cancel.click(); });
btn_rts.addEventListener('mouseout', function(event) { btn_help_cancel.click(); });
</script>
"""
components.html(js)
```


- ルービックキューブ (の各面の状態をもつクラス) は画面更新でリセットされてはならないので session_state で管理しています。
- 左列 (col1) にはルービックキューブを描画します (session_state で管理されているルービックキューブを参照して)。
  - ルービックキューブにメンバ変数 help がたっているとき、その値に応じてガイドを重ね描きするようにしています。
- 右列 (col2) にはルービックキューブを回転するボタンを設置します。ここで、マウスオーバー/マウスアウトで発動させたい変化を発動させるダミーボタンも設置しておきます。
  - マウスオーバーでメンバ変数 help をたて、マウスアウトでクリアするようにしています。
- 最後に JavaScript で以下を記述します。
  - ダミーボタンは画面に表示したくないので隠します。
  - マウスオーバー/マウスアウトを仕込みたいボタンに対して、マウスオーバー/マウスアウトを待ち受けてダミーボタンを押すよう記述します。

### 参考文献

1. [【streamlit】htmlを埋め込んでkey-bindを対応してみた - こすたろーんエンジニアの試行錯誤部屋](https://technoxs-stacker.hatenablog.com/entry/2022/10/23/012054)： Streamlit で addEventListener をする方法を全面的に参考にしました。
1. [st.button<!-- --> - Streamlit Docs](https://docs.streamlit.io/library/api-reference/widgets/st.button)： st.button の type (button 要素の kind 属性になる) はデフォルトで secondary になるようです。
1. [Components API<!-- --> - Streamlit Docs](https://docs.streamlit.io/library/components/components-api)： 一応 st.components.v1.html の公式ドキュメントです。