Help: Cannot get HoverTool to Work on Server App

Hey everyone,

I am trying to add the HoverTool capability to my Bokeh Server Application.

I am trying to create a plot (similar to the crossfilter.py server app example) and enable the hover tool to let the user know additional information about each point based on the source.

I have used the hovertool in the pass for some standalone bokeh plots but they required having to set “source” and x/y had to be strings. Does this mean its not possible to use hover tool when the plot becomes an interactive dashboard?

Thanks.

Hi @chelsey1225 It’s definitely possible to use a hover tool in Bokeh server applications, and there is no difference in how hover tools are used or configured in standalone output vs Bokeh server apps. See e.g. the “movies” example:

Movies

It’s not really possible to say much else with your question, as given. For the best chance of getting specific, actionable help, its always advised to ask questions around specific, minimal examples of actual code that you are using or trying.

Here is my code:

I cannot get the hoover tool to show column information (i.e. Sepal.Length, Variety. etc.)

Hi Chelsey,

You haven’t configured the tooltip to show the custom column information. Since you’re only using hover in your figure tools, by default you’d get

("index", "$index"),
("data (x,y)", "($x, $y)"),
("screen (x,y)", "($sx, $sy)")

In your case, you’d need to define a custom tooltip to show column names and the values associated with the columns. Also I believe custom tooltips work only on ColumnDataSource and not on individual variables since for each hover, the data has to get updated based on the value on the plot. So I’d also modify your data to a CDS.

Here are the modifications:

from bokeh.models import ColumnDataSource

TOOLTIPS = [
    ("x-axis", x_title),
    ("y-axis", y_title),
    ("x-value", "$xs"),
    ("y-value", "$ys"),
    ("color", "@c"),
    ("size", "@sz")
]

# $ - plot related fields fields, @ - fields in data source

#Create CDS for the data
source = ColumnDataSource(data=dict(xs = xs, ys = ys, c = c, sz=sz))

#Add TOOLTIPS to figure

p = figure(plot_height=600, plot_width=800, tools="pan, box_zoom, hover, reset", tooltips = TOOLTIPS, **kw)

p.circle(x='xs', y='ys', color='c', size = 'sz', source = source)

This should make the tooltips show column information. You can look at other options to customize tooltips here

https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#hovertool

Also, next time please type/copy-paste the code into a code block instead of a screenshot. It’s much easier that way :slight_smile:

Thanks! Sorry for the snapshot. I have updated my code and I am running into a Error…

My error:

 Error running application handler <bokeh.application.handlers.directory.DirectoryHandler object at 0x0000019CD8C973C8>: expected an element of ColumnData(St                                   ring, Seq(Any)), got {'xs': array([0.2, 0.2, 0.2, 0.2, 0.2, 0.4, 0.3, 0.2, 0.2, 0.1, 0.2, 0.2, 0.1,
       0.1, 0.2, 0.4, 0.4, 0.3, 0.3, 0.3, 0.2, 0.4, 0.2, 0.5, 0.2, 0.2,
       0.4, 0.2, 0.2, 0.2, 0.2, 0.4, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.2,
       0.2, 0.3, 0.3, 0.2, 0.6, 0.4, 0.3, 0.2, 0.2, 0.2, 0.2, 1.4, 1.5,
       1.5, 1.3, 1.5, 1.3, 1.6, 1. , 1.3, 1.4, 1. , 1.5, 1. , 1.4, 1.3,
       1.4, 1.5, 1. , 1.5, 1.1, 1.8, 1.3, 1.5, 1.2, 1.3, 1.4, 1.4, 1.7,
       1.5, 1. , 1.1, 1. , 1.2, 1.6, 1.5, 1.6, 1.5, 1.3, 1.3, 1.3, 1.2,
       1.4, 1.2, 1. , 1.3, 1.2, 1.3, 1.3, 1.1, 1.3, 2.5, 1.9, 2.1, 1.8,
       2.2, 2.1, 1.7, 1.8, 1.8, 2.5, 2. , 1.9, 2.1, 2. , 2.4, 2.3, 1.8,
       2.2, 2.3, 1.5, 2.3, 2. , 2. , 1.8, 2.1, 1.8, 1.8, 1.8, 2.1, 1.6,
       1.9, 2. , 2.2, 1.5, 1.4, 2.3, 2.4, 1.8, 1.8, 2.1, 2.4, 2.3, 1.9,
       2.3, 2.5, 2.3, 1.9, 2. , 2.3, 1.8]), 'ys': array([1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5, 1.4, 1.5, 1.5, 1.6, 1.4,
       1.1, 1.2, 1.5, 1.3, 1.4, 1.7, 1.5, 1.7, 1.5, 1. , 1.7, 1.9, 1.6,
       1.6, 1.5, 1.4, 1.6, 1.6, 1.5, 1.5, 1.4, 1.5, 1.2, 1.3, 1.4, 1.3,
       1.5, 1.3, 1.3, 1.3, 1.6, 1.9, 1.4, 1.6, 1.4, 1.5, 1.4, 4.7, 4.5,
       4.9, 4. , 4.6, 4.5, 4.7, 3.3, 4.6, 3.9, 3.5, 4.2, 4. , 4.7, 3.6,
       4.4, 4.5, 4.1, 4.5, 3.9, 4.8, 4. , 4.9, 4.7, 4.3, 4.4, 4.8, 5. ,
       4.5, 3.5, 3.8, 3.7, 3.9, 5.1, 4.5, 4.5, 4.7, 4.4, 4.1, 4. , 4.4,
       4.6, 4. , 3.3, 4.2, 4.2, 4.2, 4.3, 3. , 4.1, 6. , 5.1, 5.9, 5.6,
       5.8, 6.6, 4.5, 6.3, 5.8, 6.1, 5.1, 5.3, 5.5, 5. , 5.1, 5.3, 5.5,
       6.7, 6.9, 5. , 5.7, 4.9, 6.7, 4.9, 5.7, 6. , 4.8, 4.9, 5.6, 5.8,
       6.1, 6.4, 5.6, 5.1, 5.6, 6.1, 5.6, 5.5, 4.8, 5.4, 5.6, 5.1, 5.1,
       5.9, 5.7, 5.2, 5. , 5.2, 5.4, 5.1]), 'c': '#31AADE', 'sz': 9}

