6

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:

  1. 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).
  2. 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!

1 Answers1

0

You can add a hover tool to highlight the connected edges on hover, but also set the default line alpha to zero:

import networkx as nx

from bokeh.models import Range1d, MultiLine, Circle, HoverTool
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.plotting import figure, show

G=nx.karate_club_graph()

plot = figure(plot_width=400, plot_height=400,
            x_range=Range1d(-1.1,1.1), y_range=Range1d(-1.1,1.1))
plot.add_tools(HoverTool(tooltips=None))

r = from_networkx(G, nx.circular_layout, scale=1, center=(0,0))

r.node_renderer.glyph = Circle(size=15, fill_color='#2b83ba')
r.node_renderer.hover_glyph = Circle(size=15, fill_color='#abdda4')

r.edge_renderer.glyph = MultiLine(line_alpha=0, line_width=5)  # zero line alpha
r.edge_renderer.hover_glyph = MultiLine(line_color='#abdda4', line_width=5)

r.inspection_policy = EdgesAndLinkedNodes()
plot.renderers.append(r)

show(plot)

enter image description here

bigreddot
  • 33,642
  • 5
  • 69
  • 122