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

# はじめに

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

今回は主に**処理した結果を画面表示するといった出力系ウィジェット**についてまとめます。
ちなみに入力系ウィジェットはコチラ↓
　[ダッシュボード作成ライブラリPanelのウィジェット紹介 ～入力系編～](https://qiita.com/hisakichi95/items/fb3a12338d5cc8911f76)

特に入力系ウィジェットと区別する必要もないのですが、以下パネルと呼びます。
出力系のパネルは基本`pn.pane.～()`でインスタンスを生成します。

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

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

## Str
文字列を表示するだけのパネルです。あまり細かく見た目の調整ができないので、後述するHTMLやMarkDownの方が正直便利です。

```py
# インスタンス生成
str_pane = pn.pane.Str("こんにちは")
# 表示
str_pane
```

![30_Str.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/b1c9e46a-2699-bedb-b6fa-b4068720cc24.jpeg)

## HTML
HTML記法を使用できるため細かく書式設定可能なパネルです。
pn.pane.HTML(<HTML文字列>)でインスタンスを生成します。

```py
# インスタンス生成
html_pane = pn.pane.HTML("""
<h3>本日の出来事</h3>
<div style="background-color: whitesmoke; border-radius: 5px; padding: 5px;">
    <ul>
        <li>雨が降った</li>
        <li>お昼ご飯が美味しかった</li>
        <li>映画館で「ゴジラ」を見た</li>
    </ul>
<div>
""")

# 表示
html_pane
```

![31_HTML.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/18a50cc0-61d8-49ae-1e7d-8d815fb87a7c.jpeg)
右側のマージンが設定されていないためバランスがおかしい点はご容赦ください。

#### f文字列を用いた表示

f文字列を使用してhtmlに変数の値を埋め込むことも可能です。

```py
movie = "100日間生きたワニ"

# インスタンス生成
html_pane_f = pn.pane.HTML(f"""
<h3>本日の出来事</h3>
<div style="background-color: whitesmoke; border-radius: 5px; padding: 5px;">
    <ul>
        <li>映画館で「{movie}」を見た</li>
    </ul>
<div>
""")

# 表示
html_pane_f
```

![32_HTML_f.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/9606a6a8-29ec-8c38-74cd-25b10a712097.jpeg)

#### styleの指定と注意点

style引数にdict型で書式を指定すれば、まとめて書式変更できます。

```py
# インスタンス生成（style引数指定あり）
html_pane2 = pn.pane.HTML("""
<h3>本日の出来事</h3>
<div style="background-color: whitesmoke; border-radius: 5px; padding: 5px;">
    <ul>
        <li>雨が降った</li>
        <li>お昼ご飯が美味しかった</li>
        <li>映画館で「ゴジラ」を見た</li>
    </ul>
<div>
""", style={"color": "blue", "text-align": "center"})

# 表示
html_pane2
```

![32_HTML_style.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/1ffd5710-32ce-b17d-c0c8-3f84afd8c60a.jpeg)

styleオブジェクトを指定して後から追加・変更することも可能ですが、<font color="red">styleにそのまま特定の書式だけ記されたdict型を渡してしまうと追加指定しなかった書式項目が初期化されてしまいます</font>。これを回避するため、style[書式項目]=変更後の書式内容と指定する必要があります。
複製にはcloneメソッドを使用。（copy.deepcopyだとエラーになる）

```py
# インスタンス複製
html_pane3 = html_pane2.clone()

# dictを渡してしまうと元の書式は失われる。
html_pane2.style = {"font-family":"游明朝"}

# 上を回避するためstyleの要素keyに値を渡して書式を追加指定
html_pane3.style["font-family"]="游明朝"

# 表示
pn.Row(html_pane2, html_pane3)
```

![33_HTML_style_change.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0587b243-e287-511e-9d93-1351eb7631bf.jpeg)

#### スタイルシート（CSS）の適用

少し面倒ですが、スタイルシートによる書式設定も可能です。
まず、CSSの中身を記した文字列を用意しておき、過去におまじないと紹介したextensionを記述する際、raw_css引数にCSSを記述した文字列を渡しておきます。
CSSの中で指定するClass名の法則性はまだよく理解できていないのですが、.bk.をつけないと上手く表示されないっぽいです。（bkはbokehのbk?）

```py
# CSS記述
css = """
.bk.panel-html {
  background: #ff8080;
  border-radius: 5px;
  border: 1px black solid;
  padding: 0px 40px 10px 20px;
}
"""

# 拡張時にraw_cssを指定
pn.extension(raw_css = [css])
```

CSSの指定ができたら、HTMLインスタンスを生成する際、引数のcss_classesにlist型でクラス名を指定します。
このクラス名には上記で指定したクラス名から.bk.を除外したものを指定する必要があります。（なぜかは理解できていない）

```py
# インスタンス生成（css_classes引数指定あり）
html_pane_css = pn.pane.HTML("""
<h3>本日の出来事</h3>
<div>
    <ul>
        <li>雨が降った</li>
        <li>映画館で「ゴジラ」を見た</li>
    </ul>
<div>
""", css_classes=['panel-html'])

# 表示
html_pane_css
```

![34_HTML_css.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/69a4dc87-b75f-3619-9f86-99514a88c50c.jpeg)

## Markdown
マークダウン方式に対応したパネルも存在します。
表記方法が異なること以外は大体HTMLと同じです。

```py
# インスタンス生成
markdown_pane = pn.pane.Markdown("""
### 本日の出来事
+ お昼にパスタを食べた<br> 

改行するには  
スペース２つか<br>&lt;br&gt;タグが必要
""")

# 表示
markdown_pane
```

![35_markdown.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/23307352-59f1-05be-5862-82f87321a42f.jpeg)

## PNGなど（画像表示）

画像を表示するパネルです。
ファイルの拡張子に合わせてパネルを選択する必要があります。
例えばPNGなら、pn.pane.PNG(<画像ファイル名>)でインスタンス生成します。

```py
# インスタンス生成
png_pane = pn.pane.PNG(r"C:\フォルダー\test.png")
# 表示
png_pane
```

![36_png.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/a70c6cdf-876f-a415-4ff5-855a83383148.jpeg)

#### パス指定時の注意点（拡張子は要:小文字表記）

注意しないといけないのは、<font color="red">拡張子が大文字の場合、そのまま拡張子部分を大文字で読み込むとエラーが出る</font>ことです。元のファイルの拡張子が大文字でも、小文字で指定しましょう。

```py
# 大文字拡張子（PNG）だとエラーが出る

png_pane_err = pn.pane.PNG(r"C:\フォルダー\test.PNG")
# -> ～.pngとして指定すればエラーは出ない
```

![37_png_error.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/693c4178-a243-3a76-6059-35405ee3adf4.jpeg)

#### サイズの指定

引数でwidth,heightを指定することで、画像サイズを変更することも可能です。

```py
# インスタンス生成（width指定あり）
png_pane2 = pn.pane.PNG(r"C:\フォルダー\test.png", width=100)
# 表示
png_pane2
```

![38_png_sizing.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/cbd078e4-66f8-3574-7823-7b04dcce318b.jpeg)

#### Pillow等ファイル形式でないデータの表示

Pillow等のデータは直接読み込めないので、<font color="red">io.BytesIOを使用して一度メモリ上に保存し、そのアドレスを指定して読み込む</font>と良いです。

```py
from PIL import Image
import io

# Pillow形式の画像用意
pil_image = Image.open(r"C:\フォルダー\test.png")
# 画像をメモリ上に格納するためのバッファ用意
buffer = io.BytesIO()
# バッファに画像をpngとして格納
pil_image.save(buffer, format="png")

# インスタンス生成 + 表示
pn.pane.PNG(buffer)
```

## DataFrame

pandasのDataFrameを表示できるパネルです。 Jupyter等では直接表示した方がきれいに整えて表示されるため恩恵薄そうな気もしますが、やはりアプリケーションとして考えたときにグラフの横に表示するなどレイアウト面ではメリットもありそうです。まだ検証できていませんが、Flaskなどで作成したWebアプリケーションでDataFrameを確認する場合にも必要そうです。

事前準備：表示するデータ読み込み。

```py
import pandas as pd

# csv読み込み
df = pd.read_csv(r"C:\フォルダー\pokemon_kanto.csv",  index_col=0)
```

#### 幅表示の異常

そのまま表示すると幅がおかしくなってしまいます。

```py
pn.pane.DataFrame(df) 
```

![39_DataFrame_distorted.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/cce6f963-4af6-1fe3-9f66-53f803bbad94.jpeg)

対応策①sizing_mode=を"stretch_width"に指定
⇒出力可能な範囲に合わせてフレーム全体の横幅が拡張され、各カラムの幅が均等に調整されます。

```py
pn.pane.DataFrame(df, sizing_mode='stretch_width')
```

![40_DataFrame_stretch.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/8c5d62af-8878-7802-cb2a-9bd564ec50f0.jpeg)

対応策② widthをちゃんと指定
⇒指定されたwidthにそって各カラムの幅が均等に調整されます。

```py
pn.pane.DataFrame(df, width=728)
```

![41_DataFrame_width_order.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/c1f98ece-2e45-2ee7-464b-512b491ad656.jpeg)

本当は、DataFrameを直接表示したときのように各カラムごとに幅を自動調整してくれれば良いのですが、やり方が現状分かりません。。（公式リファレンスを見る限りは特に設定せずともそのような見た目になっているのですが。。。）

#### 表示行数の指定、要素数の可視化

show_dimensionsをTrueにすると、pandasのように何行×何列かを表示できます。
またmax_rowsを指定するとこちらもpandasのように途中を省略してくれます。
（自動的に高さが決まってスクロールするようになってしまうため、height指定した方が無難）

```py
df_pane_row6 = pn.pane.DataFrame(df, width=768, height= 400, 
                                  max_rows=6, show_dimensions= True)
df_pane_row6
```

![42_DataFrame_maxrows_show_dimensions.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/caeef93e-ee0a-6d11-2ea1-d4315f7e4075.jpeg)

## Matplotlib
グラフを表示するパネルも複数ありますが、まずは無難にMatplotlibを紹介します。
panelでMatplotlibを表示するにはfigureが必要なので、まずfigureを準備します。


```py
import matplotlib.pyplot as plt

# 日本語を表示できるようにするためフォントを変更
plt.rcParams['font.family'] = "Meiryo"

# figure（描画領域）の定義
fig1 = plt.figure()
# axes（グラフ）の定義
ax1 = fig1.add_subplot(1,1,1)

# dataframeから先ほど定義したaxesにグラフを描画
df.plot.scatter(ax= ax1,x='weight', y='素早さ')
```

あとはpane.Matplotlib()にfigureを渡してあげるだけです。

```py
# インスタンス生成 + 描画
pn.pane.Matplotlib(fig1)
```

![43_matplotlib.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/2b66f786-98fd-29af-c269-d718c8c3e6c7.jpeg)

## HoloViews

グラフ表示で個人的におススメしたいのが、もう一つ紹介するHoloViewsパネルです。
panelではグラフ系のラッパーツールHoloviewsのオブジェクトも表示することができます。 
下記の例を見ていただければ伝わるかと思いますが、上のMatplotlibと比較して、非常に少ない行数でグラフを表示できます。

描画は生成したオブジェクトをpane.Holoviews()の引数に指定するだけでOKです。

```py
# !pip install holoviews
import holoviews as hv
​
# グラフオブジェクトの生成
poke_graph = hv.Scatter(df,kdims=["height"],vdims=["weight"])

# 描画
pn.pane.HoloViews(poke_graph)
```

![44_holoviews.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/498af849-d801-ebd8-a645-16be424299e6.jpeg)

#### HoloViewsの書き方（簡易版）

HoloViewsの書き方については過去記事も是非見ていただきたいのですが、簡単に説明すると

+ Scatter ← グラフの種類を指定。ここでは散布図。
+ df ← ソースとなるデータを指定
+ kdims ← KeyDimensionsの略。とりあえずX軸に該当すると理解している。要リスト形式で渡す必要あり。
+ vdims ← ValueDimensionの略。Y軸に該当する以外に、例えばマーカーカラーやサイズなどのパラメータとして使いたいカラムもここで指定しておく。

#### panelウィジェットと組み合わせてグラフをカスタマイズ

また、HoloViewsはマーカーのサイズなどのパラメータをpanelの各種ウィジェットと連動させることができます。
連動させたいウィジェットの.jslinkメソッドに引数で以下を指定するとOKです。
・連動させたいHoloviewsオブジェクト（Bokehオブジェクトも可）
・オブジェクトのなんのパラメータに連動させるか。下記例ではglyph.sizeでマーカーサイズに連動させている。
正直どのパラメータをいじるために何という文字列を指定する必要があるのか整理できていません。ただ、どうやらBokehモデルが関係しているっぽいので、Bokehの公式リファレンスが参考になるかもしれません（[例：BokehのScatterリファレンス](http://docs.bokeh.org/en/latest/docs/reference/models/glyphs/scatter.html#bokeh.models.Scatter)）

```py
# スライダー（整数型）オブジェクト生成
slider = pn.widgets.IntSlider(name='Integer Slider', start=1, end=5, step=1, value=1)

# グラフの点サイズをスライダーの初期値に指定
poke_graph = poke_graph.opts(size=slider.value)
# スライダーをグラフの点サイズとリンクさせる
slider.jslink(poke_graph, value="glyph.size")

# 描画
pn.Column(poke_graph, slider)
```

![45_holoviews_jslink.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/efb22aa8-d5ef-eeff-ec77-227e809f6ee5.gif)

## Folium

地図を表示するパネルはいくつか存在しますが、その中でも一番敷居が低そうなFoliumを紹介します。
といってもFoliumでMapオブジェクトを生成し、Foliumパネルに引数として与えてあげるだけです。

```py
import folium

# Foliumのmapオブジェクト生成
m = folium.Map(location=[35.658584,139.7454316], zoom_start=12)

# インスタンス生成
folium_pane = pn.pane.plot.Folium(m, height=400, width= 600)
# 表示
folium_pane
```

![46_folium_map.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/0112bd3f-3562-b335-6a01-ff9ddb8f71d5.jpeg)


#### マーカーの追加
マーカー追加するには、以下のようにMarkerオブジェクトをMapに追加し、そのMapをパネルに与えます。

```py
import folium

# Foliumオブジェクト生成
m = folium.Map(location=[35.658584,139.7454316], zoom_start=12)

# マーカー追加
folium.Marker(
    location=[35.658584, 139.7454316], popup="東京タワー"
).add_to(m)
        
# インスタンス生成
folium_pane = pn.pane.plot.Folium(m, height=400, width= 600)
# 表示
folium_pane
```

![47_folium_map_marker.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/26c5d6ae-dce6-a6e9-6420-fc576b45e338.jpeg)

ただし、Folium自体の問題として、ポップアップに日本語が使われていた場合に文字が縦方向に並んでしまうというのがあるみたいです。おそらく英語と日本語の一文字当たりの幅が影響していそうですね。。
![48_folium_map_marker_ng.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/52902613-133a-ef07-3c52-51f3102259f3.jpeg)
幸いpopupにはHTMLタグが使用できますので、これで問題を強引に解決してみます。

```py
import folium

CHAR_WIDTH = 15  # 1文字あたりの幅（px）

# HTMLタグによるポップアップ幅の指定
def define_width(text):
    width_px = str(len(text) * CHAR_WIDTH)
    return f"<div style='width:{width_px}px; text-align:center'>{text}</div>"
 
# Foliumオブジェクト生成
m = folium.Map(location=[35.658584,139.7454316], zoom_start=12) 

# マーカー追加
folium.Marker(
    location=[35.658584, 139.7454316], popup= define_width("東京タワー")
).add_to(m)
        
# インスタンス生成
folium_pane = pn.pane.plot.Folium(m, height=400, width= 600)
# 表示
folium_pane
```

![49_folium_map_marker_ok.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/45084742-ddde-9ff5-ea18-6a8c6a5eb48d.jpeg)

#### ウィジェットと組み合わせてみる

せっかくのPanelライブラリなので、ウィジェット（ボタン）と組み合わせみます。
ボタンを押すと東京タワーのポップアップの表示／非表示が切り替わります。
マーカーのポップアップには事前に生成したPopupオブジェクトを指定し、
そのPopupオブジェクトのshowの中身のTrue／Falseを切り替えることで、表示・非表示を切り替えています。

```py
import folium
CHAR_WIDTH = 15  # 1文字あたりの幅（px）

# HTMLタグによる装飾を
def define_width(text):
    width_px = str(len(text) * CHAR_WIDTH)
    return f"<div style='width:{width_px}px; text-align:center'>{text}</div>"

# Foliumオブジェクト生成（マップタイプを白黒に）
m = folium.Map(location=[35.658584,139.7454316], zoom_start=12, tiles="Stamen Toner")

# ポップアップ追加
popup = folium.Popup(html= define_width("東京タワー"), show=True)
# マーカー追加
folium.Marker(
    location=[35.658584, 139.7454316], popup= popup
).add_to(m)

# ボタンウィジェット生成
b = pn.widgets.Button(name="push", button_type= "primary")

# ポップアップの表示状態とボタンタイプを切り替える関数
def pushed_button(event):
    # ポップアップの表示状態の切り替え
    popup.show = not popup.show
    # ポップアップ表示状態にあわせボタンタイプの切り替え   
    b.button_type = "primary" if popup.show else "success"
    # Foliumパネルのオブジェクト更新
    folium_pane.object = m

# ボタンに関数を関連付け
b.on_click(pushed_button)
        
# インスタンス生成
folium_pane = pn.pane.plot.Folium(m, height=400, width= 600)
# 表示
pn.Row(folium_pane, b)
```

![50_folium_with_button.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/ce73127b-a3fb-4573-2c80-36312287bdd2.gif)
（毎度マップを読み込んでいるのが気になります。。直接mapのPopupにアクセスできれば良いのですが、方法がわかりません。。。）

ちなみにマップの見た目をtiles="Stamen Toner"のところで白黒に変更しています。
"Stamen Toner"はマーカーも見やすく、デザイン的に気に入っています。


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