Updating graph based on the selection - dropdown x 2

Hi, could anyone have a look at my code and give some hints what’s wrong?
Everything is visible but with no interactions. I’d like to use numpy so the code takes lists as inputs only.

import numpy as np
from bokeh.io import curdoc, show, output_notebook
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, Select, Div, Title, WidgetBox
from bokeh.plotting import figure

feature_list = ['name', 'age', 'sex', 'salary']
var = [[1,2,3,4,5,6,7,8],
       [3,5,2,1,4,4,5,3],
       [4,3,6,4,3,3,5,2],
       [3,3,2,1,6,1,1,3]]

def map_feature(name, value):
    return {i: y for i, y in zip(name, value)}

data = map_feature(feature_list, var)
source = ColumnDataSource(data=data)

def viz_plot():

    x = list(data.keys())[0]
    y = list(data.keys())[-1]

    vp = figure(plot_width = 300, plot_height = 300)
    vp.circle(x = x, y = y, source = source)
    vp.xaxis.axis_label = x
    vp.yaxis.axis_label = y
    return vp

select_x_axis = Select(title = 'X-axis',
                       value = feature_list[0],
                       options = feature_list)

select_y_axis = Select(title = 'Y-Axis',
                       value = feature_list[-1],
                       options = feature_list)

def update_data(attr, old, new):
    x_value = select_x_axis.value
    y_value = select_y_axis.value
    x = {k : v for k, v in data.items() if k == x_value}
    y = {k : v for k, v in data.items() if k == y_value}
    source.data = {**x, **y}

for selector in [select_x_axis, select_y_axis]:
    selector.on_change('value', update_data)

menu = WidgetBox(select_x_axis, select_y_axis, width=200)
layout = row(menu, viz_plot(), width=800)

#show(layout)
curdoc().add_root(layout)
curdoc().title = "Menu"

You can try adding to your update function one more line on the end.

layout.children[1] = viz_plot()

Thank you , I did it in the meantime. The plot is visible only if either “name” or “salary” is selected. In case of other combinations, no plot at all. Suppose here is the problem:

    x = list(data.keys())[0]
    y = list(data.keys())[-1]

You have configured your circle glyph to render based on the fields "name" and "salary", but then later you change the contents of the source.data dict to only have different columns. If you actually look at the browser JS console, you will see the error. For example if I select "age" for the X-axis, there is this error:

That is because the glyph is still configured to look for "name" and "salary" (you have not changed it to do otherwise) but your new source.data no longer has "name" in it:

{'age': [3, 5, 2, 1, 4, 4, 5, 3], 'salary': [3, 3, 2, 1, 6, 1, 1, 3]}

You can either:

  • simultaneously update the glyph field to point at the new column name, or
  • configure the glyph with “generic” column names, e.g. x="x" then always update the same "x" and "y" columns in the data source

Most of the examples take the second approach and it is typically what I personally prefer.

Thanks Bryan,

I thought it’s straightforward but after several hours of investigating this case my brain is exploding. Are you able to give us any example to makes our lives simpler?
I have found crossfilter example but seems it doesn’t use ColumnDataSource.

Hi,

I generally create a plot property class which I can then access to assign stuff like axis names, source data, etc. You can call and update this class with callbacks and then reference it with your plotting function. Not sure if it’s the prefered way, but it does work.

import numpy as np
from bokeh.io import curdoc, show, output_notebook
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, Select, Div, Title, WidgetBox
from bokeh.plotting import figure

class plot:
    def __init__(self, feature_list, var):
        self.x = feature_list[0]    
        self.y = feature_list[1]
        self.data = map_feature(feature_list, var)
        self.source = self.cds(self.x, self.y)
        
    def cds(self, x, y):
        return ColumnDataSource(dict({'x': self.data[x], 'y': self.data[y]}))

feature_list = ['name', 'age', 'sex', 'salary']
var = [[1,2,3,4,5,6,7,8],
       [3,5,2,1,4,4,5,3],
       [4,3,6,4,3,3,5,2],
       [3,3,2,1,6,1,1,3]]

def map_feature(name, value):
    return {i: y for i, y in zip(name, value)}
data = map_feature(feature_list, var)

plot = plot(feature_list, var)

def viz_plot():
    vp = figure(plot_width = 300, plot_height = 300)
    vp.circle(x = 'x', y = 'y', source = plot.source)
    vp.xaxis.axis_label = plot.x
    vp.yaxis.axis_label = plot.y
    return vp

select_x_axis = Select(title = 'X-axis',
                       value = feature_list[0],
                       options = feature_list)

select_y_axis = Select(title = 'Y-Axis',
                       value = feature_list[-1],
                       options = feature_list)

def update_data(attr, old, new):
    plot.x = select_x_axis.value
    plot.y = select_y_axis.value
    plot.source = plot.cds(plot.x, plot.y)
    layout.children[1] = viz_plot()

for selector in [select_x_axis, select_y_axis]:
    selector.on_change('value', update_data)

menu = WidgetBox(select_x_axis, select_y_axis, width=200)
layout = row(menu, viz_plot(), width=800)

#show(layout)
curdoc().add_root(layout)
curdoc().title = "Menu"

@jnava in general avoid creating entirely new CDS. They are very heavyweight objects with many , many connections, events and signals attached. It’s always prefabable to update an existing CDS, e.g. by assigning to its .data attribute.

@grzegorz.malinowski You’ve stumbled across the literal only example that does things a bit different. Almost every example in the repo is similar to the sliders.py example:

plot = figure(...)

# configure the glyph to always look at "x" and "y" columns
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

def update_data(attrname, old, new):

    ...

    # update source.data with new x and y column values
    source.data = dict(x=new_x, y=new_y)

In your case, specifically, it might (untested) look like:

# keep all data in CDS, but also initialize "x" and "y" columns
data = map_feature(feature_list, var)
data['x'] = data['name']
data['y'] = data['salary']
source = ColumnDataSource(data=data)

def update_data(attr, old, new):
    x_value = select_x_axis.value
    y_value = select_y_axis.value
 
   # update "x" and "y" from the columns given by the selects
    source.data.update({
        'x': source.data[x_value]
        'y': source.data[y_value]
    })
1 Like

Thank you guys, you both are awesome.

1 Like