Changing label text on mouseover of other object

I’m trying to understand how to control interactivity in Bokeh. My problem is a little more complicated than any examples I’ve seen on-line.

I have plotted two MultiPolygon shapes and I want to change the text of a label object when the mouse pointer is over one or other of the shapes. I could display the desired text using tooltips, but I have reasons for not wanting to do so (based on a more complicated plot I’m trying to make)

Here’s the outline of my code

from typing import Iterable
from osgeo import ogr
import numpy as np

from bokeh.models import CustomJS,  Label, HoverTool, MultiPolygons, ColumnDataSource
from bokeh.plotting import figure
from bokeh.io import show

def main():
    tools = "pan,wheel_zoom,box_zoom,reset"
    fig = figure(
        tools=tools,
        tooltips="@tooltip", 
    )

    text = "This is some text"
    label = Label(x=200, y=10,x_units='screen', y_units='screen', text=text)
    fig.add_layout(label)

    wkt1 = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"
    wkt2= "POLYGON ((75 10, 85 45, 55 40, 50 20, 75 10), (60 30, 75 35, 70 20, 60 30))"
    handle = plot_geometry_outlines(fig, [wkt1, wkt2], ["WKT 1", " WKT 2"])

    callback = CustomJS(
        args=dict(
            label=label,
            polys=handle, 
        ),
        code="""
            //Not sure what I should put here to
            //update "label" text with the multipolygon
            //currently under the mouse
            """
    )

    hover_tool = HoverTool(tooltips="@tooltip", callback=callback)
    fig.tools.append(hover_tool)
    show(fig)

What should I put in the CustomJS callback so that the label’s text says “WKT 1” when the mouse hovers over the first geometry, and “WKT 2” when it hovers over the second geometry? Alternatively, is there a better way to acheive the same goal?

Thanks in advance for any advice.

For completeness, here is the code in plot_geometry_outlines which adds a MultiPolyon glyph to the figure.

def plot_geometry_outlines(fig, polygons: Iterable, tooltips: Iterable[str], **kwargs):
    assert len(polygons) == len(tooltips)

    #defaults
    props = {
        'line_color': '#000000',    
    }
    props.update(kwargs)

    #Create a column data source in the format expected by multipolygon
    #This function is a detail, not relevant to the question, I think
    cds = create_cds_from_array(polygons, tooltip=tooltips) 

    glyph = MultiPolygons(xs="xs", ys="ys", **props)
    handle = fig.add_glyph(cds, glyph)
    return handle 

def create_cds_from_array(polygons, **kwargs):
    lmap = lambda x, y: list(map(x, y))

    obj = lmap(lambda x: as_bokeh(x), polygons)
    xs = lmap(lambda x: x[0], obj)
    ys = lmap(lambda x: x[1], obj)
    kwargs['xs'] = xs
    kwargs['ys'] = ys
    cds = ColumnDataSource(kwargs)
    return cds 


def as_bokeh(obj):
    obj = ogr.CreateGeometryFromWkt(obj)
    lng = ogrGeometryToNestedList(obj, 0)
    lat = ogrGeometryToNestedList(obj, 1)
    return lng, lat 


def ogrGeometryToNestedList(geometry, dim):
    """Converter used for Bokeh

    Converts one dimension (lng or lat) of a polygon-type geometry to
    a nested list in a format bokeh will like
    """

    out = []

    name = geometry.GetGeometryName()
    if name == 'MULTIPOLYGON':
        for i in range(geometry.GetGeometryCount()):
            poly = geometry.GetGeometryRef(i)
            out.extend(ogrGeometryToNestedList(poly, dim))
    elif name == 'POLYGON':
        ringlist = []
        for i in range(geometry.GetGeometryCount()):
            ring = geometry.GetGeometryRef(i)
            points = np.atleast_2d(ring.GetPoints())
            points = points[:, dim]
            ringlist.append( points.tolist())
        out.append(ringlist)
    else:
        raise ValueError("Currently only polygons and multipolygons supported for Bokeh ")
    
    return out 

The logic you should implement in the CustomJS should be along the lines of:

  • ID the index in the CDS that is currently moused over (this is yielded via cds.inspected.indices , which gives you an array containing all the hovered indices)
  • Retrieve the “label” you want given that index
  • Update the label/labelset source with that

The other thing to consider is how to trigger the callback → in your example you’re using the HoverTool to trigger it but you can also use the MouseMove event. Basically if you go

fig.js_on_event('mousemove',callback)

the callback will trigger as the user moves the mouse anywhere over the plot… so in the callback you’ll want to check if cds.inspected.indices.length > 0 , and if true, then do the above mentioned stuff.

If you can pull out this the guts of your code into an actual Minimal Reproducible Example using dummy data I can assist further :smiley:

1 Like

Oops, my minimal reproducible example was a bit too minimal. I’ve edited the question to include the missing function. This code works, at least on my machine.

I tried your suggestion and it works perfectly. For future reference, this is what my top level function now looks like

def main():
    tools = "pan,wheel_zoom,box_zoom,reset"
    fig = figure(
        tools=tools,
        tooltips="@tooltip", 
    )

    text = "This is some text"
    label = Label(x=200, y=10,x_units='screen', y_units='screen', text=text)
    fig.add_layout(label)

    wkt1 = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"
    wkt2= "POLYGON ((75 10, 85 45, 55 40, 50 20, 75 10), (60 30, 75 35, 70 20, 60 30))"
    handle = plot_geometry_outlines(fig, [wkt1, wkt2], ["WKT 1", " WKT 2"])

    callback = CustomJS(
        args=dict(
            label=label,
            polys=handle, 
        ),
        code="""
            var cds = polys.data_source;
            var indices = cds.inspected.indices;
            if (indices.length > 0)
            {
                var index = indices[0];
                var text = cds.data['tooltip'][index];
                label.text= text;
            }
        """
    )

    fig.js_on_event('mousemove',callback)

    show(fig)

I couldn’t find any reference to ColumnDataSource.inspected in my web searching. Is this feature documented?

Thanks again for your help!

1 Like

It does not exist on the Python side yet, so no. Currently I would consider it an implementation detail that has never officially been decided to include under public supported APIs. If you’d like to advocate for that happening, I would suggest opening a GitHub development discussion.

Hi Bryan,

I’m a little reluctant to start advocating for design decisions in Bokeh based on what is only my second plot created with the package. That said, querying the ColumnDataSource to see which elements have been moused-over does feel somewhat logical.

As with everything Bokeh it would be nice to be able to define the behaviour entirely in Python, but I have no idea how much work would be involved.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.