【地図】Streamlit 1.39からst.pydeck_chartで選択した点を取得できるようになりました

# はじめに
10月にリリースされた Streamlit 1.39.0 から、`st.pydeck_chart` による地図描画で、選択した点を取得できるようになりました。

https://docs.streamlit.io/develop/quick-reference/release-notes/2024#version-1390

https://docs.streamlit.io/develop/api-reference/charts/st.pydeck_chart

今回は、そちらの機能の紹介と、簡単なデモアプリを紹介しようと思います。


# 概要
## `st.pydeck_chart` の紹介
`st.pydeck_chart` は、PyDeck ライブラリを Streamlit アプリ内で使用して、地図や3D可視化を簡単に表示できるコンポーネントです。

基本的には、次のようなソースコードで地図描画が可能になります。
```python
import streamlit as st
import pydeck as pdk

# データを用意
data = [
    {"latitude": 35.6895, "longitude": 139.6917, "place": "東京"},
    {"latitude": 34.6937, "longitude": 135.5023, "place": "大阪"},
    {"latitude": 43.0642, "longitude": 141.3469, "place": "札幌"},
    {"latitude": 35.0116, "longitude": 135.7681, "place": "京都"},
    {"latitude": 33.5904, "longitude": 130.4017, "place": "福岡"},
]

# レイヤーを設定
layer = pdk.Layer(
    "ScatterplotLayer",
    data=data,
    get_position="[longitude, latitude]",
    get_color="[200, 30, 0, 160]",
    get_radius=50000,
)

# 初期表示の設定
view_state = pdk.ViewState(
    latitude=36.2048,
    longitude=138.2529,
    zoom=3,
)

# Pydeckチャートを表示
chart = pdk.Deck(
    layers=[layer],
    initial_view_state=view_state,
    map_style=None,
)

st.pydeck_chart(chart)

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/22ddf806-2ee0-4843-1c8a-d995af926a63.png)



## なぜ嬉しいか？
Streamlit 周りで有名なライブラリとして `folium` がありこちらなら柔軟性も高いです。

しかし、Streamlit 公式でこうした操作が行えるコンポーネントは存在しなかったので、「シンプルに実装したいんだけど、選択した点を取るにはサードパーティを入れないといけないな・・・」といったときに有用となります。

また、Snowflake に統合された Streamlit では地図描画として `st.map` と `st.pydeck_chart` しか使えず、両方とも選択した点の取得ができなかったので、それができるようになるのも嬉しいポイントですね。

:::note warn
ただし、記事公開時点では Streamlit in Snowflake の Streamlit バージョンの最新が 1.35 なので、1.39 の実装までもう少し待つ必要があります。
:::



# `st.pydeck_chart` で選択した点を取得する
`st.pydeck_chart` で選択した点を取得するには、下記のようにレイヤー設定時とコンポーネント呼び出し時にいくつか設定を追加してあげるだけで良いです。

```diff_python
import streamlit as st
import pydeck as pdk

# データを用意
data = [
    {"latitude": 35.6895, "longitude": 139.6917, "place": "東京"},
    {"latitude": 34.6937, "longitude": 135.5023, "place": "大阪"},
    {"latitude": 43.0642, "longitude": 141.3469, "place": "札幌"},
    {"latitude": 35.0116, "longitude": 135.7681, "place": "京都"},
    {"latitude": 33.5904, "longitude": 130.4017, "place": "福岡"},
]

# レイヤーを設定
layer = pdk.Layer(
    "ScatterplotLayer",
    data=data,
    get_position="[longitude, latitude]",
    get_color="[200, 30, 0, 160]",
    get_radius=50000,
+   pickable=True,
+   auto_highlight=True,
+   id="map",
)

# 初期表示の設定
view_state = pdk.ViewState(
    latitude=36.2048,
    longitude=138.2529,
    zoom=3,
)

# Pydeckチャートを表示
chart = pdk.Deck(
    layers=[layer],
    initial_view_state=view_state,
    map_style=None,
)

event = st.pydeck_chart(
    chart,
+   selection_mode="single-object",
+   on_select="rerun",
)

+event.selection
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/3a02ae1b-7ca6-d3a1-5fcc-557f1371968a.png)

