Alexander Reshytko
Alexander Reshytko

Reputation: 2236

Bokeh: enable hover tool on image glyphs

Is it possible to enable hover tool on the image (the glyph created by image(), image_rgba() or image_url()) so that it will display some context data when hovering on points of the image. In the documentation I found only references and examples for the hover tool for glyphs like lines or markers.

Possible workaround solution:

I think it's possible to convert the 2d signal data into a columnar Dataframe format with columns for x,y and value. And use rect glyph instead of image. But this will also require proper handling of color mapping. Particularly, handling the case when the values are real numbers instead of integers that you can pass to some color palette.

Upvotes: 1

Views: 2619

Answers (3)

Pablo Reyes
Pablo Reyes

Reputation: 3123

Update for bokeh version 0.12.16

Bokeh version 0.12.16 supports HoverTool for image glyphs. See: bokeh release 0.12.16

for erlier bokeh versions:

Here is the approach I've been using for Hovering over images using bokeh.plotting.image and adding in top of it an invisible (alpha=0) bokeh.plotting.quad that has Hovering capabilities for the data coordinates. And I'm using it for images with approximately 1500 rows and 40000 columns.

# This is used for hover and taptool    
imquad = p.quad(top=[y1], bottom=[y0], left=[x0], right=[x1],alpha=0)

A complete example of and image with capabilities of selecting the minimum and maximum values of the colorbar, also selecting the color_mapper is presented here: Utilities for interactive scientific plots using python, bokeh and javascript. Update: Latest bokeh already support matplotlib cmap palettes, but when I created this code, I needed to generate them from matplotlib.cm.get_cmap

In the examples shown there I decided not to show the tooltip on the image with tooltips=None inside the bokeh.models.HoverTool function. Instead I display them in a separate bokeh.models.Div glyph.

Upvotes: 1

Alex Whittemore
Alex Whittemore

Reputation: 1015

Building off of Alexander Reshytko's self-answer above, I've implemented a version that's mostly ready to go off the shelf, with some examples. It should be a bit more straightforward to modify to suit your own application, and doesn't rely on Pandas dataframes, which I don't really use or understand. Code and examples at Github: Bokeh - Image with HoverTool

Upvotes: 1

Alexander Reshytko
Alexander Reshytko

Reputation: 2236

Okay, after digging more deeply into docs and examples, I'll probably answer this question by myself.

The hover effect on image (2d signal) data makes no sense in the way how this functionality is designed in Bokeh. If one needs to add some extra information attached to the data point it needs to put the data into the proper data model - the flat one.

tidying the data

Basically, one needs to tidy his data into a tabular format with x,y and value columns (see Tidy Data article by H.Wickham). Now every row represents a data point, and one can naturally add any contextual information as additional columns.

For example, the following code will do the work:

def flatten(matrix: np.ndarray,
            extent: Optional[Tuple[float, float, float, float]] = None,
            round_digits: Optional[int] = 0) -> pd.DataFrame:

    if extent is None:
        extent = (0, matrix.shape[1], 0, matrix.shape[0])

    x_min, x_max, y_min, y_max = extent

    df = pd.DataFrame(data=matrix)\
        .stack()\
        .reset_index()\
        .rename(columns={'level_0': 'y', 'level_1': 'x', 0: 'value'})

    df.x = df.x / df.x.max() * (x_max - x_min) + x_min
    df.y = df.y / df.y.max() * (y_max - y_min) + y_min

    if round_digits is not None:
        df = df.round({'x': round_digits, 'y': round_digits})

    return df

rect glyph and ColumnDataSource

Then, use rect glyph instead of image with x,y mapped accordingly and the value column color-mapped properly to the color aesthetics of the glyph.

color mapping for values

  • here you can use a min-max normalization with the following multiplication by the number of colors you want to use and the round
  • use bokeh builtin palettes to map from computed integer value to a particular color value.

With all being said, here's an example chart function:

def InteractiveImage(img: pd.DataFrame,
          x: str,
          y: str,
          value: str,
          width: Optional[int] = None,
          height: Optional[int] = None,
          color_pallete: Optional[List[str]] = None,
          tooltips: Optional[List[Tuple[str]]] = None) -> Figure:
    """

    Notes
    -----
        both x and y should be sampled with a constant rate
    Parameters
    ----------
    img
    x
        Column name to map on x axis coordinates
    y
        Column name to map on y axis coordinates 
    value
        Column name to map color on
    width
        Image width
    height
        Image height
    color_pallete
        Optional. Color map to use for values
    tooltips
        Optional.
    Returns
    -------
        bokeh figure
    """

    if tooltips is None:
        tooltips = [
            (value, '@' + value),
            (x, '@' + x),
            (y, '@' + y)
        ]

    if color_pallete is None:
        color_pallete = bokeh.palettes.viridis(50)

    x_min, x_max = img[x].min(), img[x].max()
    y_min, y_max = img[y].min(), img[y].max()

    if width is None:
        width = 500 if height is None else int(round((x_max - x_min) / (y_max - y_min) * height))
    if height is None:
        height = int(round((y_max - y_min) / (x_max - x_min) * width))

    img['color'] = (img[value] - img[value].min()) / (img[value].max() - img[value].min()) * (len(color_pallete) - 1)
    img['color'] = img['color'].round().map(lambda x: color_pallete[int(x)])

    source = ColumnDataSource(data={col: img[col] for col in img.columns})

    fig = figure(width=width,
                 height=height,
                 x_range=(x_min, x_max),
                 y_range=(y_min, y_max),
                 tools='pan,wheel_zoom,box_zoom,reset,hover,save')

    def sampling_period(values: pd.Series) -> float:
        # @TODO think about more clever way
        return next(filter(lambda x: not pd.isnull(x) and 0 < x, values.diff().round(2).unique()))

    x_unit = sampling_period(img[x])
    y_unit = sampling_period(img[y])

    fig.rect(x=x, y=y, width=x_unit, height=y_unit, color='color', line_color='color', source=source)
    fig.select_one(HoverTool).tooltips = tooltips

    return fig

#### Note: however this comes with a quite high computational price

Upvotes: 1

Related Questions