my updated code:

# Pandas for data management
import pandas as pd

# os methods for manipulating paths
from os.path import dirname, join

# Bokeh basics 
from bokeh.layouts import column, row
from bokeh.models import Select , ColumnDataSource
from bokeh.palettes import Category10_10
from bokeh.plotting import curdoc, figure

df = pd.read_csv("./Bokeh_App/Data/iris.csv", sep = ",")

SIZES = list(range(6, 22, 3))
COLORS = Category10_10 
N_SIZES = len(SIZES)
N_COLORS = len(COLORS)

#data clean up


columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]


def create_figure():
    xs = df[x.value].values
    ys = df[y.value].values
    x_title = x.value.title()
    y_title = y.value.title()

    TOOLTIPS = [
    ("x-axis", x_title),
    ("y-axis", y_title),
    ("x-value", "$xs"),
    ("y-value", "$ys"),
    ("color", "@c"),
    ("size", "@sz")
    ]

    kw = dict()
    if x.value in discrete:
        kw['x_range'] = sorted(set(xs))
    if y.value in discrete:
        kw['y_range'] = sorted(set(ys))
    kw['title'] = "%s vs %s" % (x_title, y_title)

    p = figure(plot_height=600, plot_width=800, tools='pan,box_zoom,hover,reset',tooltips = TOOLTIPS, **kw)
    p.xaxis.axis_label = x_title
    p.yaxis.axis_label = y_title

    if x.value in discrete:
        p.xaxis.major_label_orientation = pd.np.pi / 4

    sz = 9
    if size.value != 'None':
        if len(set(df[size.value])) > N_SIZES:
            groups = pd.qcut(df[size.value].values, N_SIZES, duplicates='drop')
        else:
            groups = pd.Categorical(df[size.value])
        sz = [SIZES[xx] for xx in groups.codes]

    c = "#31AADE"
    if color.value != 'None':
        if len(set(df[color.value])) > N_COLORS:
            groups = pd.qcut(df[color.value].values, N_COLORS, duplicates='drop')
        else:
            groups = pd.Categorical(df[color.value])
        c = [COLORS[xx] for xx in groups.codes]
    
    source = ColumnDataSource(data=dict(xs = xs, ys = ys, c = c, sz=sz))

    p.circle(x='xs', y='ys', color='c', size='sz',source= source, line_color="white", alpha=0.6, hover_color='white', hover_alpha=0.5)

    return p


def update(attr, old, new):
    layout.children[1] = create_figure()


x = Select(title='X-Axis', value='petal.width', options=columns)
x.on_change('value', update)

