BokehのHoverToolについて

ツールチップを表示するHoverToolについてのまとめ。

bokehのバージョンは2.4.2。jupyterlabのバージョンは3.1.4。

ホバーツールに関する公式ドキュメントは[こちら](https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#hovertool)と[こちら](https://docs.bokeh.org/en/latest/docs/reference/models/tools.html?highlight=hovertool#bokeh.models.HoverTool)。CustomJSについては[こちら](https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-hover-tool)。

```python
from bokeh.plotting import (curdoc, figure, show, output_notebook,
                            ColumnDataSource, row, column)
from bokeh.models import HoverTool, ResetTool, Select, CustomJS
from bokeh.palettes import Pastel1_9 as pastel

import pandas as pd

output_notebook()
```

# HoverToolの追加

- figure()の引数toolsで文字列'hover'かHoverToolインスタンスを渡す。  
- figure()に引数tooltipsを渡す。  
- Figureインスタンスのadd_toolsメソッドを使う。  


```python
x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]

p1 = figure(width=270, height=270, tools='hover,reset')
p1.circle(x=x, y=y, size=18)

p2 = figure(width=270, height=270, tools=[HoverTool(), ResetTool()])
p2.triangle(x=x, y=y, size=18)

p3 = figure(width=270, height=270, tools='')
p3.add_tools(HoverTool(), ResetTool())
p3.square(x=x, y=y, size=18)
          
show(row(p1, p2, p3))
```

![hover_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/db501d8b-214d-674e-58e8-09085b2b6156.png)

# ツールチップの中身をカスタマイズ

- figure()に引数tooltipsを渡す。
- HoverToolのコンストラクタに引数tooltipsを渡す。
- HoverToolインスタンスのtooltips属性を変更。

値は(label, value)のタプルのリストか文字列。文字列はhtml。ソースの列名の前に'@'をつけて対応するデータへの置き換えができる。他に'$'で始まる特殊なフィールド名がある。Noneにすると非表示。tooltipsのデフォルトは

```
[('index', '$index'),
 ('data (x, y)', '($x, $y)'),
 ('screen (x, y)', '($sx, $sy)')]
```

## 特殊なフィールド名

- \$index

    インデックス

- \$name

    グリフレンダラーのname属性の値。

- \\$x, \$y

    データ上のxとy座標。

- \\$sx, \$sy

    プロットの左上を(0, 0)とする画面上のxとy座標。

- \$color

    ColumnDataSource内の色データ表示。書式は`$color[オプション]:列名`。オプションは2つ。'hex'で16進数、'swatch'でカラーボックス表示。デフォルトの表示はrgba()形式。

- \$swatch

    ColumnDataSource内の色データをカラーボックスで表示。

```python
data = dict(
        x=[1, 2, 3],
        y1=[0, 0, 0],
        y2=[1, 1, 1],
        y3=[2, 2, 2],
        glyph_color=pastel[:3]
)
source = ColumnDataSource(data)

tooltips=[
    ('index', '$index'),
    ('name', '$name'),
    ('(x, y)', '($x, $y)'),
    ('(sx, sy)', '($sx, $sy)'),
    ('color', '$color:glyph_color'),
    ('color[options]', '$color[hex, swatch]:glyph_color'),
    ('swatch', '$swatch:glyph_color')
]

p = figure(
        width=400,
        height=400,
        tools='reset',
        tooltips=tooltips
)
p.x_range.range_padding = p.y_range.range_padding = 0.4

ys = ['y1', 'y2', 'y3']
markers = ['circle', 'triangle', 'square']
for y, marker in zip(ys, markers):
    p.scatter(
        x='x',
        y=y,
        size=58,
        marker=marker,
        color='glyph_color',
        name=marker,  # Bokehのモデルは名前をつけることができる。
        source=source
    )
    
show(p)
```

![hover_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/a4b9a4c9-e2c1-dd9d-1675-926d0c5392d7.png)

## データへの置き換え

`'@column_name'`のように頭に'@'をつける。日本語やスペースのある名前は'@{列名}'と'{}'で囲む。
    
'@$name'でレンダラーのnameを列名として参照する使い方ができる。


```python
data = {
    '年': ['2020年', '2021年', '2022年'],
    'y': [3, 4, 5],
    'bar color': pastel[:3]
}
source = ColumnDataSource(data)

tooltips = [
    ('年', '@{年}'),
    ('y', '@y'),
    ('bar color', '@{bar color}'),
    ('$color', '$color:{bar color}')  # $colorでは{}は使えない
]

p = figure(
        width=400,
        height=400,
        x_range=data['年'],
        tools='hover,reset',
        tooltips=tooltips
)

p.vbar(x='年', width=0.8, top='y', color='bar color', source=source)

show(p)
```

![hover_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/f3c7a0a1-7d91-0f4e-8dc6-cb4e5c502898.png)

```python
data = {
    'x': [1, 2, 3, 4, 5],
    '2020': [1, 2, 3, 4, 5],
    '2021': [10, 8, 6, 4, 2],
    '2022': [1, 2, 3, 4, 5],
}
source = ColumnDataSource(data)

tooltips='<span style="font-size:20px;">$name:@$name</span>'

p = figure(
        width=600,
        height=400,
        tools='hover,reset',
        tooltips=tooltips
)

stackers = ['2020', '2021', '2022']

# vbar_stackはstackersの数だけVBarを作成する。
p.vbar_stack(
    stackers=stackers,
    x='x',
    width=0.8,
    color=pastel[:3],
    source=source
)

# 各レンダラーに名前を設定
for renderer, name in zip(p.renderers, stackers):
    renderer.name = name

show(p)
```

![hover_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/38b33c80-6290-bdea-aae8-be25a10a8ba6.png)

## 書式指定

列名の後に'{}'で囲って書式指定できる。デフォルトは'{0,0}'でカンマが入る。

HoverToolのformatters属性でフォーマット指定子の種類を変更できる。'numeral', 'printf', 'datetime'から選択。デフォルトは'numeral'。書式については[公式](https://docs.bokeh.org/en/2.4.2/docs/reference/models/formatters.html)のNumeralTickFormatter, PrintfTickFormatter,DatetimeTickFormatterを参照。


```python
data = {
    'x': [1, 2, 3, 4, 5],
    'y': [3.33, 1.1234, 4.56789, 2, 5.6],
    'v': [0.2, 0.15, 0.1234, 0.266, 0.33333],
    '値': [9500, 184500, 580000, 1000000, 1200000],
    'color': pastel[:5]
}
source = ColumnDataSource(data)

p = figure(width=400, height=400, tools='hover,reset')

p.tools[0].tooltips = [
    ('x{0o}', '@x{0o}'),
    ('y{0.00}', '@y{0.00}'),
    ('y{0[.]00}', '@y{0[.]00}'),
    ('v{0.000%}', '@v{0.000%}'),
    ('{値}{0[.]0a}', '@{値}{0[.]0a}')
]

p.vbar(x='x', width=0.8, top='y', color='color', source=source)

show(p)
```

![hover_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/db5cab1c-7101-4865-3d3a-d798b9634777.png)

```python
data = {
    '日付': pd.date_range('2022/4/1', periods=5),
    'y': [3, 1, 4, 2, 5],
    'value': [1e+7, 2.5e+7, 1.556e+7, 2.552e+7, 3e+7]
}
source = ColumnDataSource(data)

tooltips = '''
<div>
  <h3>@{日付}{%Y年%m月%d日}</h3>
  <p style="font-size:16px;">
    yは@y{%05.2f}<br>
    valueは@value{%3.2e}です。
  </p>
</div>'''

hover = HoverTool(
            tooltips=tooltips,
            formatters={
                '@{日付}': 'datetime',
                '@y': 'printf',
                '@value': 'printf'
            },
)
            
p = figure(width=400, height=400, x_axis_type='datetime')
p.add_tools(hover)

p.line(x='日付', y='y', line_width=8, source=source)

show(p)
```

![hover_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/30efd1ec-1c52-699d-3b30-fbfb6ca31001.png)

# ツールチップの表示関連

## 表示位置

HoverToolのanchor属性で表示位置をグリフの中央、上下左右、四隅に指定。デフォルトは'center'。適用されるグリフは限られるっぽい。自分で動作確認したのはvbar, hbarとquad。

point_policy属性を'follow_mouse'にすればマウスポインタの位置になる。

line_policy属性で線のグリフの表示位置を指定。デフォルトは'nearest'。'interp'は線上を保ちつつ表示位置が動く。'none'はマウスポインタの位置。

mode属性でツールチップの対象となる範囲を広げられる。デフォルトは'mouse'でマウスポインタが重なるグリフが対象。'hline'でポインタの水平方向に重なるグリフ全て、'vline'で垂直方向に重なるグリフ全てが対象となる。


```python
x = [1, 2, 3, 4, 5]
y1 = [3, 1, 4, 2, 5]
y2 = [v - 0.3 for v in y1]

source=ColumnDataSource(dict(x=x, y1=y1, y2=y2, color=pastel[:5]))

# 各属性の値
options = {
    'anchor': [
        'top_left', 'top', 'top_right',
        'left', 'center', 'right',
        'bottom_left', 'bottom', 'bottom_right',
    ],
    'point_policy': ['snap_to_data', 'follow_mouse'], 
    'line_policy': ['nearest', 'prev', 'next', 'interp', 'none'],
    'mode': ['mouse', 'hline', 'vline'],
}

tooltips = [('index', '$index'), ('name', '$name')]

p = figure(width=600, height=400, tools='hover,reset', tooltips=tooltips)

p.vbar(
    x='x',
    width=0.8,
    top='y1',
    color='color',
    name='棒',
    source=source
)
p.line(
    x='x',
    y='y2',
    line_width=8,
    name='線',
    source=source
)

# ウィジェットを作成してその'value'属性とホバーツールの各属性を連動させる。
selects = []
for attr, opt in options.items():    
    s = Select(options=opt, width=140, title=attr)
    s.js_link('value', p.tools[0], attr)
    
    selects.append(s)

selects[0].value = 'center'

l = column(row(*selects), p, background='#eeeeee')
    
show(l)
```

![hover_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/6903a811-79db-e628-c4b4-f2e66fb41df7.png)

attachment属性で表示ポイントの上下左右どこに表示するかを指定。'horizontal', 'vertical', 'above', 'below', 'left', 'right'。デフォルは'horizontal'。'vertical', 'horizontal'は上下、左右で自動。

show_arrow属性をFalseにするとツールチップの矢印が消える。何故かattachmentを上下にするとFalseにしても矢印が消えない。

この2つの属性はインタラクティブな変更ができなかった。


```python
x = [1, 2, 3, 4, 5]
y = [3, 1, 4, 2, 5]

p = figure(width=400, height=400, tools='hover')
p.tools[0].attachment = 'vertical'
p.tools[0].point_policy = 'follow_mouse'

p.vbar(x=x, width=0.8, top=y)

show(p)
```

![hover_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/2ff85211-c15e-1509-b1fd-50dee012cfe4.png)

## 表示対象を指定

HoverToolのrenderers属性で表示対象となるレンダラーを指定。デフォルトは'auto'で全て。

names属性だとレンダラーのnameのリストで指定できる。


```python
data = dict(
        x=[1, 2, 3, 4, 5],
        y=[3, 1, 4, 2, 5],
        color=pastel[:5],
)
source = ColumnDataSource(data)

p = figure(
        width=350,
        height=350,
        title='renderersで',
        tools='hover',
        tooltips=[('name', '$name'), ('x', '@x'), ('y', '@y')]
)

# ソースを共有してrenderersを指定すると
# ツールチップを表示した時に線のグリフが消える現象が起こるため
# lineだけdataのまま渡して別のソースを作成している。
p.vbar('x', 0.8, 'y', color='color', source=source)
p.line(x='x', y='y', line_width=8, source=data)
circle = p.circle('x', 'y', size=20, fill_color='white', name='円', source=source)

p.tools[0].renderers = [circle]

p2 = figure(
        width=350,
        height=350,
        title='namesで',
        tools='hover',
        tooltips=[('name', '$name'), ('x', '@x'), ('y', '@y')]
)

p2.vbar('x', 0.8, 'y', color='color', source=source)
p2.line(x='x', y='y', line_width=8, source=data)
p2.circle('x', 'y', size=20, fill_color='white', name='円', source=source)

p2.tools[0].names = ['円']

show(row(p, p2))
```

![hover_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/eace4aa2-39c1-f34f-a2a5-fd4cd8e1134b.png)

## ホバーツールアイコンの説明文と非表示

description属性でアイコンをホバーした時の説明文。

toggleable属性をFalseにするとアイコン非表示。


```python
x = [1, 2, 3, 4, 5]
y = [3, 1, 4, 2, 5]

p = figure(width=350, height=350, tools='hover')
p.line(x=x, y=y, line_width=8)
p.tools[0].description = '変更した説明文'

p2 = figure(width=350, height=350, tools='hover')
p2.line(x=x, y=y, line_width=8)
p2.tools[0].toggleable = False

show(row(p, p2))
```

![hover_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/ef45ad39-8692-35de-9c86-66608cafece2.png)

# JavaScriptを使って動作をカスタマイズ

callback属性にCustomJSインスタンスを渡す。CustomJSの引数codeにブラウザで実行するJavaScriptのコード。argsにJavaScript内で使いたいbokehのモデルなどを値にした辞書。キーが変数名になる。


```python
# マウスポインタを上の長方形に重ねると対応するグラフが表示されるようにする

p = figure(
        width=600,
        height=400,
        y_range=(0, 8),
        tools='hover,reset'
)

rect = p.rect(
        x=[1, 3, 5],
        y=[7, 7, 7],
        width=1,
        height=1,
        color=pastel[:3]
)

# vbar用のデータ
vbar_data = [
    {'x': [1, 2, 3, 4, 5],
     'y': [3, 1, 4, 2, 5]},
    {'x': [1, 2, 3, 4, 5],
     'y': [5, 3, 1, 3, 5]},
    {'x': [1, 2, 3, 4, 5],
     'y': [1, 2, 3, 4, 5]}
]
# 最初は空データのソース
vbar_source=ColumnDataSource(dict(x=[], y=[]))

# JavaScript内でhover_glyph属性を参照するため、適当な値を設定してhover_glyphを生成している
# 生成しないとnullになってしまう
vbar = p.vbar(
        x='x',
        width=0.8,
        top='y',
        hover_color=None,
        source=vbar_source
)

# vbar用ホバーツールを追加
hover = HoverTool(
            description='vbarのホバー',
            tooltips=[('x', '@x'), ('y', '@y')],
            renderers=[vbar]
)
p.add_tools(hover)

# CustomJS
args = dict(
    colors=pastel[:3],
    vbar=vbar,
    vbar_source=vbar_source,
    vbar_data=vbar_data,
)

# 変数cb_dataにホバーしているグリフのインデックスが入っている。
code = '''
if (cb_data.index.indices.length > 0) {
    let idx = cb_data.index.indices[0];
    let color = colors[idx];
    vbar_source.data = vbar_data[idx];
    vbar_source.change.emit();
    
    vbar.glyph.fill_color = color;
    vbar.glyph.line_color = color;
    vbar.hover_glyph.fill_color = color;
    vbar.hover_glyph.line_color = color;
}
'''

# tooltipsをNoneにして従来のツールチップは非表示。
p.tools[0].tooltips = None
p.tools[0].callback = CustomJS(code=code, args=args)
p.tools[0].renderers = [rect]

show(p)
```

![hover_02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/eb3bf742-d6cb-0039-a52b-2d39881b4e6b.gif)

# CSSを使って見た目をカスタマイズ

CSSを書き込んだテンプレートを使ってツールチップの見た目を変更する。テンプレートはJinja2のTemplateを使う。テンプレートを適用するにはサーバーを使うか一旦htmlファイルに出力してブラウザで開く。

サーバーを使う場合はcurdoc().template属性でテンプレートを適用する。テンプレート用の変数はcurdoc().template_variablesの辞書を更新する。キーが変数名になる。

ファイルに出力する場合はfile_html()を使う。テンプレートは引数template、変数は引数template_variablesで渡す。

ベースとなるデフォルトのテンプレートは[こちら](https://docs.bokeh.org/en/2.4.2/docs/user_guide/embed.html#standard-template)。

以下はサーバーを使うのとファイル出力の二つのコード。


```python
# サーバーを使う例

# 1.コピペして.pyファイルを作成
# 2.コマンドラインでファイルを保存したディレクトリに移動。
# 3.`bokeh serve ファイル名.py --show`を実行。

from bokeh.plotting import curdoc, figure, ColumnDataSource
from bokeh.models import HoverTool
from bokeh.palettes import Pastel1_9 as pastel

from jinja2 import Template

def bkapp():
    # テンプレート作成
    template = Template('''\
    {% extends base %}
    
    {% block postamble %}
      <style>
      .bk-root .bk-tooltip {
          font-size: 18px;
          background-color: lightblue;
          border-radius: 5px;
          padding: 10px;
      }
      </style>
    {% endblock %}
    
    {% block contents %}
      <h1>{{ text }}</h1>
      {{ super() }}
    {% endblock %}
    '''
    )
    template_variables = {'text': 'CSSを使った例'}
    
    curdoc().template = template
    curdoc().template_variables.update(template_variables)

    # グラフ作成
    x = [1, 2, 3, 4, 5]
    y = [3, 1, 4, 2, 5]
    source = ColumnDataSource(dict(x=x, y=y, color=pastel[:5]))

    hover = HoverTool(
                tooltips='@x, @y',
                point_policy='follow_mouse',
                show_arrow=False
    )

    p = figure(tools=[hover])
    
    p.vbar(x='x', width=0.8, top='y', color='color', source=source)
    
    curdoc().add_root(p)

bkapp()
```


```python
# ファイル出力の例
# 変数pathを保存するhtmlファイルのパスに変更して実行

from bokeh.embed import file_html
from jinja2 import Template
import webbrowser

path = '.html'

# テンプレート作成
template = Template('''\
{% extends base %}

{% block postamble %}
  <style>
  .bk-root .bk-tooltip {
      font-size: 18px;
      background-color: lightblue;
      border-radius: 5px;
      padding: 10px;
  }
  </style>
{% endblock %}

{% block contents %}
  <h1>{{ text }}</h1>
  {{ super() }}
{% endblock %}
'''
)
template_variables = {'text': 'CSSを使った例'}

# グラフ作成
x = [1, 2, 3, 4, 5]
y = [3, 1, 4, 2, 5]
source = ColumnDataSource(dict(x=x, y=y, color=pastel[:5]))

hover = HoverTool(
            tooltips='@x, @y',
            point_policy='follow_mouse',
            show_arrow=False
)

p = figure(tools=[hover])

p.vbar(x='x', width=0.8, top='y', color='color', source=source)

# htmlを出力 
html = file_html(
            p,
            resources='cdn',
            template=template,
            template_variables=template_variables,
            title='hover toolについて'
)
# 保存してブラウザで開く
with open(path, 'w', encoding='utf-8') as f:
    f.write(html)
    
webbrowser.open(path)
```

![hover_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/ddb3314d-c421-e645-e892-f3ecf9f9c6ac.png)

## 追加:別のやり方とバージョン3

tooltipsに渡すhtmlにstyleタグを書く。簡単だしjupyterでも適用される。
バージョン3ではテンプレートを使ったやり方はできないようで、この方法しかわからなかった。2と3ではCSSのセレクタの書き方が異なる。

```python
tooltips = '''\
<div>
  <style>
    .bk-root .bk-tooltip {
        font-size: 18px;
        background-color: lightblue;
        border-radius: 5px;
        padding: 10px;
    }
  </style>
  <div>@x, @y</div>
</div>
'''

tooltips_ver3 = '''\
<div>
  <style>
    :host {
        font-size: 18px;
        background-color: lightblue;
        border-radius: 5px;
        padding: 10px;
    }
  </style>
  <div>@x, @y</div>
</div>
'''

x = [1, 2, 3, 4, 5]
y = [3, 1, 4, 2, 5]
source = ColumnDataSource(dict(x=x, y=y, color=pastel[:5]))

p = figure(tooltips=tooltips)

p.vbar(x='x', width=0.8, top='y', color='color', source=source)

show(p)
```

![hover_06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/2de194c3-f222-1d33-ac7e-d6e5bd6e9874.jpeg)

バージョン3では矢印の色や大きさも変更できるっぽい。既存のクラス名や変数を使ってもう少しいじった例。bk-で始まるのが既存のクラス名。

```python
tooltips = '''\
<div>
  <style>
    :host {
      --tooltip-arrow-color: firebrick;
      --tooltip-arrow-width: 20px;
      --tooltip-arrow-height: 20px;
      --tooltip-arrow-half-width: 5px;
      --tooltip-arrow-half-height: 5px;
      font-size: 18px;
      background-color: lightblue;
      border-radius: 5px;
      padding: 10px;
    }
    .bk-tooltip-content > div > div:not(:first-child) {
      margin-top: 10px;
      padding-top: 10px;
      border-top: 1px solid grey;
    }
  </style>
  <span class="bk-tooltip-row-label">(x, y):</span>
  <span class="bk-tooltip-row-value">@x, @y</span>
</div>'''

data = dict(
    x=[1, 2, 3],
    y=[4, 5, 6],
)
p = figure(tooltips=tooltips)
p.x_range.range_padding = p.y_range.range_padding = 0.2

p.circle(source=data, color='lightgreen', alpha=0.6, radius=3)

show(p)
```

![hover_07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/cb28ee36-2bb0-00e0-988a-619656add5ad.jpeg)


JavaScriptとCSSの知識があればもっと色々できるんだろうなと思いつつ終わり。