Get Selected Glyph from NetworkX Graph in Bokeh - javascript

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

Related

Want to display HoverTool tooltip data with the TapTool in bokeh

I am new to python and want to make an interactive chart with a dataset given in class. I want to be able to simply display the data associated with the point I select with the TapTool in Bokeh. The data is too dense to jsut simply use the hovertool though. It seems the hovertool did not require javascript, but apparently getting the tap tool requires knowing a whole other coding language. I am not familiar with python as it is, so adding a javascript callback is making things a bit complicated.
Right now I have this figure plotted with my dataframe using data from the California Housing dataset from sci-kit. I also have the US_states sampledata from bokeh to plot too.
from bokeh.layouts import layout
from bokeh.plotting import figure, show, output_file
from bokeh.models import Circle, Div, ColumnDataSource, CustomJS, MultiChoice, HoverTool, LinearColorMapper, ColorBar
from bokeh.io import output_notebook
from bokeh.sampledata import us_states
from bokeh.transform import transform
from bokeh.models.tools import *
#US state sample data
us_states = us_states.data.copy()
us_states = us_states["CA"]
#Creating figure and adding california basemap
color = LinearColorMapper(palette = 'Viridis256',
low = df.MedInc.min(),
high = df.MedInc.max())
color2 = LinearColorMapper(palette = 'Inferno256',
low = df.price.min(),
high = df.price.max())
cal = figure(title = "California Housing Data Geographic Distribution",
plot_width = 1000)
cal.circle('Longitude','Latitude',source = geo_source,
color= transform('MedInc', color),size =2,
alpha = 0.2,legend_label = "Median Income")
cal.circle('Longitude','Latitude',source = geo_source,
color= transform('price', color2),size =1,
alpha = 0.2,legend_label = "House Price")
color_bar = ColorBar(color_mapper = color,
label_standoff = 14,
location = (0,0),
title = 'Median Income')
color_bar2 = ColorBar(color_mapper = color2,
label_standoff = 14,
location = (0,0),
title = 'House Price')
cal.add_layout(color_bar,'right')
cal.add_layout(color_bar2,'right')
cal.legend.location = "top_right"
cal.legend.click_policy="hide"
I have no clue what to write for the JS callback for the TapTool though since I'm not really sure how it works, how the variables transfer, etc.
callback = CustomJS(args=dict(source=geo_source), code="""
//Insert JS code here
""")
taptool = TapTool(callback=callback)
geo_source.selected.js_on_change('indices', callback)
cal.add_tools(taptool)
you should add figure to Tap event
plot.on_event(Tap, function)
and in function you can use indices to update your data/graph
def function():
print(source.selected.indices)

Use Bokeh RadioGroup to plot selected subset of Pandas DataFrame within Jupyter

Goal
Plot subsets of rows in a Pandas DataFrame by selecting a specific value of a column.
Ideally plot it in jupyter notebook.
What I did
I have minimal knowledge of Javascript, so I have managed to plot by running Bokeh server with everything written in Python.
However, I couldn't make it in Jupyter notebook with a Javascript callback. My way of doing it sounds a bit stupid: splitting the DataFrame into subsets by values of a column and putting them into a dict, then I can select a subset by the active selection from a RadioGroup.
This is my code example:
import pandas as pd
import bokeh
from bokeh.io import output_notebook, show
import bokeh.plotting as bp
import bokeh.models as bm
from bokeh.layouts import column, row
data = {
'Datetime': ['2020-04-09T10:23:38Z', '2020-04-09T22:23:38Z','2020-04-09T23:23:38Z', '2020-01-09T10:23:38Z', '2020-01-09T22:23:38Z', '2020-01-09T23:23:38Z'],
'Month': ['Apr', 'Apr', 'Apr', 'Jan', 'Jan', 'Jan'],
'Values': [1.2, 1.3, 1.5, 1.1, 3, 1.3]
}
df = pd.DataFrame.from_dict(data)
month_list = df['Month'].unique().tolist()
plot_height = 600
plot_width = 1000
col2plot = 'Values'
month_dict = {}
for m in month_list:
subset = df[df['Month'] == m].reset_index(drop=True)
month_dict[m] = subset[['Datetime', col2plot]].to_dict()
p1 = bp.figure(
plot_height=plot_height,
plot_width=plot_width,
title='Values',
toolbar_location=None,
tools="hover",
tooltips=[("DateTime", "#Datetime")]
)
src = bm.ColumnDataSource(df[df['Month'] == 'Jan'].reset_index(drop=True))
p1.line(x='index', y=col2plot, alpha=0.8, source=src)
month_selector = bm.widgets.RadioGroup(labels=month_list, active=1)
jscode = """
var month = cb_obj.labels[cb_obj.active] //selected month
const new_data = source[month]
src.data = new_data
src.change.omit()
"""
callback = bm.CustomJS(args=dict(src=src, source=month_dict), code=jscode)
month_selector.js_on_change('active', callback)
output_notebook()
show(row(p1, month_selector))
The code runs but by selecting a certain month, the plot isn't updating. This is probably due to the bad handling of the JS callback, any ideas for fixing this? Thanks a lot for your help!
Issues with your code:
In p.line, you're using the index column. But when you call pd.DataFrame.to_dict(), the column is not there. Can be fixed by adding yet another .reset_index() before .to_dict()
to_dict() returns data in the form of a dict of dicts, but ColumnDataSource needs a dict of lists. Replace the call withto_dict('list')
src.change.omit() - a typo here, it should be emit. But since you're replacing the whole data attribute instead of just changing some of the data, you can simply remove the line altogether

