Search code examples
pythonbokeh

Python Bokeh: CustomJS does not update the colors of the nodes in a NetworkRenderer


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()
                    """)

Solution

  • 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.