I'm using Bokeh to create an interactive visualisation of a NetworkX graph. What I want to do is display all edges connected to a particular node when I hover over that node with the mouse.
In the Bokeh User Guide there's an example that does more or less what I want, but I'm not satisfied with this solution for two reasons:
- The line segments are newly drawn on each hover event, which means they are shown over the node circles, which looks ugly. (This isn't noticeable in the example since the lines and the nodes are the same colour).
- My graph is weighted, with the width of an edge depending on its weight. There's no way (as far as I'm aware) to get the highlighted edges to the correct width.
So, I've tried another approach: draw all edges from the beginning, but set their visible
attributes to False
. Then create a dictionary with the node indices as keys and a list of the edges connected to that node as values. When the node is moused over, that node's edges should get their visible
attribute changed to True
. It looks more or less like this:
global glob_edges_by_node_index
edges_by_node = {}
for edge in G.edges(data=True): # create the segments (G is a preexisting NetworkX graph)
u,v,d = edge
x_0 = pos[u][0] # pos is a preexisting dictionary of x and y values for each node
y_0 = pos[u][1]
x_1 = pos[v][0]
y_1 = pos[v][1]
width = 0.03*d['weight']
highlit_edge = p.segment(x0=[x_0],x1=[x_1],y0=[y_0],y1=[y_1],color='#379bdd',line_width=width) # p is a preexisting Bokeh figure
highlit_edge.visible = False # all segments are invisible at the start
edges_by_node[u].append(highlit_edge) # put the segment into both nodes' dictionary entries
edges_by_node[v].append(highlit_edge)
id_to_index = {}
edges_by_node_index = {}
i = 0
for node_id in G.nodes(): # convert the dict keys from their original IDs to the indices seen by Bokeh
id_to_index[node_id] = i
edges_by_node_index[i] = edges_by_node[node_id]
i = i + 1
global glob_edges_by_node_index = edges_by_node_index
nodesource = ColumnDataSource(data=dict(x=x,y=y,sizes=nodesizes,colours=nodecolours)) # refers to preexisting data about the nodes
cr = p.circle('x','y',color='colours',alpha=0.7,hover_color='colours',hover_alpha=1.0,size='sizes',line_width=1,line_color='#000000',hover_line_color='#000000',source=nodesource) # draw the nodes themselves
p.add_tools(HoverTool(tooltips=None,callback=CustomJS.from_py_func(on_hover),renderers=[cr]))
def on_hover(window=None):
indices = cb_data['index']
for ind in indices:
highlit_edges = glob_edges_by_node_index[ind]
for highlit_edge in highlit_edges:
highlit_edge.visible = True # set edges to visible if they're in the dict entry of the hovered-over node
This does not work, and I'm a bit stumped as to how to fix it. Especially the use of cb_data
is a mystery to me – despite lots of Googling I haven't been able to find a clear and comprehensive reference of what information cb_data
contains, in what format, and how to access it. Any help would be much appreciated!