Get renderer name from taptool

Hi,

Can someone point me to an example on how to get the name of a renderer when its clicked? I have multiple figures, all having renderers with the same name. I’d like to tap a renderer regardless of which figure and have all lines with the same name across figures be highlighted.

I’m not tied to taptool either. Being able to hover over a glyph with the hovertool also works for my applications.

Thanks

On the Javascript side only, the Selection object has a selected_glyphs property that you can look at:

Do you have an example of how to use this? Sorry it’s not quite clear on how to utilize the Selection object

I don’t have one handy. There are lots of examples that access, e.g. selected.indices to get the selection indices. It should be exactly the same to access selected.selected_glyphs in a similar fashion to get the list of which glyphs were part of the current selection.

Actually, something this might be a simpler approach:

from bokeh.plotting import figure, show
from bokeh.models import CustomJS

p = figure(tools="tap")

red = p.scatter(x=[1, 2], y=[1, 2], size=25, color="red", name="red")
blue = p.scatter(x=[1, 2], y=[2, 1], size=25, color="blue", name="blue")

cb_code = "console.log('selected {name}')"

for r in (red, blue):
    r.data_source.selected.js_on_change(
        "indices",
        CustomJS(code=cb_code.format(name=r.name))
    )

show(p)

i.e. just configure a selection callback per renderer that has the required info baked in.

Note that “unselecting” a glyph will also trigger a change in selected.indices so you might want your callbacks to check whether the indices are empty or not.

Lastly, if the glyphs are lines, you’d want to set up callbacks on "line_indices" instead.

Here is another possibility, based on this old example

import numpy as np
from bokeh.models import ColumnDataSource, CustomJS, TapTool
from bokeh.plotting import figure, show

t = np.linspace(0, 0.1, 100)

source = ColumnDataSource(data=dict(text=['No line selected'], text_color=['black']))

p = figure()

l1 = p.line(t, 100*np.sin(t*50), color='goldenrod', line_width=30, name="goldenrod")
l2 = p.line(t, 100*np.sin(t*50+1), color='lightcoral', line_width=20, name="lightcoral")
l3 = p.line(t, 100*np.sin(t*50+2), color='royalblue', line_width=10, name="royalblue")

p.text(0, -100, text_color='text_color', source=source)

# cb_data = {geometries: ..., source: ...}
p.add_tools(TapTool(callback=CustomJS(args=dict(source=source), code= """
    // get_view is experimental and may change in the future
    const view = cb_data.source.selected.get_view()
    if (view) {
        const name = view.renderer.model.name
        source.data = {
            text: ['Selected the ' + name + ' line'],
            text_color: [name]
        }
    }
""")))

show(p)

I would say reiterate that get_view is still considered “experimental”, though it has actually been around a long time. Also that knowing to look at view.renderer.model.name is a bit obscure. But as always in OSS, the squeaky wheel gets the grease. There have just not been many folks over the year asking to do this sort of thing, and there is always more, other work to do. But if you’d like to start a discussion about ways to improve this sort of use-case, please do.

Hi Bryan,

So I am going down the route of the first solution that you provided and for some reason when I run the below code on one machine, it gives me a circular reference error but works fine when a friend runs it on their computer. Any ideas on why that is and how the go about it? I have the latest version of bokeh installed on python3.12

from bokeh.plotting import figure, show
from bokeh.layouts import row
from bokeh.models import CustomJS, TapTool, ColumnDataSource, Line, HoverTool
import numpy as np
import os

os.system('cls')

# Create figures
p1 = figure(width=400, height=300, title="Plot 1", tools='tap')
p2 = figure(width=400, height=300, title="Plot 2", tools='tap')
p3 = figure(width=400, height=300, title="Plot 3", tools='tap')

hover = HoverTool()
hover.point_policy = 'snap_to_data'

p1.add_tools(hover)
p2.add_tools(hover)
p3.add_tools(hover)

x = np.arange(0, 10)

# Create all lines
line1a = p1.line(x, np.random.randint(0, 20, np.size(x)), line_color='blue', name='blue', nonselection_line_width=2, selection_line_width = 6)
line1b = p1.line(x, np.random.randint(0, 20, np.size(x)), line_color='red', name='red', nonselection_line_width=2, selection_line_width = 6)
line2a = p2.line(x, np.random.randint(0, 20, np.size(x)), line_color='blue', name='blue', nonselection_line_width=2, selection_line_width = 6)
line2b = p2.line(x, np.random.randint(0, 20, np.size(x)), line_color='red', name='red', nonselection_line_width=2, selection_line_width = 6)
line3a = p3.line(x, np.random.randint(0, 20, np.size(x)), line_color='blue', name='blue', nonselection_line_width=2, selection_line_width = 6)
line3b = p3.line(x, np.random.randint(0, 20, np.size(x)), line_color='red', name='red', nonselection_line_width=2, selection_line_width = 6)

fig_list = [p1, p2, p3]

line_list_2 = [line1a, line1b, line2a, line2b, line3a, line3b]

# line_list = []
# for p in fig_list:
#     for r in p.renderers:
#         if isinstance(r.glyph, Line):
#             line_list.append(r)

cb_code = """
    console.log(line.name + ' selected');
    for (let i=0; i < line_list.length; i++) {
        let l = line_list[i];
        if (line !== l && line.name === l.name) {
            l.glyph.line_width = 6;
        }
    }
"""

