Bokeh hover on two tables with different data sources

Hi,

First thanks for maintaining and developing Bokeh, it’s awsome! I tried already to get help at stackoverflow ( python - Bokeh hover on two tables with different data sources - Stack Overflow) but got no answer yet:

I am trying to highlight data points inside a plot and two tables. The data source for both tables is different and can be linked by column A.

I would like to click on a data point inside the plot and the corresponding data point in table1 is highlighted (works with the code below). In addition I want to highlight all data points of table2 with the same value of column A, or at least the first data point corresponding to the selected ID.

Also, is it possible to adjust the color of the highlighted row?

from bokeh.models.widgets import Panel, Tabs, TableColumn,DataTable, Div
    import numpy as np
    from bokeh.io import  show
    from bokeh.layouts import layout
    import  bokeh.plotting
    from bokeh.models import ColumnDataSource, HoverTool
    
    
    columns = [
            TableColumn(field="A", title="A"),
            TableColumn(field="B", title="B"),
            TableColumn(field="C", title="C"),
            TableColumn(field="D", title="D"),]
    
    data1 = {"A":['ID1','ID2','ID3','ID4','ID5','ID6','ID7','ID8','ID9','ID10'],
            "B": np.random.randint(10, 20, 10),
            "C": np.random.randint(10, 20, 10),
            "D": np.random.randint(10, 20, 10)}
    source1 = ColumnDataSource(data1)
    p1 = DataTable(source=source1, columns=columns, width=300, height=200,editable=True)
    
    data2 = {"A":['ID1','ID1','ID1','ID1','ID2','ID2','ID4','ID7','ID9','ID10'],
            "B": np.random.randint(10, 20, 10),
            "C": np.random.randint(10, 20, 10),
            "D": np.random.randint(10, 20, 10)}
    source2 = ColumnDataSource(data2)
    p2 = DataTable(source=source2, columns=columns, width=300, height=200,editable=True)
    
    dumbdiv = Div(text=""" """, width=1, height=20)
    
    hover1 = HoverTool(tooltips=[("A", "@A")])
    tools1 = ["tap","pan","box_zoom","wheel_zoom","save","reset", hover1]
    
    p = bokeh.plotting.figure(width=400,height=200,x_axis_type="datetime",
                              background_fill_color="lightgray", tools=tools1)
    p.circle(x="B",y="C",source=source1, size=10)
    
    l1 = layout([[p, p1, p2]], sizing_mode='fixed')
    tab1 = Panel(child=l1, title="Three Tables")
    tabs = Tabs(tabs=[tab1],sizing_mode='scale_width')
    show(tabs)

Any tips and help would be appreciated,
Thanks

Hi @Ete,

Here is a modification of your code, with a CustomJS code block added to the TapTool to match the selected values on the second datatable to those on the first.

from bokeh.models.widgets import Panel, Tabs, TableColumn, DataTable, Div
import numpy as np
from bokeh.io import show
from bokeh.layouts import layout
import bokeh.plotting
from bokeh.models import ColumnDataSource, HoverTool, TapTool, CustomJS

columns = [
    TableColumn(field="A", title="A"),
    TableColumn(field="B", title="B"),
    TableColumn(field="C", title="C"),
    TableColumn(field="D", title="D"), ]

data1 = {"A": ['ID1', 'ID2', 'ID3', 'ID4', 'ID5', 'ID6', 'ID7', 'ID8', 'ID9', 'ID10'],
         "B": np.random.randint(10, 20, 10),
         "C": np.random.randint(10, 20, 10),
         "D": np.random.randint(10, 20, 10)}
source1 = ColumnDataSource(data1)
p1 = DataTable(source=source1, columns=columns, width=300, height=200, editable=True)

data2 = {"A": ['ID1', 'ID1', 'ID1', 'ID1', 'ID2', 'ID2', 'ID4', 'ID7', 'ID9', 'ID10'],
         "B": np.random.randint(10, 20, 10),
         "C": np.random.randint(10, 20, 10),
         "D": np.random.randint(10, 20, 10)}
source2 = ColumnDataSource(data2)
p2 = DataTable(source=source2, columns=columns, width=300, height=200, editable=True)

dumbdiv = Div(text=""" """, width=1, height=20)

hover1 = HoverTool(tooltips=[("A", "@A")])
tools1 = ["tap", "pan", "box_zoom", "wheel_zoom", "save", "reset", hover1]

p = bokeh.plotting.figure(width=400, height=200, x_axis_type="datetime",
                          background_fill_color="lightgray", tools=tools1)
p.circle(x="B", y="C", source=source1, size=10)

selection_callback = CustomJS(args=dict(source1=source1, source2=source2, p2=p2), code="""
    // clear out previously selected values
    source2.selected.indices = []
    
    // get the 'A' value of the selected row
    var source1_a = source1.data.A[source1.selected.indices[0]]
    // add every row in source2 with a matching A value to source2's list of selected points
    source2.data.A.forEach((source2_a, i) => {
        if (source2_a == source1_a) {
          console.log("match!")
          source2.selected.indices.push(i)
        }
    });

    // update the DataTable
    p2.change.emit()
""")

p.select(TapTool).callback = selection_callback
source1.selected.js_on_change('indices', selection_callback)

l1 = layout([[p, p1, p2]], sizing_mode='fixed')
tab1 = Panel(child=l1, title="Three Tables")
tabs = Tabs(tabs=[tab1], sizing_mode='scale_width')
show(tabs)

Hope this helps. Regarding changing the highlight color, this is technically possible, but not simple-- it would require overriding the existing selected css class with a new template. This isn’t my area of competence, so I will leave it for others to elaborate upon. :slight_smile:

2 Likes

Thanks a lot for your suggestion, it works! One more question, would it be also possible to highlight the rows in table 2 after the same id was selected in table 1 (not only from the plot?). If I try to add a select(TapTool) to the DataTable p1 I get:

AttributeError: 'generator' object has no attribute 'callback'

Yep! That makes sense, and I didn’t think of that. I’ve edited the code above to do that, by giving the callback a variable and then assigning the same callback to the TapTool and to source1.selected.

The last case of course would be if you click on a row in the second table and want the selection to copy over to the first table and the chart; one way to do that would be to write another version of the selection_callback given here, swapping the roles of source1 and source2.

2 Likes

Thanks, pretty cool :+1: