I’m (still) working away at figuring out how to update a 2nd figure from a hover callback. This in bokeh 3.6.2.
I have not been able to make the 2nd figure pop up when a point is hovered over, so I’ve made it part of the layout. My compromise was to pass the figure into the js callback and change the visibility when the mouse is over a point (and the figure updated via the cds) or not.
In callback_code, if I comment out setting hover_plot.visible to false, the app behaves as expected (except of course hover_plot is never set invisible). With the code as is, it appears to go into an infinite loop of calls to the callback, and with the test on index === null (et seq) always being true.
Is this a bug, or am I causing the events by having changed the visibility in the model?
Here’s a complete code example:
from bokeh.layouts import column, layout, row
from bokeh.plotting import figure, show, save
from bokeh.models import HoverTool, ColumnDataSource, CustomJS, Div
from bokeh.models.dom import Template
# Sample data for the main plot
source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[6, 7, 2, 4, 5], names=["A", "B", "C", "D", "E"],
))
# Sample data for the hover plot
x_hover = [0, 1, 2]
y_hover = [0, 1, 0]
image_urls = ["", "", ""]
hover_source = ColumnDataSource(data=dict(x_hover=x_hover, y_hover=y_hover))
# Create a hidden Bokeh figure for the tooltip
hover_plot = figure(width=200, height=150, title="Hover Plot", visible=True)
hover_plot.line(x="x_hover", y="y_hover", source=hover_source)
# Create the main figure
main_plot = figure(width=400, height=300, title="Main Plot")
mp = main_plot.scatter(x='x', y='y', size=10, source=source)
# Configure the HoverTool to embed the figure in the tooltip
callback_code = """
//console.log("Entered callback")
const data = source.data;
const hoverData = hover_source.data;
const index = cb_data.index;
const geometry = cb_data.geometry;
if (hover_plot.visible)
hover_plot.visible = false;
console.log("off point set", hover_plot.visible);
if (index === null || index === undefined || index.indices.length === 0)
console.log("geometry for null index", geometry, index);
return;
const x = geometry.x;
const y = geometry.y;
let selected_index = null;
let min_distance = Infinity;
const sx = source.data['x'];
const sy = source.data['y'];
for (let i = 0; i < sx.length; i++) {
const dx = sx[i] - x;
const dy = sy[i] - y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < min_distance) {
min_distance = distance;
selected_index = i;
}
}
// Update hover data
hoverData['x_hover'] = [x - 0.5, x, x + 0.5];
hoverData['y_hover'] = [y - 0.3, y + 0.4, y - 0.3];
// Update the source.
hover_source.change.emit();
console.log("before setting visible true", hover_plot.visible);
hover_plot.visible = true;
console.log("after setting visible true", hover_plot.visible);
hover_plot.title.text=String(selected_index);
"""
hover = HoverTool()
# Define a CustomJS callback that updates hover data on hover
h_callback = CustomJS(args=dict(hover_source=hover_source, source=source, hover_plot=hover_plot), code=callback_code)
hover.callback = h_callback
main_plot.add_tools(hover)
print(main_plot, hover_plot, hover, hover.callback)
canvas = layout(column(main_plot, hover_plot))
# Show the main plot
save(canvas)