☰Read-only archive⌕
Looking for help with Bokeh?

This is a read-only archive. Ask questions and join the conversation on GitHub.

Visit Community Support on GitHub →
Archived discussions · Captured October 28, 2026

How to use CustomJSHover to add tooltips with different formatters

Community Supporthover
cirnod
cirnod

I would like to plot a time series (x-axis: absolute time, y-axis: data) and provide in the hover tooltip the absolute and relative time. The data is provided as a ColumnDataSource to the plot. I do not want to add an additional series for the relative time to the ColumnDataSource since this represents an unnecessary data duplication. I’m looking for a way to add a “derived” series with

relative_time = absolute_time - offset 

to the hover tooltip. CustomJSHover provides a way to add a derived hover tooltip. However, so far I could not manage to add a tooltip for the same series from the ColumnDataSource but with 2 different formatters.

Minimal example:

from bokeh.plotting import figure, output_file, show
from bokeh.models.glyphs import Line
from bokeh.models import ColumnDataSource, HoverTool, CustomJSHover

x = [10, 10, 13, 14, 15]
y = [6, 7, 2, 4, 5]
source = ColumnDataSource(dict(x=x, y=y))

output_file("lines.html")

p = figure(x_axis_label='x', y_axis_label='y')
l = Line(x='x', y='y')
p.add_glyph(source, l)

offset = 10
x_custom = CustomJSHover(
    args=dict(offsetSource=ColumnDataSource(dict(offset=[offset]))),
    code="""
        return "" + (special_vars.data_x - offsetSource.data.offset[0]);
""")

p.add_tools(HoverTool(
    tooltips=[
        ( 'absolute_time','@x{0.000}' ),
        ( 'relative_time','@x{custom}' ),
    ],
    formatters={"@x": x_custom}
))

show(p)

The result is a hover tooltip like this:

absolute_time: 4
relative_time: 4

I would like to get something like this:

absolute_time: 14.000
relative_time: 4.000

Is there a way to achieve this with CustomJSHover or is this use case currently not supported?

⌁   Link to post
p-himik
p-himik

You JS code is just fine. It just doesn’t do what you want it to do.
Apart from special_vars, there’s also the format variable (that will have values "0.000" and "custom" in your case) - you will have to inspect it and add some condition on it.
To help with different formats like 0.000, you can call Bokeh.require('@bokehjs/core/util/templating').DEFAULT_FORMATTERS.numeral - it’s a function of (value, format, special_vars) that will format the value just like Bokeh does without any custom formatters.

⌁   Link to post
cirnod
cirnod

Thank you for your answer! With your information, I now managed to add a second shifted series to the hover tooltip derived from an existing series.

For later reference, I add the adapted minimal example here:

from bokeh.plotting import figure, output_file, show
from bokeh.models.glyphs import Line
from bokeh.models import ColumnDataSource, HoverTool, CustomJSHover

x = [11, 12, 13, 14, 15]
y = [6, 7, 2, 4, 5]
source = ColumnDataSource(dict(x=x, y=y))

output_file("lines.html")

p = figure(x_axis_label='x', y_axis_label='y')
l = Line(x='x', y='y')
p.add_glyph(source, l)

offset = -10
x_custom = CustomJSHover(
    args=dict(offsetSource=ColumnDataSource(dict(offset=[offset]))),
    code="""
        var numFormatter = Bokeh.require('@bokehjs/core/util/templating').DEFAULT_FORMATTERS.numeral;
        var formatSplit = format.split(':');
        if (formatSplit.length == 2 && formatSplit[0] == 'withOffset') {
            return numFormatter(special_vars.data_x + offsetSource.data.offset[0], formatSplit[1], special_vars);
        } else {
            return numFormatter(special_vars.data_x, format, special_vars)
        }
""")

p.add_tools(HoverTool(
    tooltips=[
        ( 'absolute_time','@x{0.000}' ),
        ( 'relative_time','@x{withOffset:0.000}' ),
    ],
    formatters={"@x": x_custom}
))

show(p)

A note on the use of Bokeh.require() from the Bokeh 2.0.0 release notes:

The require() function [i]s not available from CustomJS anymore. It exposed the underlying module system, which should not be used by general users. Use APIs exposed on Bokeh object or (as a last resort), use Bokeh.require() .

⌁   Link to post

End of archived discussion · Replies are disabled