How to create a conditional custom hover tool for a holoviews plot

Hover tools are very useful for interactive visualization. However, without customization, those may be limited to just visualizing the data series that built the plot. I wish to be able to add in the hover tool elements such as adding units of measure or converting units or conditional elements such as showing text that is conditional upon the value.

For example, the code below shows the simple example of a sine wave the user can control. I added a hover tool that shows the phase and frequency.

import holoviews as hv
import numpy as np
import panel as pn
import bokeh
from bokeh.resources import INLINE
from bokeh.models import HoverTool
from bokeh.models import CustomJSHover

pn.extension(safe_embed=True)
for library in [hv,pn,bokeh]:
    print ("%s version %s"% ( library, library.__version__))

hv.extension('bokeh') 

frequencies = [0.5, 0.75, 1.0, 1.25]

MyHover1 = HoverTool(
    tooltips=[
        ( 'xvals', '@xvals'),
        ( 'yvals', '@yvals'),
        ( 'phase', '@phase'),
        ( 'freq', '@freq'),
   ],
    formatters={
        'xvals' : 'numeral',
        'yvals' : 'numeral',
        'phase' : 'numeral',
        'freq' : 'numeral',
    },
    point_policy="follow_mouse"
)


def sine_curve(phase, freq):
    xvals = [0.1* i for i in range(100)]
    data = {
        'xvals' : xvals,
        'yvals' : [np.sin(phase+freq*x) for x in xvals],
        'phase' : [phase for x in xvals],
        'freq' : [freq for x in xvals],
    }
    plot =  hv.Points( data, kdims = ['xvals','yvals'], vdims = ['phase','freq'])
    return plot

phases      = [0, np.pi/2, np.pi, 3*np.pi/2]
curve_dict_2D = {(p,f):sine_curve(p,f) for p in phases for f in frequencies}
hmap = hv.HoloMap(curve_dict_2D, kdims=['phase', 'freq']).opts(tools = [MyHover1])

panel_Object = pn.pane.HoloViews(hmap)
panel_Object.save('SineHover.html', resources=INLINE, title = 'Hover Example', embed=True)

Here is the resulting image when hovering:

However, notice that Phase is multiples of PI - I wish the user to know this when the hovertool is used . Also for frequencies higher that 1, I wish the user to be notified with the text ‘High’ to indicate a high frequency rather than a number . The former corresponds for unit change and the latter is a conditional statement that is useful in many cases such as indicating infinity for very larger numbers.

There is some information on creating hover tools in those links 1 , 2 , 3 . However, I could not locate a complete example showing complex custom hover tool, and therefore I started this topic.

Here is a solution to this problem. First the complete code:

import holoviews as hv
import numpy as np
import panel as pn
import bokeh
from bokeh.resources import INLINE
from bokeh.models import HoverTool
from bokeh.models import CustomJSHover

pn.extension(safe_embed=True)
for library in [hv,pn,bokeh]:
    print ("%s version %s"% ( library, library.__version__))


hv.extension('bokeh') 

frequencies = [0.5, 0.75, 1.0, 1.25]

MyCustomFreq = CustomJSHover(code='''
        var value;
        var modified;
        if (value>1) {
            modified = "High";
        } else {
            modified = value.toString();
        }
        return modified
''')

MyCustomPhase = CustomJSHover(code='''
        var value;
        var modified;
        modified = value / Math.PI;
        return modified.toString() + " * PI ";
''')

MyHover1 = HoverTool(
    tooltips=[
        ( 'xvals', '@xvals'),
        ( 'yvals', '@yvals'),
        ( 'phase', '@phase{custom}'),
        ( 'freq', '@freq{custom}'),
   ],
    formatters={
        'xvals' : 'numeral',
        'yvals' : 'numeral',
        '@phase' : MyCustomPhase,
        '@freq' : MyCustomFreq,
    },
    point_policy="follow_mouse"
)

def sine_curve(phase, freq):
    xvals = [0.1* i for i in range(100)]
    data = {
        'xvals' : xvals,
        'yvals' : [np.sin(phase+freq*x) for x in xvals],
        'phase' : [phase for x in xvals],
        'freq' : [freq for x in xvals],
    }
    plot =  hv.Points( data, kdims = ['xvals','yvals'], vdims = ['phase','freq'])
    return plot


phases      = [0, np.pi/2, np.pi, 3*np.pi/2]
curve_dict_2D = {(p,f):sine_curve(p,f) for p in phases for f in frequencies}
hmap = hv.HoloMap(curve_dict_2D, kdims=['phase', 'freq']).opts(tools = [MyHover1])

panel_Object = pn.pane.HoloViews(hmap)
panel_Object.save('SineHover.html', resources=INLINE, title = 'Hover Example', embed=True)

hv.save(hmap,'test_holoviews.html')

Here is how hovering looks like now:


Notice that the Phase shows multiples of PI and for frequency higher than 1 Freq shows “High” otherwise it shows the frequency number.

Here are some instructions and tips to construct this properly:

  1. In the hover tool definition a few changes are important:

    • In the toot tip section it is important to point ot the correct formatter followed by {custom} , e.g. '@freq{custom}'
    • The formatter name should match the variable name and now has @ before its name, e.g. '@freq'
    • The formatter now points to a custom python function the user creates, e.g. @freq' : MyCustomFreq
  2. The custom function requires attention to the following:

    • It has a code argument that receives a string that contains JavaScript code - not python
    • The JavaScript code should return a string
    • Variables should be defined with var, e.g. var value;
    • The variable value holds the point value provided by the python code. e.g. value is freq for @freq . There are additional special names that can be found here under class CustomJSHover(**kwargs)
  3. In case of an error in definitions or in the JavaScript code, there may be no warning or error. Instead possible behaviors are:

    • There will be no change and the user will see the numbers provided by freq or phase
    • The hover tool may disappear

Using custom hover tool is very powerful and can allow some powerful capabilities as shown here, yet the basics above should be followed to use those properly.