Search code examples
pythonjupyter-notebookbokehpython-interactive

Bokeh Plot in Jupyter Notebook not updating


I want to plot some data that has multiple features, and want to make an interactive 2d plot where the user can choose the axis from a list of features, to see how any two features are related. However, in the code I have, the plot does not update based on user input.

I'm using Jupyter notebook, and am trying to do the plots with the bokeh package. I want to stick to using bokeh widgets, rather than iPython widgets. Any help would be greatly appreciated.

Here is some minimal code

import numpy as np
import pandas as pd
from bokeh.layouts import row, widgetbox
from bokeh.models import CustomJS, Slider, Select
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.io import push_notebook, output_notebook, curdoc
from bokeh.client import push_session
output_notebook()

#create sample pandaframe to work with, this will store the actual data
a = np.arange(50).reshape((5,10))
labels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
val_a = pd.DataFrame(a, columns=labels )

# Here is a dict of some keys that I want to be able to pick from for plotting
axis_map = {
    "A": "A",
    "B": "B",
    "C": "C"
}

#This is to update during the callback
code = ''' var data = val_a;
           var val1 = x_axis.value;
           var val2 = y_axis.value;
           x = data['val1'];
           y = data['val2'];
           source.trigger('change');
           print x
            '''
source = ColumnDataSource(data=dict(x=[], y=[]))
callback = CustomJS(args=dict(source=source), code=code)

#Create two select widgets to pick the features of interest 
x_axis = Select(title="X Axis", options=sorted(axis_map.keys()), value="A", callback = callback)
callback.args["val1"] = x_axis

y_axis = Select(title="Y Axis", options=sorted(axis_map.keys()), value="B", callback = callback)
callback.args["val2"] = y_axis

#plot the figures
plot = figure(plot_width=400, plot_height=400)
plot.circle(x= "x",y="y", source=source, line_width=3, line_alpha=0.6)


#update the plot
def update():
    x_name = axis_map[x_axis.value]
    y_name = axis_map[y_axis.value]

    plot.xaxis.axis_label = x_axis.value
    plot.yaxis.axis_label = y_axis.value
    print x_name
    print val_a[x_name]
    source.data = dict(
        x=val_a[x_name],
        y=val_a[y_name],

    )

controls = [ x_axis, y_axis]
for control in controls:
    control.on_change('value', lambda attr, old, new: update())


update()
push_notebook()

#Display the graph in a jupyter notebook
layout = row(plot, x_axis, y_axis)
show(layout, notebook_handle=True)

Solution

  • I think to simplify your code, you can just stick with either a JS callback or a python callback, no need for both.

    To change the data source you need to feed in the original data to the JS callback, and then select the appropriate values corresponding to the selected value in the widgets.

    You can also set the axis labels in the identical way in JS. Not sure if this is exactly your desired implementation, but should take you alot closer.

    import numpy as np
    import pandas as pd
    from bokeh.layouts import row, widgetbox
    from bokeh.models import CustomJS, Slider, Select
    from bokeh.plotting import figure, output_file, show, ColumnDataSource
    from bokeh.io import push_notebook, output_notebook, curdoc
    from bokeh.client import push_session
    output_notebook()
    
    #create sample pandaframe to work with, this will store the actual data
    a = np.arange(50).reshape((5,10))
    labels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
    val_a = pd.DataFrame(a, columns=labels )
    
    # Here is a dict of some keys that I want to be able to pick from for plotting
    axis_map = {
        "A": "A",
        "B": "B",
        "C": "C"
    }
    
    #This is to update during the callback
    code = ''' var data = source.data;
               var value1 = val1.value;
               var value2 = val2.value;
               var original_data = original_source.data
               // get data corresponding to selection
               x = original_data[value1];
               y = original_data[value2];
               data['x'] = x;
               data['y'] = y;
               source.trigger('change');
               // set axis labels
               x_axis.axis_label = value1
               y_axis.axis_label = value2
                '''
    source = ColumnDataSource(data=dict(x=val_a['A'], y=val_a['B']))
    original_source = ColumnDataSource(data=val_a.to_dict(orient='list'))
    
    
    #plot the figures
    plot = figure(plot_width=400, plot_height=400)
    plot.circle(x= "x",y="y", source=source, line_width=3, line_alpha=0.6)
    
    
    callback = CustomJS(args=dict(source=source, original_source = original_source, x_axis=plot.xaxis[0],y_axis=plot.yaxis[0]), code=code)
    
    #Create two select widgets to pick the features of interest 
    x_axis = Select(title="X Axis", options=sorted(axis_map.keys()), value="A", callback = callback)
    callback.args["val1"] = x_axis
    
    y_axis = Select(title="Y Axis", options=sorted(axis_map.keys()), value="B", callback = callback)
    callback.args["val2"] = y_axis
    
    plot.xaxis[0].axis_label = 'A'
    plot.yaxis[0].axis_label = 'B'
    
    #Display the graph in a jupyter notebook
    layout = row(plot, x_axis, y_axis)
    show(layout, notebook_handle=True)