Weird problem with selection on_change callback patching

Hi!

I’m trying to do some network visualization. I want to add callback: if some nodes are selected, change their “selected” attribute (and do lots of other stuff with their neighbors, but it is not relevant for this problem). I also need to obtain their “index” attribute form source networkx graph.

This is minimal example:

#!/usr/bin/env python3
import random as rnd
import networkx as nx
from bokeh.plotting import figure, curdoc, from_networkx
from bokeh.models import Hex
from bokeh.transform import linear_cmap
from functools import partial


def change_selection(positions,nodes_data_source,value_to_selected):
	selected_indicies = []
	nodes_data = nodes_data_source.data

	patch_data = []

	for p in positions:
		node_index = nodes_data["index"][p]
		selected_indicies.append(node_index)
		patch_data.append((p,value_to_selected))

	print(patch_data) # debuging print
	nodes_data_source.patch({'selected': patch_data})

	return selected_indicies

def update_renderers_according_selection(attr, old, new, this_renderer):
	old_set = set(old)
	new_set = set(new)
 
	added_set = new_set.difference(old_set)
	removed_set = old_set.difference(new_set)

	# indicies removed from selection and added to selection
	removed = change_selection(removed_set,this_renderer.node_renderer.data_source,0)
	added = change_selection(added_set,this_renderer.node_renderer.data_source,1)
 
	# ... other manipulation with removed and added nodes, irelevant for this example


# generate some random nodes like (some id, {"coor":(x,y), "selected":0}) 
random_nodes = [(i+10,{"coor":(rnd.randrange(-100,100),rnd.randrange(-100,100)), "selected":0}) for i in range(3000)]

# create networkx graph
g = nx.Graph()
g.add_nodes_from(random_nodes)

layout = {n:g.nodes[n]["coor"] for n in g.nodes()}

# blue unselected and red selected nodes
# the mapper is used because in the whole app are more levels of "selected" attribute value (like selected=1, neighbor_of_selected=2 ....)
mapper_nodes = linear_cmap(field_name='selected', palette=('#a1cdec', '#f20808') ,low=0 ,high=1)

# create graph renderer from networkx graph
graph_renderer = from_networkx(g,layout)
graph_renderer.node_renderer.glyph = Hex(fill_color=mapper_nodes,line_color=None)
graph_renderer.node_renderer.nonselection_glyph = Hex(fill_color=mapper_nodes,line_color=None)

nodes_data_source = graph_renderer.node_renderer.data_source

# nodes selected.on_change callback
nodes_data_source.selected.on_change("indices",
									partial(update_renderers_according_selection,
											this_renderer=graph_renderer))


plot = figure(x_range=(-100,100),y_range=(-100,100),tools="lasso_select,pan,wheel_zoom")

plot.renderers.append(graph_renderer)
curdoc().add_root(plot)

It somehow works, but if there are lots of nodes (like in this example), there is weird problem during selecting them - all the nodes sometimes collapsed to the center of the plot. It returns back if something happens, like zoom, selecting or move, but after that, sometimes are selected the right nodes, but sometimes are all selected/unselected. I have no idea why.

Can you help me please? What is the problem?

I played with the example a bit and came up with what seems to be a minimal one. If you click on any dot (perhaps multiple times), all the dots should collapse into one at (0, 0). Feels like some sort of race condition when any update from the data source arrives.

from bokeh.models import GraphRenderer, StaticLayoutProvider
from bokeh.plotting import figure, curdoc

D = 40
gr = GraphRenderer()
gr.node_renderer.glyph.size = 10
ds = gr.node_renderer.data_source
ds.data = dict(index=[i for i in range(D * D)])

gr.layout_provider = StaticLayoutProvider(graph_layout={idx: (idx % D, idx // D)
                                                        for idx in ds.data['index']})


def update_renderers_according_selection(attr, old, new):
    ds.patch(dict(index=[]))


ds.selected.on_change("indices", update_renderers_according_selection)

plot = figure(x_range=(-1, D), y_range=(-1, D), tools="tap")
plot.renderers.append(gr)
curdoc().add_root(plot)

Created https://github.com/bokeh/bokeh/issues/10474

1 Like

Thank you very much for your investigation!