Highlighting multiple hex_tiles by hovering in bokeh

Question:

I try to visualize my data in a hex map. For this I use python bokeh and the corresponding hex_tile function in the figure class. My data belongs to one of 8 different classes, each having a different color. The image below shows the current visualization:

Current Visualization
I would like to add the possibility to change the color of the element (and ideally all its class members) when the mouse hovers over it.

I know, that it is somewhat possible, as bokeh themselves provide the following example:
https://docs.bokeh.org/en/latest/docs/gallery/hexbin.html

However, I do not know how to implement this myself (as this seems to be a feature for the hexbin function and not the simple hex_tile function)

Currently I provide my data in a ColumnDataSource:

source = ColumnDataSource(data=dict(
r=x_row,
q=y_col,
color=colors_array,
ipc_class=ipc_array
))

where “ipc_class” describes one of the 8 classes the element belongs to.
For the mouse hover tooltip I used the following code:

TOOLTIPS = [
("index", "$index"),
("(r,q)", "(@r, @q)"),
("ipc_class", "@ipc_class")
]

and then I visualized everything with:

p = figure(plot_width=1600, plot_height=1000, title="Ipc to Hexes with colors", match_aspect=True,
       tools="wheel_zoom,reset,pan", background_fill_color='#440154', tooltips=TOOLTIPS)
p.grid.visible = False
p.hex_tile('q', 'r', source=source, fill_color='color')

I would like the visualization to add a function, where hovering over one element will result in one of the following:
1. Highlight the current element by changing its color
2. Highlight multiple elements of the same class when one is hovered over by changing its color
3. Change the color of the outer line of the hex_tile element (or complete class) when the element is hovered over

Which of these features is possible with bokeh and how would I go about it?

EDIT:
After trying to reimplement the suggestion by Tony, all elements will turn pink as soon as my mouse hits the graph and the color won´t turn back. My code looks like this:

source = ColumnDataSource(data=dict(
    x=x_row,
    y=y_col,
    color=colors_array,
    ipc_class=ipc_array
))

p = figure(plot_width=800, plot_height=800, title="Ipc to Square with colors", match_aspect=True,
           tools="wheel_zoom,reset,pan", background_fill_color='#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source=source, fill_color='color')

###################################

code = ''' 
for (i in cb_data.renderer.data_source.data['color'])
    cb_data.renderer.data_source.data['color'][i] = colors[i];

if (cb_data.index.indices != null) {
    hovered_index = cb_data.index.indices[0];
    hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
    for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
        if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
            cb_data.renderer.data_source.data['color'][i] = 'pink';
    }
}
cb_data.renderer.data_source.change.emit();
'''

TOOLTIPS = [
    ("index", "$index"),
    ("(x,y)", "(@x, @y)"),
    ("ipc_class", "@ipc_class")
]

callback = CustomJS(args=dict(colors=colors), code=code)
hover = HoverTool(tooltips=TOOLTIPS, callback=callback)
p.add_tools(hover)
########################################

output_file("hexbin.html")

show(p)

basically, I removed the tooltips from the figure function and put them down to the hover tool. As I already have red in my graph, I replaced the hover color to “pink”. As I am not quite sure what each line in the “code” variable is supposed to do, I am quite helpless with this. I think one mistake may be, that my ColumnDataSource looks somewhat different from Tony’s and I do not know what was done to “classifiy” the first and third element, as well as the second and fourth element together. For me, it would be perfect, if the classification would be done by the “ipc_class” variable.

Asked By: Daniel Töws

||

Answers:

Maybe something like this to start with (Bokeh v1.1.0):

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

colors = ["green", "blue", "green", "blue"]
source = ColumnDataSource(dict(r = [0, 1, 2, 3], q = [1, 1, 1, 1], color = colors))
plot = figure(plot_width = 300, plot_height = 300, match_aspect = True)
plot.hex_tile('r', 'q', fill_color = 'color', source = source)

code = ''' 
for (i in cb_data.renderer.data_source.data['color'])
    cb_data.renderer.data_source.data['color'][i] = colors[i];

if (cb_data.index.indices != null) {
    hovered_index = cb_data.index.indices[0];
    hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
    for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
        if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
            cb_data.renderer.data_source.data['color'][i] = 'red';
    }
}
cb_data.renderer.data_source.change.emit();
'''
callback = CustomJS(args = dict(colors = colors), code = code)
hover = HoverTool(tooltips = [('R', '@r')], callback = callback)
plot.add_tools(hover)
show(plot)

Result:

enter image description here

Answered By: Tony

Following the discussion from previous post here comes the solution targeted for the OP code (Bokeh v1.1.0). What I did is:

1) Added a HoverTool

