Connect hover highlight with different CDS

Hi all,

is it possible to link hover highlight between plots with different CDS, based on a common column.

import pandas as pd
import bokeh.plotting as bk
from bokeh.models.sources import ColumnDataSource
from bokeh.io import show
from bokeh.layouts import gridplot
from bokeh.models.tools import HoverTool


source1 = ColumnDataSource({'x':[1,2,3],'y':[4,5,6],'sample':['C1','C2','C3']})
source2 = ColumnDataSource({'x':[11,11.5,12,12.5,13,13.5],'y':[14,14.5,15,15.5,16,16.5],'sample':['C1','C2','C3','C1','C2','C3']})
   
f=bk.figure(tools='hover')
g = bk.figure(tools='hover')

f.scatter('x','y',size=20.0,color='red',source=source1, hover_color='gold')
g.scatter('x','y',size=20.0,color='green',source=source2, hover_color='gold')

q = gridplot([[f, g]])
show(q)

Here, for example, I’d light to highlight one scatter point in f when hovering on it, and at the same time highlight the scatter points in g sharing the same “sample” field with the point in f.
Is it possible to obtain this, e.g. coding some Javascript callback?

It’s a little complicated but here’s a commented working example.

Basically you:

  • Create a callback to attach to each HoverTool
  • Use cb_data in the callback to retrieve both the underlying CDS and the indices of the thing being hovered
  • From that CDS and the indices, retrieve the corresponding “lookup field” values (“sample” for your MRE)
  • Loop through each source you want linked, finding the indices within that source that match the “sample” values, and updating the inspected.indices property to those

The only wrinkle/question mark is if you want the linkage to “include itself” or not… see “self_hvr” in the code, it can be flipped true/false to handle either.

import pandas as pd
import bokeh.plotting as bk
from bokeh.models.sources import ColumnDataSource
from bokeh.io import show, save
from bokeh.layouts import gridplot
from bokeh.models.tools import HoverTool
from bokeh.models import CustomJS


source1 = ColumnDataSource({'x':[1,2,3],'y':[4,5,6],'sample':['C1','C2','C3']})
source2 = ColumnDataSource({'x':[11,11.5,12,12.5,13,13.5],'y':[14,14.5,15,15.5,16,16.5],'sample':['C1','C2','C3','C1','C2','C3']})
   
f=bk.figure(tools='hover')
g = bk.figure(tools='hover')

f.scatter('x','y',size=20.0,color='red',source=source1, hover_color='gold')
g.scatter('x','y',size=20.0,color='green',source=source2, hover_color='gold')

hvr_cb = CustomJS(args=dict(srcs=[source1,source2],self_hvr=True)
                  
                  ,code='''
                  var ins_i = cb_data.index.indices //gets the indices of the thing being hovered
                  var ins_src = cb_data.renderer.data_source //gets the CDS driving the thing being hovered
                  //get the 'sample' values you need to look for
                  var smps = ins_src.data['sample'].filter((x,i)=>ins_i.includes(i))
                  //if there's a hit/hovered index
                  if (ins_i.length>0){
                        //go through all sources to be "linked"
                        for (var src of srcs){
                                //self_hvr arg == if you want the hover linkage to include itself
                                //e.g. you have multiple "C1s" in source1 and you want all "C1s" in source1 to highlight when you hover over all of them
                                if ((src.id == ins_src.id && self_hvr == true)|src.id != ins_src.id){
                                        //collect all the indices in that source that match with smps
                                        var upd_smps = []
                                        for (var i = 0; i <src.data['sample'].length;i++){
                                                if (smps.includes(src.data['sample'][i])){
                                                        upd_smps.push(i)
                                                        }
                                                }
                                        //update that source's inspected indices
                                        src.inspected.indices = upd_smps
                                        }
                                else {
                                    // otherwise no hover
                                    src.inspected.indices = []
                                    }
                                src.change.emit()
                                    }
                                }
                  else {
                      //if no hit, make all sources have no inspected indices
                      for (var src of srcs){
                          src.inspected.indices = []
                          src.change.emit()
                          }
                      }
                  '''
                  )
#assign callback to each HoverTool
f_hvr = f.tools[0]
f_hvr.callback = hvr_cb
g_hvr = g.tools[0]
g_hvr.callback = hvr_cb

q = gridplot([[f, g]])
show(q)

hvr_lnk

It’s also possible to lightly abuse CustomJSHover for this kind of purpose. I say “lightly abuse” because the primary intention for CustomJSHover is to format tooltip fields. But there’s nothing stopping from just computing entirely new or different data values (e.g. taking into account another CDS somehow) and returning those as the “formatted value” instead.