Streamlitで日付やカレンダーを扱う

[Streamlit](https://streamlit.io/)はデータサイエンティストやデータエンジニア向けのPythonライブラリです。HTMLやCSSの知識がなくてもシンプルな記述でWebアプリケーションを作成することができます。

https://streamlit.io/  

## 標準コンポーネントで日付を入力する：date_input()

日付の入力で使えるシンプルなコンポーネントは```date_input```です。

https://docs.streamlit.io/library/api-reference/widgets/st.date_input  

```py:streamlit_app.py
import datetime
import streamlit as st

d = st.date_input('誕生日を入力してください。', datetime.date(1998, 2, 16))
st.write('私の誕生日は：', d)
```

デフォルトだと日付の選択範囲が±10年に設定されているので、必要に応じて```min_value```と```max_value```を設定します。  

```py:streamlit_app.py
min_date = datetime.date(1900, 1, 1)
max_date = datetime.date(2100, 12, 31)
d = st.date_input('誕生日を入力してください。', datetime.date(1998, 2, 16), min_value=min_date, max_value=max_date)
```

![input1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/fc5a68f1-2f5f-8bad-4f88-7daffed106bc.png)

なお、うるう年にも対応しています。  

## 標準コンポーネントで日付を入力する：slider()

日付の入力で使えるもう一つのコンポーネントは```slider```です。  
```value```に日付のタプルを渡してあげると日付範囲を入力させることができます。  

https://docs.streamlit.io/library/api-reference/widgets/st.slider  

```py:streamlit_app.py
import datetime
import streamlit as st

min_date = datetime.date(1900, 1, 1)
max_date = datetime.date(2100, 12, 31)
result = st.slider('調査期間を指定してください。', value=(min_date, max_date), format='YYYY-MM-DD (ddd)', min_value=min_date, max_value=max_date)
st.write('開始日は：', result[0])
st.write('終了日は：', result[1])
```

![input2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/9fe3ff5e-e710-db34-ae4b-33f16135d3f4.png)

```format```がカスタマイズ可能なので、例えば```ddd```を指定してあげれば曜日を表示することができます。  

## 3rd-Partyコンポーネント streamlit-calendar でカレンダーを表示する

カレンダーらしいカレンダーを表示するには、3rd-Partyコンポーネントである```streamlit-calendar```を使うことになりそう。  
[FullCalendar](https://fullcalendar.io/)というオープンソースのJavaScriptライブラリを元にしているそうです。  

https://github.com/im-perativa/streamlit-calendar  

### streamlit-calendar：導入手順

3rd-Partyコンポーネントなので追加インストールが必要になります。  

```sh
pip install streamlit-calendar
```

### streamlit-calendar：最小の使い方

ほぼ設定なしでも一応動作して、空のカレンダーが表示されます。あまり意味はないですね。  

```py:streamlit_app.py
import streamlit as st
import streamlit_calendar as st_calendar

st_calendar.calendar()
```

![st_calendar1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/08d07c8f-0cc9-41a8-14e0-585c4b22bcea.png)

### streamlit-calendar：イベントを表示する

イベント(予定)を表示するには、イベント一覧をdictの配列で定義して、```events```に渡す必要があります。  
イベントの定義に使えるプロパティはFullCalendarのドキュメントを参照するしかなさそう。  

https://fullcalendar.io/docs/event-object  

```py:streamlit_app.py
# イベントを定義
event1 = {
    'id': '1', # イベントを識別するためのID。重複不可
    'title': 'Data Cloud World Tour', # イベント名
    'start': '2023-09-08T09:00:00', # 時間帯も指定する
    'end': '2023-09-08T20:00:00', # 時間帯も指定する
}
event2 = {
    'id': '2',
    'title': '月〆',
    'start': '2023-08-31', # 日付のみ指定するとallDayがTrueになる。終日イベントとして表示される
}
event3 = {
    'id': '4',
    'editable': True, # ドラッグで開始日時や終了日時を変更できるようにする
    'groupId': 'Streamlit勉強会', # 同じgroupIdのイベントは同じ変更が適用される
    'title': 'Streamlit勉強会',
    'start': '2023-09-03T09:00:00',
    'end': '2023-09-03T11:00:00',
}
event4 = {
    'id': '4',
    'editable': True,
    'groupId': 'Streamlit勉強会',
    'title': 'Streamlit勉強会',
    'start': '2023-09-10T09:00:00',
    'end': '2023-09-10T11:00:00',
}
# calendarにはイベント一覧を配列にして渡す
event_list = [event1, event2, event3, event4]

# options = {
#     'initialView': 'timeGridWeek',
# }

# イベントを表示するカレンダーを作成
cal = st_calendar.calendar(events=event_list)
st.write(cal)
```

![event1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/f76b1b0e-5319-e83c-c7a4-8eb4f67137b5.png)

カレンダー上でイベント名の直前に表示される**9a**などは開始時間を略記してくれているようです。  
9:00開始なら9AM→9a、20:00開始なら8pm→8p、という感じですね。  

### streamlit-calendar：ビューを変更してみる

色々なビューが使えます。  
FullCalendarのドキュメントの```Views```にデモサイト付きで紹介されています。  

https://fullcalendar.io/docs  

```initialView```というパラメータを設定したdictを```options```に渡してあげれば初期状態で表示されるビューを設定することができます。  

```py:streamlit_app.py
# オプションを指定
options = {
    'initialView': 'dayGridMonth'
}

# イベントを表示するカレンダーを作成
cal = st_calendar.calendar(events=event_list, options=options)
```

いくつか触ってみました。  

**dayGridMonth**：デフォルト。
![view1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/37a231ff-9c62-fedc-6642-eee174110302.png)

**timeGridWeek**：日々の予定を確認するのによさそう。
![view2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/19e248e4-05bd-103a-9da9-cd63e057a99b.png)

**listWeek**：タスクがたくさんある場合はこれかな。
![view3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/046ca5f7-4903-ab89-f001-d7226fce9322.png)

**multiMonthYear**：デフォルトのものに近いですがスクロールすると他の月まで見ることができます。
![view4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/074d6362-46ce-b47a-568b-9ef430cae845.png)

**resourceTimeline**：リソース(会議室や物品など)の管理をするならこれがよさそう。
![view5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/1c6ab2da-eb72-17dd-7498-ecba536a3517.png)

```resourceTimeLine```を使う場合はいくつか追加の情報を指定する必要があります。  
```options```に渡す設定でリソースIDを定義しておき、それぞれのイベントにどのリソースIDと対応しているのかを定義します。  

```py:streamlit_app.py
# オプションを指定。会議室の予約状況をイメージしたもの
options = {
    'initialView': 'resourceTimeline',
    'resourceGroupField': '会議室',
    'resources': [
        {'id': 'A', '会議室': '新宿オフィス', 'title': '8人会議室A [飲食不可]'},
        {'id': 'B', '会議室': '新宿オフィス', 'title': '8人会議室B [飲食不可]'},
        {'id': 'C', '会議室': '新宿オフィス', 'title': '8人会議室C [飲食不可]'},
        {'id': 'D', '会議室': '新宿オフィス', 'title': '個室A [北エリア]'},
        {'id': 'E', '会議室': '新宿オフィス', 'title': '個室B [北エリア]'},
        {'id': 'F', '会議室': '新宿オフィス', 'title': '個室C [南エリア]'},
    ],
}

# 会議室と対応させたイベントを追加
event5 = {
    'id': '5',
    'title': '打合せ',
    'start': '2023-09-08T13:00:00',
    'end': '2023-09-08T14:30:00',
    'resourceId': 'A', # 会議室Aのidを指定
}

# イベントを表示するカレンダーを作成
st_calendar.calendar(events=[event5], options=options)
```

### streamlit-calendar：メニューのカスタマイズ

Toolbarと呼ばれているメニューをカスタマイズすることができます。  
例えば、カレンダーの上部に表示されている前の期間/次の期間のボタンなどですね。  
これもFullCalendarのドキュメントを見ながらカスタマイズしていきます。  

https://fullcalendar.io/docs/toolbar  

```py:streamlit_app.py
# Streamlit標準のページ設定。色々表示すると狭いので幅を広げる
st.set_page_config(layout='wide')

# オプションを指定
options = {
    'initialView': 'dayGridMonth',
    # left/center/rightの3つの領域に表示するものはこの例の順番でなくてもいい
    'headerToolbar': {
        # ヘッダーの左側に表示するものを指定
        # 日付を移動するボタンが表示される。'today'を省略してもいい
        'left': 'today prev,next',
        # ヘッダーの中央に表示するものを指定
        # 'title'は表示されている日付などのこと
        'center': 'title',
        # ヘッダーの右側に表示するものを指定
        # ビュー名をカンマ区切りで列挙して指定するとビューを切り替えるためのボタンが表示される
        'right': 'dayGridMonth,timeGridWeek,listWeek',
    },
    'footerToolbar': {
        # ヘッダーと同じものをフッターにも配置できる。配置しない場合は省力する
        # 'center': 'title',
    },
    'titleFormat': {
        # 例えば月の表記を数字を指定できる
        # 年/月/日の順番にするのはlocaleで設定
        'year': 'numeric', 'month': '2-digit', 'day': '2-digit'
    },
    'buttonText': {
        # 各ボタンを日本語化してみる
        'today': '当日',
        'month': '月ごと',
        'week': '週ごと',
        'day': '日ごと',
        'list': 'リスト'
    },
    'locale': 'ja', # 日本語化する
    'firstDay': '1', # 週の最初を月曜日(1)にする。デフォルトは日曜日(0)
}

# イベントを表示するカレンダーを作成
st_calendar.calendar(events=event_list, options=options)
```

![calendar_customize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/3ba1fb77-4d85-ebb3-c37d-87e993923891.png)

設定項目を把握すれば、ある程度は好みの設定にできるんじゃないでしょうか。

### streamlit-calendar：カレンダー上の操作の検出

追記予定。  

### streamlit-calendar：イベントをすべて取得する

```streamlit_calendar.calendar```の返り値から```eventsSet```プロパティを読むことでイベントを取得できます。カレンダー上で操作した後のイベントも取得できます。  
ただし、日付やイベントをクリックしたときには他のプロパティに値が設定されてしまうので、```None```でないことをチェックする必要があります。  

```py:streamlit_app.py
# イベントを表示するカレンダーを作成
calendar_status = st_calendar.calendar(events=event_list, options=options)

# デバッグ表示
with st.expander('デバッグ表示'):
    # eventsSetはカレンダー上でイベントが表示されたときに代入されるプロパティ
    if 'eventsSet' in calendar_status and calendar_status['eventsSet'] is not None:
        # 最新のイベントはeventsSetに代入されている
        events = calendar_status['eventsSet']
        # まずDataFrameに変換する
        import pandas as pd
        df = pd.DataFrame.from_dict(events)
        # 表示する
        st.dataframe(df)
    else:
        st.write('イベントはありません')
```

![listup1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/7b470ce5-7074-2dfb-7ac5-c0df8a14f378.png)

### streamlit-calendar：その他の設定

追記予定。