Python Bokeh CustomJS: Debugging a JavaScript callback for the Taping-Tool

I am working with Python 3.6.2 and Bokeh 1.0.4 to create a custom JavaScript callback in my plot.
By tapping on one of the points in the plot, I'd like all points sharing the same attribute in the id-column to be highlighted.
Iterating over all datapoints with JavaScript and manipulating the respective 'selected'-attribute in the ColumnDataSource-object should do the trick.
Unfortunately I can not figure out how to correct this code.
# Import packages
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
# Create the data for the points
x = [0, 1, 2, 3]
y = [0, 1, 0, 1]
ids = ['A','B','A','B']
data = {'x':x, 'y':y, 'id':ids}
source = ColumnDataSource(data)
# Add tools to the plot
tap = TapTool()
hover = HoverTool(tooltips=[("X", "#x"),
("Y", "#y"),
("ID", "#id")])
# Create a plotting figure
p = figure(plot_width=400, plot_height=400, tools=[tap,hover])
# Code for the callback
code = """
// Set column name to select similar glyphs
var column = 'id';
// Get data from ColumnDataSource
var data = source.data;
// Get indices array of all selected items
var selected = source.selected.indices;
// Array to store glyph-indices to highlight
var select_inds = [];
// Check if only a single glyph is selected
if(selected.length==1){
// Get the value of the column to find similar attributes/glyphs
attribute_value = data[column][selected[0]];
// Iterate over all entries in the ColumnDataSource
for (var i=0; i<data[column].length; ++i){
// Check if items have the same attribute
if(data[column][i]==attribute_value){
// Add index to selected list
select_inds.push(i);
}
}
}
// Set selected glyphs in ColumnDataSource
source.selected.indices = select_inds;
// Save changes to ColumnDataSource
source.change.emit();
"""
# Create a CustomJS callback with the code and the data
callback = CustomJS(args={'source':source}, code=code)
# Add the callback to the ColumnDataSource
source.callback=callback
# Plots circles
p.circle('x', 'y', source=source, size=25, color='blue', alpha=1, hover_color='black', hover_alpha=1)
# Show plot
show(p)
An older version of this problem with Bokeh 0.13.0 could not solve my problem.
You were almost there! The callback has to be added to the TapTool instead of the ColumnDataSource.
# Import packages
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
# Create the data for the points
x = [0, 1, 2, 3]
y = [0, 1, 0, 1]
ids = ['A','B','A','B']
# Generate data source for the visualization
data = {'x':x, 'y':y, 'id':ids}
source = ColumnDataSource(data)
# Add tools to the plot
tap = TapTool()
hover = HoverTool(tooltips=[("X", "#x"),
("Y", "#y"),
("ID", "#id")])
# Create a plotting figure
p = figure(plot_width=400, plot_height=400, tools=[tap,hover])
# Code for the callback
code = """
// Set column name to select similar glyphs
var column = 'id';
// Get data from ColumnDataSource
var data = source.data;
// Get indices array of all selected items
var selected = source.selected.indices;
// Array to store glyph-indices to highlight
var select_inds = [];
// Check if only a single glyph is selected
if(selected.length==1){
// Get the value of the column to find similar attributes/glyphs
var attribute_value = data[column][selected[0]];
// Iterate over all entries in the ColumnDataSource
for (var i=0; i<data[column].length; ++i){
// Check if items have the same attribute
if(data[column][i]==attribute_value){
// Add index to selected list
select_inds.push(i);
}
}
}
// Set selected glyphs in ColumnDataSource
source.selected.indices = select_inds;
// Save changes to ColumnDataSource
source.change.emit();
"""
# Create a CustomJS callback with the code and the data
callback = CustomJS(args={'source':source}, code=code)
# Add the callback to the taptool
tap.callback=callback
# Plots circles
p.circle('x', 'y', source=source, size=25, color='blue', alpha=1, hover_color='black', hover_alpha=1)
# Show plot
show(p)

Interactive Slider using Bokeh