y = Select(title='Y-Axis', value='petal.length', options=columns)
y.on_change('value', update)

size = Select(title='Size', value='None', options=['None'] + continuous)
size.on_change('value', update)

color = Select(title='Color', value='None', options=['None'] + continuous)
color.on_change('value', update)

controls = column(x, y, color, size, width=200)
layout = row(controls, create_figure())

curdoc().add_root(layout)
curdoc().title = "Crossfilter"

I assumed both color and size variable are in same length as xs and ys. Since they aren’t, you could do the following:

source = ColumnDataSource(data=dict(
    xs = xs,
    ys = ys ,
    c = [c]*len(xs),
    sz = [sz]*len(xs)
))

A working example:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource

from bokeh.io import output_notebook

import numpy as np

output_notebook()

xs=np.array([0.2, 0.2, 0.2, 0.2, 0.2, 0.4, 0.3, 0.2, 0.2, 0.1, 0.2, 0.2, 0.1,
       0.1, 0.2, 0.4, 0.4, 0.3, 0.3, 0.3, 0.2, 0.4, 0.2, 0.5, 0.2, 0.2,
       0.4, 0.2, 0.2, 0.2, 0.2, 0.4, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.2,
       0.2, 0.3, 0.3, 0.2, 0.6, 0.4, 0.3, 0.2, 0.2, 0.2, 0.2, 1.4, 1.5,
       1.5, 1.3, 1.5, 1.3, 1.6, 1. , 1.3, 1.4, 1. , 1.5, 1. , 1.4, 1.3,
       1.4, 1.5, 1. , 1.5, 1.1, 1.8, 1.3, 1.5, 1.2, 1.3, 1.4, 1.4, 1.7,
       1.5, 1. , 1.1, 1. , 1.2, 1.6, 1.5, 1.6, 1.5, 1.3, 1.3, 1.3, 1.2,
       1.4, 1.2, 1. , 1.3, 1.2, 1.3, 1.3, 1.1, 1.3, 2.5, 1.9, 2.1, 1.8,
       2.2, 2.1, 1.7, 1.8, 1.8, 2.5, 2. , 1.9, 2.1, 2. , 2.4, 2.3, 1.8,
       2.2, 2.3, 1.5, 2.3, 2. , 2. , 1.8, 2.1, 1.8, 1.8, 1.8, 2.1, 1.6,
       1.9, 2. , 2.2, 1.5, 1.4, 2.3, 2.4, 1.8, 1.8, 2.1, 2.4, 2.3, 1.9,
       2.3, 2.5, 2.3, 1.9, 2. , 2.3, 1.8])
ys = np.array([1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5, 1.4, 1.5, 1.5, 1.6, 1.4,
       1.1, 1.2, 1.5, 1.3, 1.4, 1.7, 1.5, 1.7, 1.5, 1. , 1.7, 1.9, 1.6,
       1.6, 1.5, 1.4, 1.6, 1.6, 1.5, 1.5, 1.4, 1.5, 1.2, 1.3, 1.4, 1.3,
       1.5, 1.3, 1.3, 1.3, 1.6, 1.9, 1.4, 1.6, 1.4, 1.5, 1.4, 4.7, 4.5,
       4.9, 4. , 4.6, 4.5, 4.7, 3.3, 4.6, 3.9, 3.5, 4.2, 4. , 4.7, 3.6,
       4.4, 4.5, 4.1, 4.5, 3.9, 4.8, 4. , 4.9, 4.7, 4.3, 4.4, 4.8, 5. ,
       4.5, 3.5, 3.8, 3.7, 3.9, 5.1, 4.5, 4.5, 4.7, 4.4, 4.1, 4. , 4.4,
       4.6, 4. , 3.3, 4.2, 4.2, 4.2, 4.3, 3. , 4.1, 6. , 5.1, 5.9, 5.6,
       5.8, 6.6, 4.5, 6.3, 5.8, 6.1, 5.1, 5.3, 5.5, 5. , 5.1, 5.3, 5.5,
       6.7, 6.9, 5. , 5.7, 4.9, 6.7, 4.9, 5.7, 6. , 4.8, 4.9, 5.6, 5.8,
       6.1, 6.4, 5.6, 5.1, 5.6, 6.1, 5.6, 5.5, 4.8, 5.4, 5.6, 5.1, 5.1,
       5.9, 5.7, 5.2, 5. , 5.2, 5.4, 5.1])

c = '#31AADE'
sz = 9

