Programmatic highlighting with multiple renderers

Hi! I am currently working on a project where i have two types of glyphs: routes (multiline) and nodes (circles). I would live to achive the following interactivity:

  1. When user clicks on a line all circles become selected.
  2. When user clics on a circle all lines will become selected.

I was able to achive first task, however when i try to solve 2nd task with a similiar js code iam able to update source_lines.selected.indices and emit changes to ColumnDataSource via emit() method in js, visually nothing changes. Could you please advise me on what iam missing. My minimum working example is bellow:

import bokeh
import bokeh.plotting
import bokeh.tile_providers
from bokeh.models import TapTool, ColumnDataSource, MultiLine, Circle
from bokeh.models.callbacks import CustomJS

tap = TapTool()

x_circle = [1, 2, 3, 4, 5]
y_cirlce = [6, 7, 2, 4, 5]

data = {'x': x_circle, 'y': y_cirlce}

circle_source = ColumnDataSource(data)

xs_lines = [[1, 3, 2], [3, 4, 6, 6]]
ys_lines = [[2, 1, 4], [4, 7, 8, 5]]

data = {'xs': xs_lines, 'ys': ys_lines}
lines_source = ColumnDataSource(data)

p = bokeh.plotting.figure(plot_width=1600, plot_height=1000,
                          tools=['pan', 'wheel_zoom', 'reset', tap], active_scroll='wheel_zoom')

circle_renderer ='x', 'y', source=circle_source, size=20, color="navy", alpha=0.5)

selected_circle= Circle(fill_color='black')
nonselected_circle = Circle(fill_alpha=0.6)

circle_renderer.selection_glyph = selected_circle
circle_renderer.nonselection_glyph = nonselected_circle

line_renderer = p.multi_line('xs', 'ys', source=lines_source, name='route',
                            line_color='blue', legend='routes', line_width=3)

selected_line = MultiLine(line_width=4, line_color='#f4aa42')
nonselected_line = MultiLine(line_alpha=0.4, line_color='#0000ff')

line_renderer.selection_glyph = selected_line
line_renderer.nonselection_glyph = nonselected_line

js_code = '''
// Get data from ColumnDataSource
if (source_circles.selected.indices.length > 0){
    source_lines.selected.indices = [0, 1];

} else if (source_lines.selected.indices.length > 0) {
    source_circles.selected.indices = [0, 1, 2, 3, 4];

callback = CustomJS(args={'source_circles': circle_source,
                          'source_lines': lines_source}, code=js_code)

tap.callback = callback

I will have to investigate this in the next couple of days. MultiLine (and Patches) is a special case that that has nested data columns. I can imagine there may either be some issue (bug) or special consideration as a result.

1 Like

So I am afraid I don’t have great news. First things first: Don’t use tap.callback. All the old-style ad-hoc callback properties are going away in Bokeh 2.0 in just a few months The newer, generic js_on_change method can be used uniformly to attach JS callbacks to any property of any Bokeh model.

That’s not the fundamental issue here, though. So what is? Well, the problem is that by default, selection tools like TapTool operate on all glyphs in a plot. And what they do, is explicitly set empty selections on any glyph that is not hit (e.g. by the tap). And that is what is happening here. By pure coincidence, everything appears to work for selecting lines, because when your callback runs, the circle selection was already previously reset, so your changes take precedence. But in the converse situation, the opposite happens. The reset to to the lines selection happens after your callback, so the empty selection on lines takes precedence.

If you split things up like using individual tap tools tied to specific renderers:

tap_circle = TapTool(renderers=[circle_renderer])
tap_line = TapTool(renderers=[line_renderer])
p.add_tools(tap_line, tap_circle)

Then you can kinda-sorta see things start to be closer to what you want, but with one major problem for your use case: Only one tap tool can be active at a time. I.e. users would have to go to the toolbar and change which tap tool was active depending on whether they intended to select lines or circles. That’s not great.

So what are the options? I don’t have anything especially good to suggest. I would note that when making a selection append using “shift-tap” that the selection resetting behaviour is suppressed. So if you shift-tap with your original code, then things actually work exactly as you want, but then that only works once or twice, because eventually you do want selections to reset. I think to really solve this you would need to create some sort of specialized custom extension subclass that handles these special cases.

1 Like