Hide Nodes and Edges in Graph in Javascript

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:

  1. 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
  1. 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 itself

    new_graph= gen_graph()
    new_graph.layout_provider = layout_provider # exact same layout object

    plot = 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)
    
  2. 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