source = ColumnDataSource(data=dict(
    xs = xs,
    ys = ys ,
    c = [c]*len(xs),
    sz = [sz]*len(xs)
))

TOOLTIPS = [
    ("x", "@xs"),
    ("y", "@ys"),
    ("color", "@c"),
    ("size", "@sz")
]

p = figure(plot_width=700, plot_height=700, tooltips=TOOLTIPS,
           title="Hover-tool", tools='hover')

p.circle(x='xs', y='ys', color='c', size = 'sz', source=source)

show(p)

Note: I mistakenly mentioned that # $ - numeric fields, @ - text fields but in reality $ is used for plot related values like plot coordinates, while @ is used for fields in the data source.

Field names that begin with $ are “special fields”. These often correspond to values that are intrinsic to the plot, such as the coordinates of the mouse in data or screen space.
Field names that begin with @ are associated with columns in a ColumnDataSource.

Also going through your code, I could see that the size and color will not work as expected since the size of variables c and sz could be different if the size and color dropdown values are selected. I’ve update the if/else conditions for the color and size and updated the CDS as well. Your code should be as below.

import pandas as pd

# os methods for manipulating paths
from os.path import dirname, join

# Bokeh basics 
from bokeh.layouts import column, row
from bokeh.models import Select , ColumnDataSource
from bokeh.palettes import Category10_10
from bokeh.plotting import curdoc, figure

df = pd.read_csv("./Bokeh_App/Data/iris.csv", sep = ",")

SIZES = list(range(6, 22, 3))
COLORS = Category10_10 
N_SIZES = len(SIZES)
N_COLORS = len(COLORS)

#data clean up


columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]


def create_figure():
    xs = df[x.value].values
    ys = df[y.value].values
    x_title = x.value.title()
    y_title = y.value.title()

    TOOLTIPS = [
    ("x-axis", x_title),
    ("y-axis", y_title),
    ("x-value", "@xs"),
    ("y-value", "@ys"),
    ("color", "@c"),
    ("size", "@sz")
    ]

    kw = dict()
    if x.value in discrete:
        kw['x_range'] = sorted(set(xs))
    if y.value in discrete:
        kw['y_range'] = sorted(set(ys))
    kw['title'] = "%s vs %s" % (x_title, y_title)

    p = figure(plot_height=600, plot_width=800, tools='pan,box_zoom,hover,reset',tooltips = TOOLTIPS, **kw)
    p.xaxis.axis_label = x_title
    p.yaxis.axis_label = y_title

    if x.value in discrete:
        p.xaxis.major_label_orientation = pd.np.pi / 4

    sz = 9

    #updated else part

    if size.value != 'None':
        if len(set(df[size.value])) > N_SIZES:
            groups = pd.qcut(df[size.value].values, N_SIZES, duplicates='drop')
        else:
            groups = pd.Categorical(df[size.value])
        sz = [SIZES[xx] for xx in groups.codes]
    else:
        sz = [sz]*len(xs)

    #updated else part

    c = "#31AADE"
    if color.value != 'None':
        if len(set(df[color.value])) > N_COLORS:
            groups = pd.qcut(df[color.value].values, N_COLORS, duplicates='drop')
        else:
            groups = pd.Categorical(df[color.value])
        c = [COLORS[xx] for xx in groups.codes]
    else:
        c = [c]*len(xs)

    source = ColumnDataSource(data=dict(
    xs = xs,
    ys = ys ,
    c = c,
    sz = sz))

    p.circle(x='xs', y='ys', color='c', size='sz',source= source, line_color="white", alpha=0.6, hover_color='white', hover_alpha=0.5)

    return p


def update(attr, old, new):
    layout.children[1] = create_figure()


x = Select(title='X-Axis', value='petal_width', options=columns)
x.on_change('value', update)

y = Select(title='Y-Axis', value='petal_length', options=columns)
y.on_change('value', update)

size = Select(title='Size', value='None', options=['None'] + continuous)
size.on_change('value', update)

color = Select(title='Color', value='None', options=['None'] + continuous)
color.on_change('value', update)

controls = column(x, y, color, size, width=200)
layout = row(controls, create_figure())

curdoc().add_root(layout)
curdoc().title = "Crossfilter"

Just noting in passing that there are also options besides explicit columns of colors, e.g. various color mapping transforms:

As well as CustomJSTransform which could be used to modulate the size based on another column. But for data sets this small just making explicit columns for colors and sizes is probably also fine.

Thank you so much! This was very helpful and I have learned a lot with your examples!

1 Like