上記のアプリは次の Playground から操作できます。
[`st.pydeck_chart` を実行するアプリ](https://streamlit.io/playground?example=llm_chat&code=H4sIAAAAAAAAA21TTWvbMBi--1cY9dJBJuz4I_HAtx3HGHTsEoJRbZFocSxjKynZ2KHxYD0MCqWHMVpGWdjCaAvbbWVjP8ZL0v6Lvbby1SXCsvU-evU874fFujFPhJqKhJJuyIRKUjAUJuF4EFC_U2Bx0FGUVOCDhAm6CwvP69MkZTzyvAeKsqPm2bs8-5UP_-TDk9npePr2WAmIIKqrNhQVxmsUEsFEL6DokWpY2K47VkVFIY9aC1Q3HGw7eg3gOCR-AaHp-fe_N5foTWULiQneRm2DxMKWVjXWSSajr3cfvgEJUOyo0trkMw2s2Wb1fz5Tx4ZpO_eCOrua_Hy_PSgLa7pubwmqZtf1dRJI6y77vZ3EwJajmRskGja1--WZfTme_LgoSJqyCZf5cJRno7IVJ7fjq8n1RyUkA5pAI6CH-Emx3i0l0Z5PhKBJHHJRwkiGUrTNLV7SbFHhxTxlAnrtosYyooq6iLiJVp4-D3kCblVNq6gGTHh0W1t3SUjAeqlraTAkGjO_Q_ZD6j5PelRCpCe412atdghTrG2wwEVdEgNf-dtNjs6nZ59uL8az0U1-eD3PuM_ogZcKIug87RcA7BW2TH0RuWvYuKqZdUm9TM3VjTquWlVH4q8477rGXPBZeSXy7DDPPhdVzo6KQpf6it8mcGmk4mPwWohBbVO3UX6b8ywiqCcJvVWg7mopXSBJMAdQlac8oqU67dOo4IfrJ2-mVypKmXIpj6Y0pH7RL6_LIRuUsqgV0od8_yXA807ApnRzUUKTXoRWCnh5_h9nWAJWHgQAAA)


## パラメータ解説
- pdk.Layer
    - pickable: その名の通り選択可能にするオプションです
    - auto_highlight: カーソルが当たっている部分にハイライトをするオプションです
    - id: Rerun時に地図描画を一意に固定するIDです（こちらの設定を忘れて、イベント情報更新されず困ったことがありました）
- st.pydeck_chart
    - selection_mode: single-object と multi-object を指定でき、言葉の通り、一つを選択できるモードと複数を選択できるモードになります
    - on_select: ignore か rerun、コールバック関数を指定でき、選択時にアプリの再描画をするかしないかを指定できます（rerun かコールバック関数を指定した際は、先述の id パラメータを指定する必要があることに注意しましょう！）


## おまけ：取得した選択点（辞書型）を Pandas データフレームに変換する
下記のコードを追加することで、辞書型で返却された選択点を Pandas データフレームに変換することができます。最後の "map" は、pdk.Layer の id パラメータに指定した値となります。

```python
import pandas as pd

# オブジェクト部分を取得
objects = event["selection"]["objects"]["map"]
# pandas DataFrameに変換
df = pd.DataFrame(objects)

st.dataframe(df)
```

上記コードは、`st.pydeck_chart` が `selection_mode="multi-object"` でもそのまま利用できます。


## おまけ2：st.pydeck_chart の色を要素ごとに変更する
本記事とあまり関係ないですが、困ったのでメモ。

データフレームに`"color"` 列を用意しておき、例えば次のように編集しておく。
```python
df["color"] = [[0, 255, 0]] * len(df)  # デフォルトカラー（緑色）
for idx, row in df_fault.iterrows():
    if row["status"] == "なにか１":
        df.at[idx, "color"] = [255, 0, 0]  # 赤色
    elif row["status"] == "なにか２":
        df.at[idx, "color"] = [255, 255, 0]  # 黄色
```

さらに、`pdk.Layer` の `get_color` オプションにその `"color"` 列を指定する。
```python
layer = pdk.Layer(
    "ScatterplotLayer",
    data=df,
    get_position="[longitude, latitude]",
    get_color="color",
    get_radius=50000,
    pickable=True,
    auto_highlight=True,
    id="map",
)
```

これで点ごとの色を変えられる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/962d7f50-98f1-65f8-84f8-85b78ce2a7ac.png)