As I’ve been myself looking for a while across all posts on the net to perform this I thought I would share one solution I finally found to succeed in this. How to basically build and display a graph in Bokeh (from Networkx or other means) as using GraphRenderer() and finally be able to turn off (hide) parts of the graph nodes or edges based on some criteria - triggered from any widgets in javascript events. BTW, this is without using Bokeh server.
I will post in attachment a minimal source to share an example if I can. Unless Bokeh developers can tell better solutions, I have been successful performing this as following the following technique:
- Build Graph and GraphRenderer as normal and add Glyphs of your choice also as normal.
def gen_graph():
graph = GraphRenderer()
graph.node_renderer.glyph = Scatter(marker=marker_by_this_attribute, size=size_by_this_attribute, fill_color=color_by_this_attribute) # , line_color=color_by_this_attribute, size=size_by_this_attribute
graph.node_renderer.hover_glyph = Scatter(marker=marker_by_this_attribute, size=size_by_this_attribute, fill_color=node_highlight_hover_color, line_width=2)
graph.node_renderer.selection_glyph = Scatter(marker=marker_by_this_attribute, size=size_by_this_attribute, fill_color=node_highlight_select_color, line_width=2)
graph.node_renderer.nonselection_glyph = Scatter(line_width=1, fill_alpha=0.2, line_alpha=0.2, fill_color=color_by_this_attribute)
#graph.node_renderer.muted_glyph = Scatter(line_width=0, fill_alpha=0, line_alpha=0, fill_color=color_by_this_attribute)
graph.edge_renderer.glyph = MultiLine(line_alpha=0.5, line_color=edge_color, line_join=‘round’, line_width=‘line_width’) # line_width=‘line_width’, line_color=“white” Spectral4[0] line_alpha=‘line_alpha’
graph.edge_renderer.glyph.line_width = {‘field’: ‘line_width’}
graph.edge_renderer.hover_glyph = MultiLine(line_alpha=0.5, line_color=edge_highlight_hover_color, line_width=‘line_width’)
graph.edge_renderer.selection_glyph = MultiLine(line_alpha=1, line_color=edge_highlight_select_color, line_width=‘line_width’)
graph.edge_renderer.nonselection_glyph = MultiLine(line_alpha=0.2, line_color=edge_non_select_color, line_width=‘line_width’)
#graph.edge_renderer.muted_glyph = MultiLine(line_alpha=0.2, line_color=edge_non_select_color, line_width=‘line_width’) - this does not work…
#Highlight nodes and edges
graph.selection_policy = NodesAndLinkedEdges()
graph.inspection_policy = NodesAndLinkedEdges()
return graph
-
In my example I have created multiple renderers with parts of the graph and append’ed to same plot and used visible feature to turn off a full part of the graph this way purely turning off a full renderer.
Then created layout using Networkx spring layout and adjusted labels.graph_layout= networkx.spring_layout(G, scale=20, center=(0, 0)) # dict {node: array (x,y)}
layout_provider = StaticLayoutProvider(graph_layout=graph_layout)
x, y = zip(*layout_provider.graph_layout.values()) # this returns X, Y nodes used for label positioning
y = [y[i]+.04 for i in range(len(x))] # label a little higher than the node center itselfnew_graph= gen_graph()
new_graph.layout_provider = layout_provider # exact same layout objectplot = figure(tools=“box_select,pan,box_zoom,wheel_zoom,doubletap,save,undo,redo,reset”, active_scroll=‘wheel_zoom’, min_width=min_width, aspect_ratio=aspect_ratio, sizing_mode=‘scale_both’, width_policy=‘max’, height_policy=‘max’,
x_range=Range1d(-22, 28), y_range=Range1d(-20, 22), title=title, toolbar_location= ‘above’, output_backend=“webgl”) #plot_height=plot_height,plot.renderers.append(new_graph)
-
but this is not the main topic of this post. It was the easy part! How can we turn off basically programmatically parts of the graphs based on some criteria - mostly based on attributes nodes values for instance - and without any Bokeh crash eventually! In my example I use some dict data that I build beforehand which tells which layer of the graph each node are part of - but it could be using anything else - now that I know how to hide nodes, i’ll use a different technique triggering this.
a) add a copy.deepcopy of all renderers attached to the plot such as
backup_data_renderers['All'][layer]= [ # this is used to restore visualization when sub selection is closed
copy.deepcopy(new_graph.node_renderer.data_source.data),
copy.deepcopy(new_graph.edge_renderer.data_source.data),
copy.deepcopy(labels.source.data)
]
It will be used later as a master to further copies using
JSON.parse(JSON.stringify(backup[“All”][layer][0])); // insure deepcopy
as I have multiple renderers myself, one can then switch off renderer using this:
#Add Labels
source_label = ColumnDataSource({'x': x_list, 'y': y_list, 'name': new_nodes})
labels = LabelSet(x='x', y='y', text='name', source=source_label, background_fill_color='white', text_font_size='12px', background_fill_alpha=.7)
plot.renderers.append(labels)
# no label is default
plot.renderers[renderer*2+1].visible= False
b) Build and pass the backup deepcopy of the data renderer to your js_on_event call such as (I’ve got a bunch of widgets myself, this is one):
button_Territories.js_on_event("menu_item_click", CustomJS(args=dict(plot=plot, ms=multi_select, kw=sortedKWs, kwd=KW_dict, slider=slider, layers=layers, hover=hover,
tooltips=backup_hover_tooltips, checkbox=checkbox, checkbox2=checkbox2,
bc=button_Territories, bs=button_sources, rbc=radio_button_group, rbcl=COUNTERS,
backup=backup_data_renderers, nl=node_layer_dict,
coln=plot.renderers[0].node_renderer.data_source.column_names,
cole=plot.renderers[0].edge_renderer.data_source.column_names), code="""
//console.log('Dropdown: label/item=' + b.label + "/" + this.item);
bc.label= this.item;
if (bc.label == "All") {
bc.label= "Territories";
}
if (bc.label != "Territories"){
bs.label= "Sources";
}
"""+ bingo_code))
c) The master piece comes now, set in the “Bingo” code as below:
bingo_code="""
var v = slider.value/10;
var sublist= bc.label;
if (sublist == 'Territories') {
sublist= 'All';
}
if (bs.label != "Sources") {
sublist= bs.label;
}
var l= layers[sublist].length; // a virer
var lall= layers['All'];
for (var li=0; li < lall.length; li++) {
var p = plot.renderers[li*2];
var pl = plot.renderers[li*2+1];
var layer= lall[li];
p.node_renderer.data_source.data = JSON.parse(JSON.stringify(backup["All"][layer][0])); // insure javascript deepcopy
p.edge_renderer.data_source.data = JSON.parse(JSON.stringify(backup["All"][layer][1]));
pl.source.data= JSON.parse(JSON.stringify(backup["All"][layer][2]));
}
if (sublist != "All") { //checkbox3.active.includes(0)
// remove all unecessary nodes from layers
for (var li=0; li < lall.length; li++) {
var layer= lall[li];
var p = plot.renderers[li*2];
var pl = plot.renderers[li*2+1];
var pndata = p.node_renderer.data_source.data;
var pedata = p.edge_renderer.data_source.data;
var pldata = pl.source.data;
var keep_list= []; // build keep node index list for the layer
var keep_node_list= [];
for (var n=0; n < pndata['index'].length; n++) { // all nodes in data
var node= pndata['index'][n]; // use index for node list
if (node in nl && sublist in nl[node] && nl[node][sublist].includes(layer)) {
keep_list.push(n); // add index here in 'All'
keep_node_list.push(node); // add node name here used for Edges updates
}
else
pldata['name'][n]= ""
}
// FIX NODE attr LISTS for this layer and sublist
for (var a=0; a < coln.length; a++) { // all coln[a] attr dicts to change
var br= backup['All'][layer][0][coln[a]]; //[0] node data
pndata[coln[a]]= [];
for (var n=0; n < keep_list.length; n++) {
pndata[coln[a]].push(br[keep_list[n]]);
}
//this is the TRICK !! pad until size reached
if (n < br.length)
for (var n=n; n < br.length; n++) {
pndata[coln[a]].push(null);
}
}
// FIX EDGE LISTS from keep_node_list[]
keep_list= [];
for (var n=0; n < pedata['start'].length; n++) { // all edges in data
var edge_start= pedata['start'][n];
var edge_end= pedata['end'][n];
if (keep_node_list.includes(edge_start) && keep_node_list.includes(edge_end)) { // if node is present in any of the nodes start/end, but both to be present
keep_list.push(n); // keep this edge
}
}
for (var a=0; a < cole.length; a++) {
var br= backup['All'][layer][1][cole[a]]; //[1] edge
pedata[cole[a]]= [];
for (var n=0; n < keep_list.length; n++) {
pedata[cole[a]].push(br[keep_list[n]]);
}
//pad until size reached - this is the TRICK to hide Edges
if (n < br.length)
for (var n=n; n < br.length; n++) {
pedata[cole[a]].push(null);
}
}
} // end layers in 'All'
}
"""+ redraw_code
Thanks god the code does not crash - no error in navigator… Basically with this technique guys setting null to the nodes and edges start/end you need to hide, the bokeh code seems to skip the nodes and edges rendering without crashing or even setting warning messages. The only mandatory thing is to keep the same number of nodes & edges even if parts are set to null.
Of course once this is done, the recovery of the hidden parts should be the reverse function copying back the original nodes and edges data back from deepcopy backup (I’m using JSON parse for that as you saw.
I have tried many other techniques without success including trying to fix the layout X/Y or shortening the list of Nodes/Edges in the renderers data but - hell this always will bring a Size error reported and crash browser eventually.
Dear Bokeh developer - I would be happy to see a feature one day as setting visible property on nodes with automatic ‘hide’ of all connected edges (or not). This feature really is useful for graph as it is always required to drill down into graphs or browsing through as in a forest cutting trees to progress inside is often necessary…
Enjoy your day guys. Happy to answer questions if needed. Sorry about my example not totally functional but at least you get the point… and one applying this will save hours and days (for me) looking for a solution to hide nodes/edges programmatically.
Thierry