Search code examples
pythonplotdata-sciencebokeh

Bokeh Hover tooltip on Box-Whisker Plot


I'm new to bokeh, but trying desperately to apply a Hover tooptip to a Box-Whisker plot. I am trying to display the Q1,Q2,Q3 and IQR values when hovering over the Box glyph but have been unsuccessful in my attempts, and confused by the process of creating the components of a Box-Whisker plot

I am using the example provided from the bokeh documentations.

import numpy as np
import pandas as pd
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Plot, Quad,Range1d,HoverTool, Panel, Tabs,Legend, LegendItem
from bokeh.plotting import figure, output_file, show


# generate some synthetic time series for six different categories
cats = list("abcdef")
yy = np.random.randn(2000)
g = np.random.choice(cats, 2000)
for i, l in enumerate(cats):
    yy[g == l] += i // 2
df = pd.DataFrame(dict(score=yy, group=g))

# find the quartiles and IQR for each category
groups = df.groupby('group')
q1 = groups.quantile(q=0.25)
q2 = groups.quantile(q=0.5)
q3 = groups.quantile(q=0.75)
iqr = q3 - q1
upper = q3 + 1.5*iqr
lower = q1 - 1.5*iqr

# find the outliers for each category
def outliers(group):
    cat = group.name
    return group[(group.score > upper.loc[cat]['score']) | (group.score < lower.loc[cat]['score'])]['score']
out = groups.apply(outliers).dropna()

# prepare outlier data for plotting, we need coordinates for every outlier.
if not out.empty:
    outx = []
    outy = []
    for keys in out.index:
        outx.append(keys[0])
        outy.append(out.loc[keys[0]].loc[keys[1]])

p = figure(tools="", background_fill_color="#efefef", x_range=cats, toolbar_location=None)

# if no outliers, shrink lengths of stems to be no longer than the minimums or maximums
qmin = groups.quantile(q=0.00)
qmax = groups.quantile(q=1.00)
upper.score = [min([x,y]) for (x,y) in zip(list(qmax.loc[:,'score']),upper.score)]
lower.score = [max([x,y]) for (x,y) in zip(list(qmin.loc[:,'score']),lower.score)]

# stems
p.segment(cats, upper.score, cats, q3.score, line_color="black")
p.segment(cats, lower.score, cats, q1.score, line_color="black")

# boxes
p.vbar(cats, 0.7, q2.score, q3.score, fill_color="#E08E79", line_color="black")
p.vbar(cats, 0.7, q1.score, q2.score, fill_color="#3B8686", line_color="black")

# whiskers (almost-0 height rects simpler than segments)
p.rect(cats, lower.score, 0.2, 0.01, line_color="black")
p.rect(cats, upper.score, 0.2, 0.01, line_color="black")

# outliers
if not out.empty:
    p.circle(outx, outy, size=6, color="#F38630", fill_alpha=0.6)

p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = "white"
p.grid.grid_line_width = 2
p.xaxis.major_label_text_font_size="16px"

# Format the tooltip
tooltips = [
    ('q1', '@q2'),
    ('q2', '@q1'),
    ('q3', '@q3'),
    ('iqr', '@iqr'),
]

# Add the HoverTool to the figure
p.add_tools(HoverTool(tooltips=tooltips))

output_file("boxplot.html", title="boxplot.py example")

show(p)

enter image description here


Solution

  • Beyond a few built-in "special" variables, like mouse coordinates, the hover tool expects column names from the ColumnDataSource. To get access to q1, q2, etc. you need to create the boxes using a ColumnDataSource rather than plain arrays.

    First, prepare the data for the boxes:

    # separate ColumnDataSource for boxes
    boxes_data = pd.concat([
        q1.rename(columns={"score":"q1"}),
        q2.rename(columns={"score":"q2"}),
        q3.rename(columns={"score":"q3"}),
        iqr.rename(columns={"score":"iqr"})
    ], axis=1)
    
    

    Next, in the portion of the code that draws the boxes, save the references to renderers so that the HoverTool only triggers on them and not figure-wide:

    # boxes
    boxes_source = ColumnDataSource(boxes_data)
    top_box = p.vbar(
        "group", 0.7, "q2", "q3", fill_color="#E08E79",
        line_color="black", source=boxes_source)
    
    bottom_box = p.vbar(
        "group", 0.7, "q1", "q2", fill_color="#3B8686",
        line_color="black", source=boxes_source)
    
    # add hover just to the two box renderers
    box_hover = HoverTool(renderers=[top_box, bottom_box],
                             tooltips=[
                                 ('q1', '@q1'),
                                 ('q2', '@q2'),
                                 ('q3', '@q3'),
                                 ('iqr', '@iqr')
                             ])
    p.add_tools(box_hover)
    

    The rest of the code is fine as is so the result is: enter image description here