ダッシュボード作成ライブラリPanelのウィジェット紹介 ～入力系編～

# はじめに

前回、Jupyter上にダッシュボードを作成できるPanelというライブラリを紹介しました。
　⇒　[”エコ”にダッシュボードをつくろう　～Panelライブラリの紹介～](https://qiita.com/hisakichi95/items/717b0c78c00a9de9bd5a)
学習コストが少ないというのが特徴のライブラリですが、各ウィジェットのオプション設定は色々調べないと分からないようなものも多数ありました。後学のため整理しておきます。

今回は主にユーザーのアクションを受け取る入力系ウィジェットについてまとめます。

**2021/12/11追記**　出力系のウィジェットまとめました！
[ダッシュボード作成ライブラリPanelのウィジェット紹介 ～出力系編～](https://qiita.com/hisakichi95/items/dc40bc6956a35c99afee)

## 共通
Panelを扱うには、pipなどでpanelをインストールした上で下記のおまじないを冒頭で行なう必要があります。

```python
# ! pip install panel
import panel as pn
# おまじない
pn.extension()
```

## Button

イベントを指定しクリックすると反応が得られるボタン型のウィジェットです。
nameオプションに文字列を渡すとその文字が表示されたボタンを生成できます。

```python
# ボタンウィジェットの生成
pn.widgets.Button(name= "test")
```

![01_Button.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/76e98b85-333e-5bd2-27f4-54ad6321e4e9.jpeg)

#### button_type（色）の指定

button_typeを指定すると、ボタンの色を変更できます。

```python
pn.Column(
    pn.widgets.Button(name="default", button_type="default"),
    pn.widgets.Button(name="primary", button_type="primary"),
    pn.widgets.Button(name="success", button_type="success"),
    pn.widgets.Button(name="warning", button_type="warning"),
    pn.widgets.Button(name="danger", button_type="danger"))
```

![02_button_type.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/d5b0a8dd-d475-5a6d-bb08-b7ebab8c9d92.jpeg)


ボタンの高さやフォント・フォントサイズ・上記以外の色への変更はできなさそうです。
（一応、css_classesを使ってクラスにCSSを指定すればフォントなどは変更できそうですが、他のウィジェットも影響を受けてしまいそう）

#### ボタン押下時のイベント指定
ボタンを押したときの動作を割り当てるには、defでイベント関数を宣言した後、on_clickで引数にイベント関数名を指定するだけです。
以下の例は、ボタンを押すたびにボタンの表示（nameとbutton_type）が変更されるというものです。

```python
# クリック時のイベント規定
def click(event):
    # ボタンのタイプがプライマリーだったら、表示を「押すなよ」にしてボタンタイプをサクセスに。
    if button_e.button_type == "primary":
        button_e.name = "押すなよ"
        button_e.button_type = "success"
    # ボタンのプライマリー以外だったら、表示を「押せよ」にしてボタンタイプをプライマリーに。
    else:
        button_e.name = "押せよ"
        button_e.button_type = "primary"

# ボタンウィジェットの生成
button_e = pn.widgets.Button(name= "押すとイベント実行")
# clickした時のイベントを指定
button_e.on_click(click)

# 描画
button_e
```

![03_Buttonイベント.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/45800cb2-feba-937d-a997-20ad568061f7.gif)


## CheckBox
押すたびにON・OFF切り替わるウィジェットです。

```python
# インスタンス生成
ck = pn.widgets.Checkbox(name= "あなたは神を信じますか")
#表示
ck
```

![04_checkbox.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/c8b13d38-8273-91d7-4117-25e7884766e5.jpeg)

#### ON/OFF状態の取得

チェックボックスの状態はvalueで取得でき、値はTrue/Falseで返されます。

```python
# チェックボックスの値表示
ck.value
```

　↓
True

## Select
押すとプルダウンが表示され、その中から一つ選択肢で選ぶタイプのウィジェットです。
引数optionsに選択肢が列挙されたリスト型を指定します。

```python
select = pn.widgets.Select(options=["春", "夏", "秋", "冬"], value="春")
select
```

![05_select.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/e2336ab4-cb79-39c1-64e3-b25de6f17fe4.jpeg)

#### 初期値の指定

引数のnameでウィジェットと一緒に表示する項目名、valueで初期値を指定可能です。（省略すると一番上のになる）

```python
# 初期値指定なし
s1 = pn.widgets.Select(name="季節：初期値指定なし", options=["春", "夏", "秋", "冬"] )
# 初期値指定あり
s2 = pn.widgets.Select(name="季節：初期値指定あり", options=["春", "夏", "秋", "冬"], value= "秋")

# 表示
pn.Column(s1, s2)
```

![06_select_ini.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/2c3fb95d-1b05-3ce6-ca26-76e1359a4ad6.jpeg)

#### 選択されている要素の取得

選択されている中身を取得するときは、valueメソッドで取得できます。

```python
s3.value
```

　↓
'春'

## RadioBoxGroup
Select同様複数の選択肢から一つの要素を選ばせるウィジェットとしてはRadioBoxGroupもあります。

```python
# インスタンス生成
radio = pn.widgets.RadioBoxGroup(name= "ラジオボックス", options= ["春", "夏", "秋", "冬"])
#表示
radio
```

![07_radiobox.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/cb7250c4-6c2f-4ab3-d776-300bf88d1e05.jpeg)
詳細はSelectと大体同じです。

## MultiSelect

先ほどのSelectに似たウィジェットで、複数の項目を選択できるウィジェットとしてMultiSelectがあります。
Selectはクリックするとプルダウンが表示される感じでしたが、MultiSelectはプルダウンを広げずに選択肢が表示されています。
そして、Ctrキー, Shiftキー, ドラッグ等を活用して項目を複数選択できます。

```python
# インスタンス生成
m_select = pn.widgets.MultiSelect(name= "季節選択（複数）", options= ["春", "夏", "秋", "冬"])
# 表示
m_select
```

![08_multiselect.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/6c876b19-a718-3638-0b56-1640377757ed.jpeg)

#### 表示範囲の指定

全部表示させたくない場合は、sizeで表示する要素数を指定します。

```python
# インスタンス生成
m_select_size3 = pn.widgets.MultiSelect(name= "季節（複数選択、サイズ3）", options= ["春", "夏", "秋", "冬"],
                                        size= 3)
# 表示
m_select_size3
```

![09_multiselect_size.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/c4e45724-9ecf-d231-01ae-d94263faf648.jpeg)

#### 値の取得

値はvalueで取得でき、選択されている要素名がリストで返されます。

```python
m_select.value
```

　↓
['春', '秋']

## CheckBoxGroup

同様に、複数の選択肢から一つを選択させるウィジェットとしてCheckBoxGroupがあります。

```python
# インスタンス生成
checkbox_grp = pn.widgets.CheckBoxGroup(name= "季節（チェックボックスグループ）", options= ["春", "夏", "秋", "冬"])
# 表示
checkbox_grp
```

![10_checkboxgroup.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/355e24be-59de-67cf-7946-52951bfa818b.jpeg)

#### 値の取得

結果がvalueで取得でき、リストで返される点も同様です。

```python
checkbox_grp.value
```

　↓
['秋', '冬']

## MultiChoice

上記２つと比較して少し動作が異なるウィジェットとしてMultiChoiceがあります。
クリックすると選択肢が出てくることはMultiSelectと同様ですが、文字入力をすると選択肢を絞り込むことができます。
Ctrl押さずとも複数項目選択できるので、PC操作に不慣れなユーザーが触れるケースを想定するとMultiSelectより有用かもしれません（表示項目の順番指定できないのが難点ですが）

```python
# インスタンス生成
m_choice = pn.widgets.MultiChoice(name= "季節（マルチチョイス）", options= ["春", "夏", "秋", "冬","春分の日", "ハルジオン"],
                                 height= 200, )
# 表示
m_choice
```

![11_multichoice.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/aceffdb6-d087-e241-467f-44514cb394ea.gif)


heightは指定しておかないと、表示が見づらくなるので、指定しておいた方が良さげです。

#### 選択できる要素数上限の指定

max_itemsで選択できる要素の上限を指定できる点も、MultiSelectやCheckBoxGroupより使いやすいポイントですね。

```python
# インスタンス生成
m_choice_item2 = pn.widgets.MultiChoice(name= "季節（アイテム数上限2）", options= ["春", "夏", "秋", "冬"],
                                         height= 200)
# 選択可能数の上限指定
m_choice_item2.max_items = 2

# 表示
m_choice_item2
```

![12_multichoice_maxitems.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0f5b251f-8da5-eca6-72e7-ee1b2ddc6d59.gif)


## IntSlider, FloatSlider
つまみを動かして値を取得するスライダータイプのウィジェットです。
整数型（IntSlider）と浮動小数点型（FloatSlider）があります。
引数のstartで下限値、endで上限値を指定します。

```python
# 整数型スライダーのウィジェット生成
i_slider = pn.widgets.IntSlider(name="Int型スライダー", start= 1, end= 10)
# 浮動小数点型スライダーのウィジェット生成
f_slider = pn.widgets.FloatSlider(name="Froat型スライダー", start= 1, end = 10)

# 表示
pn.Column(i_slider, f_slider)
```

![13_slider.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/6e641fa3-d361-b263-0189-f75709423467.gif)


#### ステップ数と初期値の指定

stepで幾つずつ刻むか指定できます。
valueでは初期値を指定できますが、スライダーの値はstart値基準でstep値が刻まれるます。そのため、下記例では初期値の5から一度動かしてしまうと、再び5につまみを合わせることが出来なくなっています。

```python
# 初期値とステップ数を指定したスライダー
sl_stvl = pn.widgets.FloatSlider(name= "カスタムスライダー", start= 1, end= 10, step= 0.3, value= 5)
# 表示
sl_stvl
```

![14_slider_step_value.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/b8c328d6-c8e6-e015-b985-4f1724b7bb20.gif)

#### 表示形式の指定

値の表示形式はformatで指定できます。
numbroの旧フォーマットというもので指定します↓。
参考サイト：http://numbrojs.com/old-format.html

通常のformatでは表現の幅が少し狭いので、こだわった表示をしたかったら、bokehモデルのPrintfTickFormatterを使います。
formatにPrintfTickFormatterを指定し、さらにその中のformatにPrintf表示形式を文字列で指定します。
printfについては下記サイトが参考になりました。
https://www.mm2d.net/main/prog/c/printf_format-01.html

```python
# フォーマットの指定（小数点以下1桁まで表示）
f_slider2 = pn.widgets.FloatSlider(name="小数点1桁まで表示", start= 1, end = 10,
                                  format="0.0")

# PrintfTickFormatterのインポート
# （bokehはpanelと同時にインストールされるため追加インストール不要です）
from bokeh.models.formatters import PrintfTickFormatter
# フォーマットの指定（100％中の〇〇％と表示）
f_slider3 = pn.widgets.FloatSlider(name="戸愚呂弟", start= 0, end = 120,
                                  format=PrintfTickFormatter(format="100 %% 中の %.1f %%"))

# 表示
pn.Column(f_slider2, f_slider3)
```

![15_slider_format.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/db91fd60-2e65-0556-2431-ca6ec27a1271.jpeg)


#### スライダーの見た目のカスタマイズ

その他、バーの色（bar_color）や、縦横の向き（orientation="vertical"で縦向き）などを指定できます。
directionは左から（ltr:Left_to_Right）か右から（rtl:Right_to_Left）かを指定します。
縦向きの場合は下側がRigthとなります。

```python
# カスタマイズしたスライダー
customize_slider2 = pn.widgets.IntSlider(start= 0, end= 10, 
                                        bar_color="#FF8080",
                                        orientation="vertical",
                                        direction="rtl",
                                        height=100)
# 表示
customize_slider2
```

![16_slider_customize.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/1c124458-3630-b372-a645-9d15ed775079.jpeg)


## RangeSlider, IntRangeSlider（範囲指定型スライダー）
範囲を指定できるRangeSlider（←浮動小数点型。整数型はIntRangeSlider）もあります。

```python
# 範囲スライダー（整数型）
range_int_slider = pn.widgets.IntRangeSlider(name="Int型_範囲スライダー", start= 1, end = 10)
# 範囲スライダー（浮動小数点型）
range_float_slider = pn.widgets.RangeSlider(name="Float型_範囲スライダー", start= 0, end= 10, step= 0.1)

# 表示
pn.Column(range_int_slider, range_float_slider)
```

![17_range_slider.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/d257be1d-6973-9251-0f0b-6a2dfae759eb.jpeg)


#### 値の取得

RangeSliderの値もvalueで取得でき、左端と右端のタプルで返されます。

```python
print(range_int_slider.value)
```

(1, 10)

## TextInput

入力した文字列を取得するウィジェットです。
placeholoderで未入力の時に表示する文字列を指定します。

```python
# インスタンス生成
textinput = pn.widgets.TextInput(name= "テキストインプット", placeholder= "ここに文字を入力してください。")

# 表示
textinput
```

![18_text_input.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/cf30dd9f-031d-666a-22ef-8c20f12d1ad3.jpeg)

#### 高さ・幅の指定

前回書いた記事では「高さを指定できない」と間違ったことを書いてしまいましたが、普通にheight指定できます。

```python
# インスタンス生成
textinput_sizing = pn.widgets.TextInput(name= "高さも幅も変えられます", placeholder= "ここに文字を入力してください。", 
                                       width= 100, height= 80)

# 表示
textinput_sizing
```

![19_text_input_sizing.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0999f9e3-c417-b15b-fe5a-b706f0cf9bd6.jpeg)


#### 入力文字数上限の指定

入力する文字数の上限も指定できます。

```python
# インスタンス生成
textinput_max10 = pn.widgets.TextInput(name= "最大10文字", placeholder= "全角含め最大10文字入力できます",
                                      max_length= 10)

# 表示
textinput_max10
```

![20_textinput_maxlength.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/44b25052-fb1f-151c-6118-53372bda1e5e.gif)


ちなみに、ウィジェットの状態に応じて関数を動作させるdependsデコレータを使用すれば、入力できる文字数の残りを表示する、といったカスタマイズも可能です。

```python
# インスタンス生成
textinput_max100 = pn.widgets.TextInput(name= "文字数のこり:100", placeholder= "全角含め最大100文字入力できます",
                                      max_length= 100)

# テキストボックスの中身が更新されるたびに実行する関数
@pn.depends(textinput_max100.param.value, watch= True)
def length_count(text_value):
    text_l = textinput_max100.max_length - len(text_value)
    textinput_max100.name = "文字数のこり:" + str(text_l)

# 表示
textinput_max100
```

![21_text_length_count.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/b54dcccc-c39d-01a7-5380-2fdb51e74684.gif)

完全にリアルタイムではなく、ウィジェット上でEnter押したときに内容が反映されるようです。


## FileInput
FileInputを使えば、ユーザーにファイルを選択させることができます。

```python
# インスタンス生成
file_input = pn.widgets.FileInput(name= "ファイル選択")
# 表示
file_input
```

![22_fileinput.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/08e00430-3fc8-3eda-db85-f18333c088d0.jpeg)


前回の記事で紹介しなかったのは、選択したファイルを扱うにはひと手間必要だからです。
まず、一応filename使えば選択されたファイルの名前を取得することができるのですが、フルパスでは取得することができません。
そのため、ファイルをデータとして扱うにはvalueを使用する必要があります。
ただし、valueで取得したデータはbytes型となるため、中身のタイプに応じて変換する必要があります。
そのやり方として標準ライブラリioのBytesIOもしくはStringIOを使用してメモリ上に一度保存する方法があります。

#### 選択されたファイルの取り扱い（文字列データ以外のケース）

文字列以外のデータはBytesIOでバイナリデータをメモリに書き込みます。
下記は選択された画像データ（png）を読み込むケースです。

```python
import io
from PIL import Image

# bytesデータを標準ライブラリioのBytesIOを使用しメモリ上に保存
buffer = io.BytesIO(file_input.value)
# メモリ上のファイルを読み込み
im = Image.open(buffer)
# 画像表示
im
```

#### 選択されたファイルの取り扱い（文字列データのケース）

文字列データ（CSVなど）はStringIOを使用します。
このとき、バイナリデータをいったんデコードする必要があります。デコードでは文字コードを指定する必要があるのですが、無難にutf-8-sig（BOM付きUTF8）を指定しておけば良いと思います。
下記はcsvのデータを取り出しDataFrameに変換するケースです。

```python
import io
import pandas as pd

# 読み込んだファイルのバイトデータをデコード後、StringIO形式でメモリに保存
buffer = io.StringIO(file_input.value.decode("utf-8-sig"))
# 保存したメモリ上のアドレスを参照しCSVとして読み込みDataFrameに変換
df = pd.read_csv(buffer)
# DataFrame表示
df
```

#### 選択したファイルを同一セルで扱う場合の注意点

一つのセル内でFileInputと表示を行なう場合、「～.valueにデータがない！」とエラーが出るので、Buttonを使用して能動的に変換を行なうか、dependsデコレータを使用してファイル選択がされたと同時に変換を実行するようにする必要があります。（dependsデコレータについては前回記事を参照）
下記はdependsを使用しファイルが開かれると同時に画像が表示される例です。

```python
# インスタンス生成
file_input = pn.widgets.FileInput(name= "ファイル選択", accept=".png")
output = pn.pane.PNG()

# ファイルが選択されると実行される関数
@pn.depends(file_input.param.value, watch=True)
def get_file(f_value):
    # バイト列をメモリ上のバッファに格納
    buffer = io.BytesIO(f_value)
    # バッファを読み込み
    output.object = buffer

# 表示
pn.Column(file_input, output)
```

![25_fileinput_with_open.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/b02ac37b-1711-c5bc-0249-4d6983e5f1c7.gif)

#### GoogleColaboratotyでもローカルのファイルを取り扱うことが可能

GoogleColaboratoryでファイルを扱う場合、毎回GoogleDriveと接続してファイルをアップロードするなど、何かと手間がかかるイメージがあります。
FileInputはGoogleColaboratory上でもローカルのファイルをアップロードしてそのまま使用できるので、手っ取り早くファイルを開きたい場合などにオススメです。

![26_fileinput_googlecolab.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/68df1818-2ea6-d8f4-b7d5-6c96c124bec2.gif)

# 今後
今回は、とりあえず自分が使いそうな入力系ウィジェットについてまとめました。
また気になるウィジェットが出てきたら適宜追加していきたいと思います。