I want to be able to select different underlying data to change the colors of nodes in a NetworkRenderer object by using JS callbacks, but the plot is not actually updated unless I use some tricks.
I have tried to use CustomJS to change the 'fill_value'
property of the node_renderer
to use a different column in the node_renderer.data_source
, but that does not update the plot (even though I can see that the 'fill_value
' is correctly changed if I do some logging in the browser console).
The only way that I have managed to actually get the colors to change is the following. In essence I need to copy different data in the node_renderer.data_source
, which would be ok, BUT that is not enough: I can get the colors to actually change only when I run another callback that change some property of the nodes via the js_link
method (in this example I change the node size with a spinner) - as soon as I change the size the colors get updated and the process needs to be repeated to get them to change again.
I believe this is not a normal behavior, but is it a bug or a problem with my code?
import numpy as np
import networkx as nx
from bokeh.models import Circle, MultiLine, Select, Spinner, CustomJS
from bokeh.plotting import figure, from_networkx, show
from bokeh.palettes import Viridis256
from bokeh.layouts import column
#starting from this example: https://docs.bokeh.org/en/latest/docs/examples/topics/graph/node_and_edge_attributes.html
G = nx.karate_club_graph()
SAME_CLUB_COLOR, DIFFERENT_CLUB_COLOR = "darkgrey", "red"
edge_attrs = {}
for start_node, end_node, _ in G.edges(data=True):
edge_color = SAME_CLUB_COLOR if G.nodes[start_node]["club"] == G.nodes[end_node]["club"] else DIFFERENT_CLUB_COLOR
edge_attrs[(start_node, end_node)] = edge_color
nx.set_edge_attributes(G, edge_attrs, "edge_color")
plot = figure(width=400, height=400, x_range=(-1.2, 1.2), y_range=(-1.2, 1.2),
x_axis_location=None, y_axis_location=None, toolbar_location=None,
title="Graph Interaction Demo", background_fill_color="#efefef",
tooltips="index: @index, club: @club")
plot.grid.grid_line_color = None
graph_renderer = from_networkx(G, nx.spring_layout, scale=1, center=(0, 0))
#Some random data for the node colors
cds = ColumnDataSource({'Colors1': [Viridis256[int(val*255)] for val in np.random.rand(len(G.nodes))],
'Colors2': [Viridis256[int(val*255)] for val in np.random.rand(len(G.nodes))]})
#Copy it in the node_renderer
graph_renderer.node_renderer.data_source.data['color'] = cds.data['Colors1']
graph_renderer.node_renderer.glyph = Circle(size=15, fill_color="color")
graph_renderer.edge_renderer.glyph = MultiLine(line_color="edge_color",
line_alpha=1, line_width=2)
#Define the widgets for the interactions
sel = Select(value='Colors1', options=['Colors1', 'Colors2'])
spin = Spinner(title="Glyph size", low=1, high=40, step=0.5, value=10, width=80)
callback = CustomJS(args=dict(graph_renderer=graph_renderer, cds=cds),
code="""
var selection = cb_obj.value
graph_renderer.node_renderer.data_source.data['color'] = cds.data[selection]
graph_renderer.node_renderer.change.emit()
""")
sel.js_on_change('value', callback)
spin.js_link('value', graph_renderer.node_renderer.glyph, 'size')
#Make the plot
plot.renderers.append(graph_renderer)
my_lay = column(sel, spin, plot)
show(my_lay)
EDIT: should it be useful to others, the correct callback as per the accepted answer should be:
callback = CustomJS(args=dict(graph_renderer_ds=graph_renderer.node_renderer.data_source, cds=cds),
code="""
var selection = cb_obj.value
graph_renderer_ds.data['color'] = cds.data[selection]
graph_renderer_ds.change.emit()
""")
It is the data source that is watched for change events to trigger a redraw
graph_renderer.node_renderer.data_source.change.emit()
The callback could be simplified by configuring the data source itself in args
rather then the graph renderer.