I'm trying to use a bokeh interactive slider to modify the contents of a plot, similar the example here. I have a two nested lists x and y.
I simply want the slider to change the index of the lists to plot. i.e. If the slider index = 0, then plot x[0] vs y[0], if the slider index is 1, plot x[1] vs y[1], etc...
The documentation example computes the new data on the fly, which is not feasible for the data that I need to work with.
When I run the code below, nothing shows up in the plot... I don't know javascript, so I'm guessing this is where I'm going wrong.
I'm running Python 3.5 and Bokeh 0.12. This is all run within a jupyter-notebook.
import numpy as np
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, show
from bokeh.io import output_notebook
from bokeh.resources import INLINE
output_notebook(INLINE)
x = [[x*0.05 for x in range(0, 500)],
[x*0.05 for x in range(0, 500)]]
y = [np.sin(x[0]),
np.cos(x[1])]
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=400, plot_height=400)
plot.line('x'[0], 'y'[0], source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value');
x = data['x'][f];
y = data['y'][f];
source.trigger('change');
""")
slider = Slider(start=0, end=1, value=0, step=1, title="index", callback=callback)
layout = row(plot, slider)
show(layout)
Instead of having a slider changing the index of the data to be plotted, you could define two ColumnDataSources: source_visible and source_available where the first one holds the data that is currently being shown in the plot and the second one acts as a data repository from where we can sample data in CustomJS callback based on user selection on the web page:
import numpy as np
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, Slider, CustomJS
from bokeh.plotting import Figure, show
# Define data
x = [x*0.05 for x in range(0, 500)]
trigonometric_functions = {
'0': np.sin(x),
'1': np.cos(x),
'2': np.tan(x),
'3': np.arctan(x)}
initial_function = '0'
# Wrap the data in two ColumnDataSources
source_visible = ColumnDataSource(data=dict(
x=x, y=trigonometric_functions[initial_function]))
source_available = ColumnDataSource(data=trigonometric_functions)
# Define plot elements
plot = Figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source_visible, line_width=3, line_alpha=0.6)
slider = Slider(title='Trigonometric function',
value=int(initial_function),
start=np.min([int(i) for i in trigonometric_functions.keys()]),
end=np.max([int(i) for i in trigonometric_functions.keys()]),
step=1)
# Define CustomJS callback, which updates the plot based on selected function
# by updating the source_visible ColumnDataSource.
slider.callback = CustomJS(
args=dict(source_visible=source_visible,
source_available=source_available), code="""
var selected_function = cb_obj.value;
// Get the data from the data sources
var data_visible = source_visible.data;
var data_available = source_available.data;
// Change y-axis data according to the selected value
data_visible.y = data_available[selected_function];
// Update the plot
source_visible.change.emit();
""")
layout = row(plot, slider)
show(layout)
Keep in mind that if your data is large, it might take a while to send it all at once to the client's browser.

Dependent Sliders with Bokeh, how to write the callbacks

I want to be able to slide through many plots which are the result of simulations accross 3+ dimensions. I am using the Bokeh package via Python.
For simplicity, let's assume I have two dimensions : d, and nc. But nc depends on d in the following way:
if d=100, nc=56,57
if d=20, nc=5,6
And I have 4 pictures:
d_100_nc_56.png,
d_100_nc_57.png,
d_20_nc_5.png,
d_20_nc_6.png
So I want two sliders, one for d, and one for nc, to cycle the .png images through the image_url function of Bokeh.plotting.Figure . However, the value of the nc slider should update itself as I change the slider in d
from bokeh.io import vform
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, output_file, show
output_file('image.html')
source = ColumnDataSource(data=dict(url=['d_100_nc_55.png']))
p = Figure(x_range=(0,1), y_range=(0,1))
callback_nc = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
old = data['url'][0]
to_replace=old.substring(old.lastIndexOf("nc_")+3,old.lastIndexOf(".png"))
data['url'][0] = old.replace(to_replace,f.toString(10))
source.trigger('change');
""")
callback_d = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
old = data['url'][0]
to_replace=old.substring(old.lastIndexOf("d_")+2,old.lastIndexOf("_nc_"))
data['url'][0] = old.replace(to_replace,f.toString(10))
source.trigger('change');
""")
p.image_url('url',source=source, x=0, y=1,w=1,h=1)
p.text(x=0,y=0,text=source.data['url'])
slider_nc = Slider(start=55, end=65, value=1, step=1, title="nc", callback=callback_nc)
slider_d = Slider(start=20, end=100, value=100, step=80, title="density", callback=callback_d)
layout = vform(slider_nc,slider_d, p)
show(layout)
However, I do not know how to pass the d slider as an argument to the nc slider to fetch its properties and update them on the fly. Is this possible ? Otherwise it limits the use of multiple sliders through bokeh quite substantially.
Edit: updated for more recent versions
You pass the slider the same way you pass source, as an item in the args dictionary. Any Python-side Bokeh model you pass there is automatically made available to the callback. Then, BokehJS model properties exactly match the python properties described in the reference guide Here is an example that updates one slider based off another:
# Example from Bokeh 0.12.x
from bokeh.plotting import show, output_file
from bokeh.layouts import column
from bokeh.models import CustomJS, Slider
s1 = Slider(start=1, end=10, value=1, step=1)
s2 = Slider(start=0, end=1, value=0, step=1)
s1.callback = CustomJS(args=dict(s1=s1, s2=s2), code="""
s2.end = s1.value;
""")
output_file("foo.html")
show(column(s1,s2))

Categories

Resources