Hover Tool dynamic fields

Hello!

Is it possible to somehow make fields in HoverTool disappear depending on the data inside?

Here is some example code

from bokeh.layouts import column
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Plot

import numpy as np

data = {
	"x": [1,2,3],
	"y": [1,2,3],
	"flag": [True, False, False],
	"extra_info": ["abc", None, None],
}

TOOLTIPS = [
    ("x", "@x"),
    ("y", "@y"),
    ("extra_info", "@extra_info"),
]

p = figure(width=300, height=300, tools="pan,reset,save,hover", tooltips=TOOLTIPS)
source = ColumnDataSource(data=data)
p.line(source=source)

doc = curdoc()
doc.add_root(column(p))

The hover tool in this example will show abc under extra_info for the first data point. For the second and third point it will show ??? under extra_info.

My aim would be to make the extra_info not appear at all for the points where it has no value. As you can see there is also a flag which correlates with the extra_info. Ideally, I would like to write custom JS that will decide what fields to show depending on the data inside. Something like:

if (point.flag) {
  tooltips = [
    ("x", point.x),
    ("y", point.y),
    ("extra_info", point.extra_info)
  ]
} else {
  tooltips = [
    ("x", point.x),
    ("y", point.y),
  ]
}

I can see that it is possible to create CustomJs Hover Tool as presented here JavaScript callbacks — Bokeh 2.4.0 Documentation but I have no idea what kind of attributes should I access to achieve the desired result. Would it be possible for some Bokeh pro to help with an example solution?

Here’s a way to do it with CustomJS. Check out the comments in the code for explanation:

from bokeh.plotting import figure, show, save
from bokeh.models import ColumnDataSource, HoverTool, CustomJSHover, CustomJS

src = ColumnDataSource(data={'x':[0,1,2],'y':[2,3,1],'Flag':['ShowMe','ShowMe','HideMe']})


f = figure()
r = f.scatter(x='x',y='y',source=src)

#instantiate a hovertool, pointing to the renderers we want
hvr = HoverTool(renderers=[r])  #but not spec'ing any tooltips for now

#customjs attached to the callback:
cb = CustomJS(args=dict(hvr=hvr, src=src) #pass in both the CDS and the HoverTool
              ,code='''
              //src.inspected.indices reports the indices of the source you are hovered over
              if (src.inspected.indices.length>0){
                      //if the 'Flag' field at the first inspected index == something we want to show, then update the HoverTools tooltips
                     if (src.data['Flag'][src.inspected.indices[0]] == 'ShowMe'){
                             hvr.tooltips = [['','@Flag']]
                             }
                     //otherwise set it to null
                     else {
                         hvr.tooltips = null
                           }
                      }
              ''')
hvr.callback = cb #instructs this callback to trigger whenever the hoverTool function is called
f.add_tools(hvr)

show(f)

conditionalhover

There is probably also a way to do this with a custom html tooltip as well that may offer other advantages/power… but for me at least that’s a lot more involved than this.

2 Likes

WoW this is so amazing. Simple, works great and it’s greatly extensible! Thank you!

Would it be possible for you to share how did you find the parameters? I find it very hard to use browser developer tools to investigate my CustomJS code. Do you just read the source code of bokeh and write the code that will hopefully work or are you using some special tool that makes it easy to experiment with CustomJS code? I am thinking about something similar to the features IDEs give you with autocompletion and being able to jump to the source code.

Thanks!

I find it very hard to use browser developer tools to investigate my CustomJS code.

Me too honestly. I’ve just gotten good at making MREs and/or accepting of the time investiture. All I do is make a dummy example (like above), pass the bokeh model i’m interested in to the callback, and console.log the model instance and check the browser, then try stuff until it works.

The other tip is that in 99% of cases, if its an attribute of the python class, then it’s also an attribute on the JS side… and when it’s not/it’s slightly different etc, it’s because there’s some really important extra nuance/reason for it. Now where it gets interesting is how bokeh “translates” these attributes, like in the example above I had to figure out that on the python side, you supply a list of tuples to the tooltips arg, but on the js side, you supply an array of arrays. I figured that out by console.logging the hovertool and drilling down into it to see what the tooltips attribute looked like.

I can say with complete honesty that I can barely read the TS source code.

Sorry, got a final improvement on this. You can avoid the if statements in the CustomJS entirely by adding a “hoverkey” to the CDS, and then also passing a corresponding tooltip_dictionary to the CustomJS too. This gives you a lot more control:

from bokeh.plotting import figure, show, save
from bokeh.models import ColumnDataSource, HoverTool, CustomJSHover, CustomJS

src = ColumnDataSource(data={'x':[0,1,2],'y':[2,3,1],'Fruit':['Banana','Orange','Apple'],'Veggie':['Brocolli','Spinach','Corn'],'hoverkey':['Fruitx','Fruitx','Veggiey']})

#make a tooltip dictionary based on hoverkey
tt_dict = {'Fruitx':[['Fruit:','@Fruit'],['X','@x']]
           ,'Veggiey':[['Veggie:','@Veggie'],['Y','@y']]
           }

f = figure()
r = f.scatter(x='x',y='y',source=src)

#instantiate a hovertool, pointing to the renderers we want
hvr = HoverTool(renderers=[r])  #but not spec'ing any tooltips for now

#customjs attached to the callback:
cb = CustomJS(args=dict(hvr=hvr, src=src, tt_dict=tt_dict) #pass in both the CDS and the HoverTool and the tt_dict
              ,code='''
              //src.inspected.indices reports the indices of the source you are hovered over
              if (src.inspected.indices.length>0){
                      //get the hoverkey for the hovered item
                      const k = src.data['hoverkey'][src.inspected.indices[0]]
                      //use tt_dict to set the tooltips to that key's value in tt_dict
                      hvr.tooltips = tt_dict[k]
                      }
                      
              ''')
hvr.callback = cb #instructs this callback to trigger whenever the hoverTool function is called
f.add_tools(hvr)

show(f)