グラフ上でマウスドラッグしてデータの詳細を表示させたい

# やりたいこと
グラフ上で範囲を選択して、その範囲に含まれているデータの詳細を別テーブルなどで表示させたいです。
![01_アウトプットイメージ_.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/e751053b-5e57-634e-a881-35cdec368304.jpeg)

# できたもの
（gif）
![02_output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/574cd5ad-03b0-8868-7ebb-98f80e440a98.gif)

**HoloViews**の**Selection1D**を活用して実現できました。

# 解説

上記をつくる過程を紹介しながら、解説をします。
過去の記事で紹介したようなHoloViewsやPanelの解説と一部重複しますが、ご承知おきください。

https://qiita.com/8_hisakichi_8/items/8be61af4e141d11a77d9

https://qiita.com/8_hisakichi_8/items/717b0c78c00a9de9bd5a

## コード

```python
import pandas as pd
import panel as pn
import holoviews as hv
from sklearn.datasets import load_iris
iris = load_iris()

def initialize():
  df = pd.DataFrame(iris.data, columns=iris.feature_names)
  df['species'] = iris.target_names[iris.target]
  return df

# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更（列名に()が含まれるとホバーツールの表示おかしくなるため）
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        selection_info.value = sdf.query("index in @selected_index").drop("size", axis=1)
        selection_info.visible = True
        
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# 選択データ表示テーブルの定義
selection_info = pn.widgets.DataFrame(df, width=400, height=400, visible=False, row_height=25, autosize_mode="fit_viewport")

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, selection_info)
```

## 環境

