Bokehの散布図でラベルを動的に表示・非表示させる

本稿ではこういう散布図をつくります▼
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/153e4727-3e64-87bc-bfc7-e1b79fcbe34b.gif" width=480 />

## Bokehとは
**Bokeh** はビジュアリゼーション用Pythonライブラリです。

https://bokeh.org/

類似するライブラリはいくつかありますが、Bokehは **インタラクティブ** な点が大きな特徴です。デフォルトのまま適当に使っても拡大縮小やエクスポートなどの機能が動的に使えるような出力が得られ、カスタマイズによる機能追加等も柔軟に行なえます。（その代わりJavaScriptが動く環境でないと真価を発揮できず、画像を吐き出すだけの別ライブラリのほうが取り回しが良いケースもあると思います）
ライブラリのインターフェース自体も使いやすく、個人的に好きなライブラリです。

## 環境
- Google Colaboratory
- Python 3.7.13
- Bokeh 2.4.3

## Bokehで散布図をつくる

### サンプルデータの準備
Bokehに付属している[ペンギンデータセット](https://docs.bokeh.org/en/latest/docs/reference/sampledata.html?highlight=flowers#module-bokeh.sampledata.penguins)を使って、まずは普通の散布図を描く準備をします。

```python
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import ColumnDataSource, Label, LabelSet, Button, CustomJS
from bokeh.layouts import column

from bokeh.sampledata.penguins import data
```

```
output_notebook()
```

`data`に入っているデータを確認します。欠損が含まれるので、まずは削除しておきます。

```
data = data.dropna()
```

中を覗いてみます。

```python
data.head()
```

|index|species|island|bill\_length_mm|bill\_depth_mm|flipper\_length_mm|body\_mass_g|sex|
|---|---|---|---|---|---|---|---|
|0|Adelie|Torgersen|39\.1|18\.7|181\.0|3750\.0|MALE|
|1|Adelie|Torgersen|39\.5|17\.4|186\.0|3800\.0|FEMALE|
|2|Adelie|Torgersen|40\.3|18\.0|195\.0|3250\.0|FEMALE|
|4|Adelie|Torgersen|36\.7|19\.3|193\.0|3450\.0|FEMALE|
|5|Adelie|Torgersen|39\.3|20\.6|190\.0|3650\.0|MALE|

`species`は、`Adelie`・`Chinstrap`・`Gentoo`の3種類のカテゴリ値になっています。

```python
data["species"].unique()
> array(['Adelie', 'Chinstrap', 'Gentoo'], dtype=object)
```

X軸に`bill_length_mm`、Y軸に`bill_depth_mm`を取り、3種類の`species`を色分けして表示する散布図を描いてみます。

データを前処理して、

```python
x = data["bill_length_mm"]
y = data["bill_depth_mm"]
colors = [{"Adelie": "red", "Chinstrap": "green", "Gentoo": "blue"}[r] for r in data["species"]]
```

実際の表示するコードは下記です。

```python
p = figure()
p.circle(x, y, color=colors, fill_alpha=0.2, size=10)

show(p)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/93c23696-d340-5020-6f6f-c4bbc6a95fa9.png" width=480 />


### ツールチップで値を表示
Bokehには`tooltips`という機能があり、カーソルを当てると小さいウィンドウが出てきて好きな値を表示するようにできます。ここでは`@colors`のような特殊な記法で変数の値を表示できます。

参考: [First steps 4: Customizing your plot — Bokeh 2\.4\.3 Documentation](https://docs.bokeh.org/en/latest/docs/first_steps/first_steps_4.html#adding-tooltips)


```python
TOOLTIPS = [
    ("index", "$index"),
    ("(x,y)", "($x, $y)"),
    ("desc", "色は@colors"),
]
p = figure(tooltips=TOOLTIPS)
source = ColumnDataSource(data={'x_values': x, 'y_values': y, 'colors': colors})

p.circle(source=source, x="x_values", y="y_values", color="colors", fill_alpha=0.2, size=10)

show(p)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/e7ff1443-66ba-866b-2729-c83c665e279f.gif" width=480 />

### ラベルを表示
ツールチップはカーソルを当てた値しか表示できません。`LabelSet`を使って、すべての値のラベルを表示するようにしてみます。

参考: [annotations — Bokeh 2\.4\.3 Documentation](https://docs.bokeh.org/en/latest/docs/reference/models/annotations.html#labelset)

```python
p = figure()

source = ColumnDataSource(data={'x_values': x, 'y_values': y, 'colors': colors, '色': [ f"色は「{c}」" for c in colors ] })
labels = LabelSet(x='x_values', y='y_values', text='色', x_offset=5, y_offset=5, source=source, render_mode='canvas')
p.add_layout(labels)

p.circle(source=source, x="x_values", y="y_values", color="colors", fill_alpha=0.2, size=10)

show(p)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/eed54491-e974-ad94-86b3-a28f3fdf874c.png" width=480 />

大変なことになってしまいました。

ラベルの表示・非表示を、好きなタイミングで動的に切り替えられるようにしましょう。
Bokehにはインタラクティブなウィジェットを追加し、JavaScriptで挙動を制御できます。

参考: [Adding widgets — Bokeh 2\.4\.3 Documentation](https://docs.bokeh.org/en/latest/docs/user_guide/interaction/widgets.html)

```python
p = figure()

source = ColumnDataSource(data={'x_values': x, 'y_values': y, 'colors': colors, '色': [ f"色は「{c}」" for c in colors ] })
labels = LabelSet(x='x_values', y='y_values', text='色', x_offset=5, y_offset=5, source=source, render_mode='canvas')
p.add_layout(labels)

p.circle(source=source, x="x_values", y="y_values", color="colors", fill_alpha=0.2, size=10)

button = Button(label="ラベルかくす")
button.js_on_click(CustomJS(args=dict(labels=labels), code="labels.visible = !labels.visible;"))

show(column(p, button))
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/ababbada-cedf-713f-bb55-feb69bc78a20.gif" width=480 />

## 参考
- [python \- Hide annotation alongside legend in Bokeh \- Stack Overflow](https://stackoverflow.com/questions/61210735/hide-annotation-alongside-legend-in-bokeh)
- [Bokehで散布図\(scatter plot\)を作成する例 \- シラベタ](https://shirabeta.net/Bokeh-example-iris-circle.html#.YtUmfXZBy4R)