for line in line_list_2:
    callback = CustomJS(args=dict(line=line, line_list=line_list_2), code=cb_code)
    line.data_source.selected.js_on_change("line_indices", callback)

show(row(p1, p2, p3))

@alikarb I think I have some good news and bad news. The code above indeed has a circular reference that will not work in later versions of Bokeh. The good news is that can be worked around fairly simply by passing the model IDs instead of the objects themselves. Here is a full example using scatter rather than line:

from collections import defaultdict
from bokeh.plotting import figure, show, row
from bokeh.models import CustomJS
import numpy as np

x=np.arange(0, 10)
y1a = np.random.randint(0, 20, np.size(x))
y1b = np.random.randint(0, 20, np.size(x))
y2a = np.random.randint(0, 20, np.size(x))
y2b = np.random.randint(0, 20, np.size(x))
y3a = np.random.randint(0, 20, np.size(x))
y3b = np.random.randint(0, 20, np.size(x))

p1 = figure(width=400, height=300, title="Plot 1", tools="tap,reset")
p2 = figure(width=400, height=300, title="Plot 2", tools="tap,reset")
p3 = figure(width=400, height=300, title="Plot 3", tools="tap,reset")

lines = (
    p1.scatter(x, y1a, color='blue', name='blue', selection_line_width=6),
    p1.scatter(x, y1b, color='red', name='red', selection_line_width=6),
    p2.scatter(x, y2a, color='blue', name='blue', selection_line_width=6),
    p2.scatter(x, y2b, color='red', name='red', selection_line_width=6),
    p3.scatter(x, y3a, color='blue', name='blue', selection_line_width=6),
    p3.scatter(x, y3b, color='red', name='red', selection_line_width=6),
)

code = """
for (const id of source_ids[line.name]) {
    const source = line.document.get_model_by_id(id)
    source.selected.indices = cb_obj.indices
}
"""

# partition the source ids by name
source_ids = defaultdict(list)
for line in lines:
    source_ids[line.name].append(line.data_source.id)

for line in lines:
    callback = CustomJS(args=dict(line=line, source_ids=source_ids), code=code)
    line.data_source.selected.js_on_change("indices", callback)

show(row(p1, p2, p3))

This gets everything working using Bokeh’s built-in machinery for selection highlighting and un-highlighting out of the box.

The bad news is that I can only get this to work for scatter-like glyphs, and not for line [1]. I think there are just some bugs or under-developed features in the case of lines because selecting lines (as it has turned out) is just not nearly as common a thing users seem to need to do. I hope to write up some GH issues with details around this to hopefully make improvements for the future. But I am not sure I have any other concrete suggestions to make things work well for lines at the moment. If I come up with something useful, I will certainly post it back here.


  1. More precisely I would expect the exact code above that works for scatter to also work with only the substitutions scatterline and indicesline_indices, but it does not. ↩︎

Actually @alikarb I will mention one more option. There’s not enough information here about your actual use case to know for sure if this is application, but if multi_line is an option that you can use, then linked selections work out of box, no callback needed at all. The reason for this is that multi_line is basically a “scatter plot of lines” and all the lines can share a single data source, which is the basis for Bokeh’s built-in linked selection.

from bokeh.plotting import figure, show, row
from bokeh.models import ColumnDataSource
import numpy as np

x=np.arange(0, 10)

source = ColumnDataSource(data=dict(
    xs=[x, x],
    y1s=[np.random.randint(0, 20, np.size(x)), np.random.randint(0, 20, np.size(x))],
    y2s=[np.random.randint(0, 20, np.size(x)), np.random.randint(0, 20, np.size(x))],
    y3s=[np.random.randint(0, 20, np.size(x)), np.random.randint(0, 20, np.size(x))],
    color=["blue", "red"]
))

p1 = figure(width=400, height=300, title="Plot 1", tools="tap,reset")
p1.multi_line("xs", "y1s", color="color", source=source, selection_line_width=6),

p2 = figure(width=400, height=300, title="Plot 2", tools="tap,reset")
p2.multi_line("xs", "y2s", color="color", source=source, selection_line_width=6),

p3 = figure(width=400, height=300, title="Plot 3", tools="tap,reset")
p3.multi_line("xs", "y3s", color="color", source=source, selection_line_width=6),

show(row(p1, p2, p3))

ScreenFlow

Thanks Bryan! I’ll have to look at your example with the model ids a bit more to digest if that is a working solution for my needs. If not, I think the multi-line might be a good solution. I might need to re-factor my code a bit to handle the data differently. Only concern I have there is if only works with line-glyphs or can I also use scatter-like glyphs?

A little more about my use-case. I’m plotting hardware test data under different conditions i.e. one figure is data for multiple parts at cold temperature, 2nd is same parts, same test but at room temperature, and 3rd figure is hot temperatures.

Each of the 3 figures is identical in terms of glyph names, colors, line_dash, line_width, etc. Only difference is just the temperature in which things were tested at. I plot the data using scatter & line glyphs for easier visualization (ends up like below image).

For review of the test data, I’d like to be able to tap the data in one figure (doesnt matter which one) and have those glyphs across all figures be “highlighted” so we can look at the trend of performance under the different test conditions. In the end, I think multiline could work if I can also toggle the marker sizes, widths of marker along with the line glyphs. One thought is to use the model id approach in parallel with multi line unless you have a cleaner approach out of the box.

Appreciate the help on this!