2) Added a JS callback to the HoverTool which:

  • Resets the hex colors to the original ones (colors_array passed in the callback)
  • Inspects the index of currently hovered hex (hovered_index)
  • Gets the ip_class of currently hovered hex (hovered_ip_class)
  • Walks through the data_source.data['ip_class'] and finds all hexagons with the same ip_class as the hovered one and sets a new color for it (pink)
  • Send source.change.emit() signal to the BokehJS to update the model

The code:

from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool

colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']

source = ColumnDataSource(data = dict(
    x = x_row,
    y = y_col,
    color = colors_array,
    ipc_class = ipc_array
))

p = figure(plot_width = 800, plot_height = 800, title = "Ipc to Square with colors", match_aspect = True,
           tools = "wheel_zoom,reset,pan", background_fill_color = '#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source = source, fill_color = 'color')

###################################
code = ''' 
for (let i in cb_data.renderer.data_source.data['color'])
    cb_data.renderer.data_source.data['color'][i] = colors[i];

if (cb_data.index.indices != null) {
    const hovered_index = cb_data.index.indices[0];
    const hovered_ipc_class = cb_data.renderer.data_source.data['ipc_class'][hovered_index];
    for (let i = 0; i < cb_data.renderer.data_source.data['ipc_class'].length; i++) {
        if (cb_data.renderer.data_source.data['ipc_class'][i] == hovered_ipc_class)
            cb_data.renderer.data_source.data['color'][i] = 'pink';
    }
}
cb_data.renderer.data_source.change.emit();
'''

TOOLTIPS = [
    ("index", "$index"),
    ("(x,y)", "(@x, @y)"),
    ("ipc_class", "@ipc_class")
]

callback = CustomJS(args = dict(ipc_array = ipc_array, colors = colors_array), code = code)
hover = HoverTool(tooltips = TOOLTIPS, callback = callback)
p.add_tools(hover)
########################################

output_file("hexbin.html")

show(p)

Result:

enter image description here

Answered By: Tony

Another approach is to update cb_data.index.indices to include all those indices that have ipc_class in common, and add hover_color="pink" to hex_tile. So in the CustomJS code one would loop the ipc_class column and get the indices that match the ipc_class of the currently hovered item.
In this setup there is not need to update the color column in the data source.

Code below tested used Bokeh version 3.0.2.

from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool

colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']

source = ColumnDataSource(data = dict(
    x = x_row,
    y = y_col,
    color = colors_array,
    ipc_class = ipc_array
))

plot = figure(
    width = 800,
    height = 800, 
    title = "Ipc to Square with colors",
    match_aspect = True,
    tools = "wheel_zoom,reset,pan",
    background_fill_color = '#440154'
)
plot.grid.visible = False
plot.hex_tile(
    'x', 'y',
    source = source,
    fill_color = 'color',
    hover_color = 'pink' # Added!
)

code = '''
  const hovered_index = cb_data.index.indices;
  const src_data = cb_data.renderer.data_source.data;
  if (hovered_index.length > 0) {
    const hovered_ipc_class = src_data['ipc_class'][hovered_index];
    var idx_common_ipc_class = hovered_index;
    for (let i = 0; i < src_data['ipc_class'].length; i++) {
      if (i === hovered_index[0]) {
        continue;
      }
      if (src_data['ipc_class'][i] === hovered_ipc_class) {
        idx_common_ipc_class.push(i);
      }
    }
    cb_data.index.indices = idx_common_ipc_class;
    cb_data.renderer.data_source.change.emit();
  }
'''

TOOLTIPS = [
    ("index", "$index"),
    ("(x,y)", "(@x, @y)"),
    ("ipc_class", "@ipc_class")
]

callback = CustomJS(code = code)
hover = HoverTool(
    tooltips = TOOLTIPS,
    callback = callback
)
plot.add_tools(hover)

output_file("hexbin.html")
show(p)
Answered By: jonas
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.