I am trying to get the index of nodes selected using box select from a GraphRender object in Bokeh in order to create a linked datatable. (I want to be able to get the index for a selected node)
The question is somewhat similar to: JavaScript callback to get selected glyph index in Bokeh however I was unable to solve it using their proposed solution.
The full code is below and I have tried to solve it with a custom JS callback but I was unable to do so.
Any help is much appreciated. Thanks in advance!
(Note: This is my first question so please let me know if further information is required.)
import pandas as pd
import numpy as np
from bokeh.layouts import row, widgetbox, column
from bokeh.models import ColumnDataSource, CustomJS, StaticLayoutProvider, Oval, Circle
from bokeh.models import HoverTool, TapTool, BoxSelectTool, GraphRenderer
from bokeh.models.widgets import RangeSlider, Button, DataTable, TableColumn, NumberFormatter
from bokeh.io import curdoc, show, output_notebook
from bokeh.plotting import figure
import networkx as nx
from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes, NodesOnly
# Import / instantiate networkx graph
G = nx.Graph()
G.add_edge('a', 'b', weight=0.6)
G.add_edge('a', 'c', weight=0.2)
G.add_edge('c', 'd', weight=0.1)
G.add_edge('c', 'e', weight=0.7)
G.add_edge('c', 'f', weight=0.9)
G.add_edge('a', 'd', weight=0.3)
# Node Characteristics
node_name = list(G.nodes())
positions = nx.spring_layout(G)
node_size = [k*4 for k in range(len(G.nodes()))]
nx.set_node_attributes(G, node_size, 'node_size')
visual_attributes=ColumnDataSource(
pd.DataFrame.from_dict({k:v for k,v in G.nodes(data=True)},orient='index'))
# Edge characteristics
start_edge = [start_edge for (start_edge, end_edge) in G.edges()]
end_edge = [end_edge for (start_edge, end_edge) in G.edges()]
weight = list(nx.get_edge_attributes(G,'weight').values())
edge_df = pd.DataFrame({'source':start_edge, 'target':end_edge, 'weight':weight})
# Create full graph from edgelist
G = nx.from_pandas_edgelist(edge_df,edge_attr=True)
# Convert full graph to Bokeh network for node coordinates and instantiate Bokeh graph object
G_source = from_networkx(G, nx.spring_layout, scale=2, center=(0,0))
graph = GraphRenderer()
# Update loop where the magic happens
def update():
selected_df = edge_df[(edge_df['weight'] >= slider.value[0]) & (edge_df['weight'] <= slider.value[1])]
sub_G = nx.from_pandas_edgelist(selected_df,edge_attr=True)
sub_graph = from_networkx(sub_G, nx.spring_layout, scale=2, center=(0,0))
graph.edge_renderer.data_source.data = sub_graph.edge_renderer.data_source.data
graph.node_renderer.data_source.data = G_source.node_renderer.data_source.data
graph.node_renderer.data_source.add(node_size,'node_size')
def selected_points(attr,old,new):
selected_idx = graph.node_renderer.selected.indices #does not work
print(selected_idx)
# Slider which changes values to update the graph
slider = RangeSlider(title="Weights", start=0, end=1, value=(0.25, 0.75), step=0.10)
slider.on_change('value', lambda attr, old, new: update())
# Plot object which is updated
plot = figure(title="Meetup Network Analysis", x_range=(-1.1,1.1), y_range=(-1.1,1.1),
tools = "pan,wheel_zoom,box_select,reset,box_zoom,crosshair", plot_width=800, plot_height=800)
# Assign layout for nodes, render graph, and add hover tool
graph.layout_provider = StaticLayoutProvider(graph_layout=positions)
graph.node_renderer.glyph = Circle(size='node_size')
graph.selection_policy = NodesOnly()
plot.renderers.append(graph)
plot.tools.append(HoverTool(tooltips=[('Name', '@index')]))
# Set layout
layout = column(slider,plot)
# does not work
#graph.node_renderer.data_source.on_change("selected", selected_points)
# Create Bokeh server object
curdoc().add_root(layout)
update()
Instead of putting the listener on graph.node_renderer.data_source.on_change, use this instead:
graph.node_renderer.data_source.selected.on_change("indices", selected_points)
This would trigger server-side response.
Hongyi