+ Windows10 + Jupyterlab<br>(Google Colabでも実装可能ですが、重い+謎の警告出るためローカル環境推奨）
+ Python 3.9.1
+ HoloViews 1.14.5
+ Panel 0.12.1

## インポートと使用データの取得

ライブラリとデータの準備を行ないます。
使用するデータですが、sklearnを使ってみんな大好きIRISデータをダウンロードします。

```python
import pandas as pd
import panel as pn
import holoviews as hv
from sklearn.datasets import load_iris
iris = load_iris()

def initialize():
  df = pd.DataFrame(iris.data, columns=iris.feature_names)
  df['species'] = iris.target_names[iris.target]
  return df
```
pandas,sklearnの他、**holoviews, panel, bokehは必須のライブラリなので、未インストールの場合はpipコマンドでインストールしておきます**（ここでは割愛）。


## 基本のグラフをHoloviewsで書く

```python
# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)"]) 
scatter = scatter.opts(width=500, height=400, fontscale=1.5, size=5,
                   show_grid=True, title="Iris_data")

# 表示
scatter
```

HoloViewsを使用する場合、下記のように毎回セルごとにおまじないをする必要があります

:::note warn
\# HoloViewsのバックエンド指定
<font color="Red">hv.extension("bokeh")</font>
:::

グラフを描画します。第一引数に使用するデータ（今回の場合dfというDataFrame）を指定し、`kdims`(=Key Dimensions)にX軸にあたる要素、`vdims`(=Value Dimensions）にY軸にあたる要素をリストで指定します。

:::note warn
\# 描画するグラフデータの生成
scatter = <font color="Red">hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)"]) </font>
:::

その後、`.opts()`メソッドを使用してグラフの様々な設定を変更。

:::note warn
scatter = scatter<font color="Red">.opts(<font color="Darkblue">width=550, height=400, fontscale=1.5, size=5,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;show_grid=True, title="Iris_data"</font>)</font>
:::

表示は作成したインスタンスをそのまま書くだけで、ノート上で表示してくれます（DataFrameなどと同じ感覚）

:::note warn
\# 表示
scatter
:::

ここまでの結果
&nbsp;&nbsp;↓
![10_default_holoviews.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/ca58f04f-9493-c692-c087-4e897fe3c031.jpeg)


## グラフの色を変更する

<details><summary><font color="Darkblue">この章までで書くコード（クリックして表示）</font></summary>

```python
# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species"]) 
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right", 
                   width=550, height=400, fontscale=1.5, size=5,
                   show_grid=True, title="Iris_data",
                   )

# 表示
scatter
```

</details>

データ内の特定の列の情報をもとにグラフの色を設定したい場合、vdimsに指定したい列名を追加した上で、optsメソッドで`color`引数にその列名を追加します。数値でもカテゴリデータでも同じ書き方でOKです。

:::note warn
\# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", <font color="Red">"species"</font>]) 
scatter = scatter.opts(<font color="Red">color="species",  </font><font color="Darkblue">cmap="Category10", legend_position="right", </font>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;width=550, height=400, fontscale=1.5, size=5,
:::

また、optsメソッドの`cmap`でカラーマップを、`legend_position`で凡例の表示位置を指定できます。

ここまでの結果
&nbsp;&nbsp;↓
![11_withcolor_holoviews.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/5540d03a-d695-6d11-cd02-63fcdf9a3f5f.jpeg)


## グラフの各データの表示サイズを変更する

<details><summary><font color="Darkblue">この章までで書くコード（クリックして表示）</font></summary>

```python
# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# サイズを規定する列を追加
def minmax_norm(df_input):
  return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
df["size"] = (minmax_norm(df["petal width (cm)"]) + 1) * 5

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species", "size"]) 
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right", 
                   width=550, height=400, fontscale=1.5, size="size", 
                   show_grid=True, title="Iris_data",
                   )

# 表示
scatter
```

</details>

バブルチャートのように、ある列のデータから各点のサイズを変更することもできます。
色の時と同じような指定が可能なのですが、データの数値をそのまま当てはめてしまうと、点が異常に大きくなってしまうため、所望のサイズになるよう数値を変換する必要があります。
やり方は色々ありますが、今回はPandasの列（Series）を受け取って0～1で正規化した列を返す関数を作り、その数値を加算乗算して所望の数値に変換する方法を採用しました。

:::note warn
 \# サイズを規定する列を追加
 <font color="Red">def minmax_norm(df_input):</font>
 &nbsp;&nbsp;<font color="Red">return (df_input - df_input.min()) / ( df_input.max() - df_input.min())</font>
 <font color="Red">df["size"] = (minmax_norm(df["petal width (cm)"]) + 1) * 5</font>
:::

size列を作成したら、あとは色の時と同じように追加した列名（ここではsize）をvdimsに追加し、optsメソッドの`size`引数に指定するだけです。

:::note warn
 \# 描画するグラフデータの生成
 scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species"<font color="Red">, "size"</font>]) 
 scatter = scatter.opts(color="species", cmap="Category10", legend_position="right", 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;width=550, height=400, fontscale=1.5, size=<font color="Red">"size", </font>
:::

ここまでの結果
&nbsp;&nbsp;↓
![12_withsize_holoviews.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/e942c820-33e8-e395-8d83-c994a3540742.jpeg)


## プルダウンで各軸に指定する項目を選べるようにする

<details><summary><font color="Darkblue">この章までで書くコード（クリックして表示）</font></summary>

```diff_python
# 初期化
df = initialize()

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value, color_ax, "size"])
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       )

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)
```

</details>

ここら辺から**panel**というライブラリを使用してインタラクティブ要素を追加していきます。
まず、プルダウンを追加してユーザーが自由に軸の項目を選択できるようにします。

### 準備

panelは、使用前にpn.extension()を実行する必要があります。

:::note warn
 \# Panelを使うためのおまじない
 <font color="Red">pn.extension()</font>
:::

### 軸の項目を選択するプルダウンの作成

panelを使用する準備ができたら、まず各軸を選択できるSelect（プルダウンで選択肢を選択させる）ウィジェットのインスタンスを`pn.widgets.Select()`で作成します。
`options`引数にリストを与えると、選択肢として表示されるようになります。
今回はシンプルにdf.columnsをリストに変換して渡しました（データ型を無視しているので項目によってはエラーがでます）

:::note warn
\# Selectウィジェットの定義
x_ax = <font color="Red">pn.widgets.Select(<font color="Darkblue">name= "X軸",</font> options=list(df.columns), <font color="Darkblue">width=150</font>)</font>
y_ax = <font color="Red">pn.widgets.Select(<font color="Darkblue">name= "Y軸",</font> options=list(df.columns), <font color="Darkblue">width=150</font>)</font>
size_ax = <font color="Red">pn.widgets.Select(<font color="Darkblue">name= "サイズ",</font> options=list(df.columns), <font color="Darkblue">width=150</font>)</font>
color_ax = <font color="Red">pn.widgets.Select(<font color="Darkblue">name= "カラー",</font> options=list(df.columns), <font color="Darkblue">width=150</font>)</font>
:::

ウィジェットの名前やサイズはお好みで。

インスタンスを生成出来たら、実際にウィジェットを配置してみます。
`pn.Column()`を使用すれば、縦方向にウィジェットを配置できます。

:::note warn
\# 表示
pn.Row(<font color="Red">pn.Column(x_ax, y_ax, size_ax, color_ax, </font>display_graph_but, <font color="Darkblue">margin=10</font><font color="Red">)</font>, graph_pane)
:::

今回、少し窮屈に感じたので`margin`を指定して間隔を空けています。

あと、試行錯誤を繰り返す中で毎回軸項目を選択しなおすのが面倒だったので、Selectの初期値を一つずつズらして別の項目が初期値になるように設定しています。

:::note warn
\# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
<font color="Darkblue">for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):</font>
&nbsp;&nbsp;<font color="Darkblue">widget.value = widget.options[min(i, len(widget.options))]</font>
:::

### グラフ表示ボタンの作成

次に、グラフを表示させるためのボタンを作っていきます。
まず、`pn.widgets.Button()`メソッドでButtonインスタンスを生成します。
`name`引数には、ボタンに表示させたいテキストを指定します。

:::note warn
\# グラフ表示用ボタンの定義
display_graph_but = <font color="Red">pn.widgets.Button(name="グラフ出力"<font color="Darkblue">, width=50, margin = [30, 10, 10, 10]</font>)</font>
:::

widthなどのボタンのサイズ指定はお好みで。
また、Selectウィジェットとも少し間隔を空けたいので、margin引数にリストを指定して少し細かくマージンを設定しています。
marginに与えるリストの中はHTMLと同様で[上、右、下、左]の順となります。0時スタートの時計回りと考えれば覚えやすいですかね。

表示はSelectウィジェットの下に配置するので、pn.Columnの中に生成したインスタンスを追加します。

:::note warn
\# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, <font color="Red">display_graph_but</font>, margin=10), graph_pane)
:::

### ボタンを押下したときの動作の指定

続いて、Buttonを押下したときの動作を指定していきます。
まず、`def`で引数に`event`を指定した関数を作ります。

:::note warn
\# グラフ表示用関数
<font color="Red">def display_graph(event):</font>
:::

その中で実際に動作させたい処理を書いていきます。
今回の場合、まず前項まで書いてきたグラフ描画のコードを移植します。
そこから、Selectウィジェットで選択されている値でグラフを描画するようにしたいので、列名を直接指定していた箇所を`Selectウィジェット.value`で選択されている項目を受け取れるようにします。
sizeは正規化の際にsize.valueを渡すので、optsメソッドのsize引数には固定の"size"を渡しておけばOKです。

:::note warn
\# グラフ表示用関数
def display_graph(event):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;\# サイズを規定する列を追加
&nbsp;&nbsp;&nbsp;&nbsp;def minmax_norm(df_input):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
&nbsp;&nbsp;&nbsp;&nbsp;df["size"] = (minmax_norm(df[<font color="Red">size_ax.value</font>]) + 1) * 5
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;\# 描画するグラフデータの生成
&nbsp;&nbsp;&nbsp;&nbsp;scatter = hv.Scatter(df, kdims=[<font color="Red">x_ax.value, </font>], vdims=[<font color="Red">y_ax.value, color_ax</font>, "size"])
&nbsp;&nbsp;&nbsp;&nbsp;scatter = scatter.opts(color=<font color="Red">color_ax.value</font>, cmap="Category10", legend_position="right", 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;width=550, height=400, fontscale=1.5, size=<font color="Darkblue">"size"</font>, 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;show_grid=True, title="Iris_data",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
:::

扱うscatterは関数外でも使用するため、`global`で宣言をしておきます。

:::note warn
\# グラフ表示用関数
def display_graph(event):
&nbsp;&nbsp;&nbsp;&nbsp;\# 関数外でも使用するグラフデータ+選択データをglobal宣言
&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">global scatter</font>
:::

処理をする関数をつくれたら、先程作ったButtonインスタンスに`.on_click()`メソッドを使って処理を登録します。

:::note warn
\# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力"<font color="Darkblue">, width=50, margin = [30, 10, 10, 10]</font>)
<font color="Red">display_graph_but.on_click(display_graph)</font>
:::

### グラフ表示領域の作成

次にグラフを表示させる領域を作っていきます。
HoloViewsグラフの表示領域を作るには、`pn.pane.HoloViews()`メソッドを使用します。

:::note warn
\# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
blank_hv = hv.Scatter((1, 1))
graph_pane = <font color="Red">pn.pane.HoloViews(</font>blank_hv, visible = False<font color="Red">)</font>
:::

しかし、これをvisible=False（見えない状態）にしても、HoloViewsインスタンスを与えていないとエラーになってしまいます。なので、適当なオブジェクトを指定することでエラーを回避しています。

:::note warn
\# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
<font color="Red">blank_hv = hv.Scatter((1, 1))</font>
graph_pane = pn.pane.HoloViews(<font color="Red">blank_hv</font>, visible = False)
:::

表示するパネルが用意できたら、先ほど作成したグラフ描画関数に、このパネルで表示するグラフを変更し、さらに非表示を解除する処理を加えます。

:::note warn
\# グラフ表示用関数
def display_graph(event):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;\# ウィジェットで描画するグラフデータの指定と表示
&nbsp;&nbsp;&nbsp;&nbsp;graph_pane<font color="Red">.object = scatter</font>
&nbsp;&nbsp;&nbsp;&nbsp;graph_pane<font color="Red">.visible = True</font>
:::

グラフ表示パネルは、`pn.Row()`を使用してこれまで作成してきたウィジェットの横に配置をします。
尚、pn.Rowとpn.Columnは組み合わせて使用可能です。

:::note warn
\# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), <font color="Red">graph_pane</font>)
:::

ここまでの結果（gif）
&nbsp;&nbsp;↓
![13_interactive_trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/a02e8ef0-fe51-24d9-e47c-1d3571bc93d1.gif)


## オンマウスでデータを表示する機能（ホバーツール）追加

<details><summary><font color="Darkblue">この章までで書くコード（クリックして表示）</font></summary>

```python
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更（列名に()が含まれるとホバーツールの表示おかしくなるため）
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# データ未入力だとウィジェットエラー出るので仮のグラフデータ格納
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS)]
                       )

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)
```

</details>

折角なので、オンマウスしたときに詳細情報が表示される機能（ホバーツール）を追加します。

まず、`bokeh.models`の中の`HoverTool`をインポートします。

:::note warn
\# ホバーツールのインポート
<font color="Red">from bokeh.models import HoverTool</font>
:::

次に、オンマウスしたときに表示させたい内容を記述します。
一行に表示させたい情報をタプルにし、それを表示させたい情報分リストにまとめて渡します。（今回はリスト内包表記で記述）
`@列名`で各要素における列名のデータを参照することができます。
ただ、@で参照する列名に一部の記号や日本語などが使用されている場合、列名を`{}`で囲う必要があるので注意が必要です。

:::note warn
\# ホバーツールの設定
<font color="Red">TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]</font>
:::

あとはグラフの.opts()メソッドの中で`tools`にリストで`HoverTool()`を指定します。
そして、そのHoverToolの引数として先ほど作成した表示させたい情報（今回の場合TOOLTIPS）を指定すればOKです。

:::note warn
&nbsp;&nbsp;&nbsp;&nbsp;scatter = scatter.opts(…
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">tools=[HoverTool(tooltips=TOOLTIPS)]</font>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
:::

もし@で参照した情報の一部が????となり上手く表示されない場合、以下のような処置で解決できる可能性があります。
・vdimsに参照する列名を追加する
@で参照する列はvdimsに追加する必要があるみたいなので、list(df.columns)としてdfのすべての列をvdimsに追加してしまいます。ただ、vdimsは先頭の要素が自動的にy軸の項目になるので、先にy_ax.valueを渡した後に他の列名を追加で渡しています。

:::note warn
&nbsp;&nbsp;&nbsp;&nbsp;\# 描画するグラフデータの生成
&nbsp;&nbsp;&nbsp;&nbsp;scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[<font color="Red">y_ax.value</font>] <font color="Red">+ list(df.columns)</font>)  # hoverで表示するデータはすべてvdimsに追加する必要あり</font>
:::

・列名で一部記号を削除
列名に()が使用されているとその列を参照したときに????と表示されてしまうみたいです。全角に置換してみたり、別の種類の括弧を使用してもダメでした。
今回は素直に括弧を削除して対応しました。

:::note warn
\# 列名変更（列名に()が含まれるとホバーツールの表示おかしくなるため）
<font color="Red">df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]</font>
:::

ここまでの結果
&nbsp;&nbsp;↓
![14_hovertool.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/29ab31af-faef-169a-96b3-93a8a9078c8d.jpeg)


## Selection1Dによる選択領域の取得

<details><summary><font color="Darkblue">この時点でのコード（クリックして表示）</font></summary>

```python
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更（列名に()が含まれるとホバーツールの表示おかしくなるため）
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)
```

</details>

ようやく、本投稿のタイトル回収ができる項目に来ました。
グラフ上をマウスドラッグで範囲指定し、その中に含まれるデータを参照していきます。

まず、グラフにボックス選択ツールを追加します。
先ほどHoverToolを追加した、`.opts`メソッドの`tools`の中に`box_select`を追加するだけでOK。
（投げ縄ツールを追加したい場合は`lasso_select`を追加します）

:::note warn
&nbsp;&nbsp;&nbsp;&nbsp;scatter = scatter.opts(…
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tools=[HoverTool(tooltips=TOOLTIPS)<font color="Red">, "box_select"</font>],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color="Darkblue">active_tools=["box_select"]</font>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
:::

また、`active_tools`にリストで`box_select`を追加しておけば、ボックス選択ツールが初期状態から自動でONにすることが出来ます。

各種選択ツールの準備が出来たら、ツールで選択した範囲の情報にアクセスできるようにします。
方法はシンプルです。グラフ描画処理の中で、`hv.streams.Selection1D`を定義し、引数`source`に参照するグラフオブジェクトを指定するだけです。

:::note warn
\# グラフ表示用関数
def display_graph(event):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;\# 選択された領域のデータを取得・格納
&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">selection = hv.streams.Selection1D(source=scatter)</font>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
:::

これでselectionに選択範囲の情報が格納されるようになりました。

試しに適当な範囲をツールで選択し、別セルでselectionのインデックスを表示させてみます。
Selection1Dオブジェクトのインデックスを参照したい場合、`.index`で参照可能です。

![15_box_select.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/af8747d6-d660-41c2-d176-ba71b138291b.jpeg)
&nbsp;&nbsp;↓
![16_preview_selection_index.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/dcd8cc11-5ef8-1ba4-2581-6717da9fbdab.jpeg)

## 選択データをリアルタイムで別パネルに出力

<details><summary><font color="Darkblue">完成したコード（クリックして表示）</font></summary>

```python
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更（列名に()が含まれるとホバーツールの表示おかしくなるため）
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定（すべて同じにせず一つずつずらす）
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義（データ未入力だとエラー出るので仮グラフデータ格納）
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        selection_info.value = sdf.query("index in @selected_index").drop("size", axis=1)
        selection_info.visible = True
        
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# 選択データ表示テーブルの定義
selection_info = pn.widgets.DataFrame(df, width=400, height=400, visible=False, row_height=25, autosize_mode="fit_viewport")

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, selection_info)
```

</details>

最後の仕上げです。
グラフ上で選択ツールによりデータが選択される度に、テーブルに詳細情報を表示・更新されるようにします。

まず、詳細情報を表示するテーブルを定義します。
テーブルには`pn.widgets.DataFrame()`を使用します。第一引数にDataFrameを与えればテーブルで表示することが出来ます。

:::note warn
\# 選択データ表示テーブルの定義
selection_info = <font color="Red">pn.widgets.DataFrame(df, <font color="Darkblue">width=400, height=400, </font>visible=False, <font color="Darkblue">row_height=25, autosize_mode="fit_viewport"</font>)</font>
:::

panelにはwidgetsとpaneにそれぞれDataFrameを表示するウィジェットが存在します。表示ならpaneのDataFrameを使用した方が良さそうなのですが、いかんせんこれが使いづらい。。。（列幅を自動調整できないなど）
なので、今回はwidgets.DataFrameを採用しました。

表示はこれまでと同様なので解説は割愛します。

:::note warn
\# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, <font color="Red">selection_info</font>)
:::

テーブルが用意できたら、selection.indexが更新されるたびに（=選択ツールで選択されるたびに）実行される関数を作成します。
何かの値の変化に反応して関数を実行させるには、`@pn.depends()`デコレータで関数をデコレートします。デコレータの第一引数には`監視されるオブジェクト.param.監視される変数`を指定します。
そしてその変化を常に監視したいので`watch=True`にします。

:::note warn
\# グラフ表示用関数
def display_graph(event):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:
&nbsp;&nbsp;&nbsp;&nbsp;\# 選択された領域が変わるたびに実行される関数
&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">@pn.depends(selection.param.index, watch=True)</font>
&nbsp;&nbsp;&nbsp;&nbsp;def selected(selected_index):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\# データフレームの更新と表示
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;：
:::

（この処理はselectionを参照するため、そのselectionが確実に定義されているグラフ描画関数の中で記述します（そうしないとエラーになる））

続いて、デコレートされる関数の中身を書いていきます。
関数宣言時の引数にはデコレータを書くときに指定した値（今回の場合は、選択されたデータのindex）を受け取る変数を指定します。
そして、dfをそのindexでフィルタし、不要な列（size列）を削除してから、先ほど作ったテーブルの`value`として指定します。
ついでにテーブルの非表示解除も同じタイミングで行なっておきます。

:::note warn
&nbsp;&nbsp;&nbsp;&nbsp;\# 選択された領域が変わるたびに実行される関数
&nbsp;&nbsp;&nbsp;&nbsp;@pn.depends(selection.param.index, watch=True)
&nbsp;&nbsp;&nbsp;&nbsp;def selected(<font color="Red">selected_index</font>):
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\# データフレームの更新と表示
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">selection_info.value = df.query("index in @&#xFFF0;selected_index").drop("size", axis=1)</font>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font color="Red">selection_info.visible = True</font>
:::

以上で完成です！
&nbsp;&nbsp;↓
![17_viewing_table.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/32acf996-6ec7-dbff-a039-992e0ea4c7ae.jpeg)

# 所感・今後

+ **HoloViewsもPanelも便利なので、もっと認知度上がってほしい**と思いもあって今回の記事を書きました。
+ 今回は散布図による例を作ってみましたが、ヒストグラムやヒートマップでも可能。<br>（気が向けばこちらも今後記事にしてみるかも）
+ qiitaで記事を書くとき、コード内で強調表示などをしたかったので、noteを乱用してみました。少しでも見やすくなっていれば幸いです。