Connecting multiple widgets to the same plot

Hello everyone, I’m currently working on a project that seeks to analyze world population statistics and present the findings in an interactive data visualization dashboard. For context below is a picture of the data frame that I’m working with and a link to a plot that is similar to what I want to achieve.

So far I have gotten the multi-select widget to work fine as well as the hover tools, however at the moment, I’m having some trouble figuring out the best method on how to connect both the multi-choice widget and the range slider widget to the same plot i.e whether it would be a better idea to write separate callback functions or just put both into one.

Especially given the fact that the value from the range slider will directly affect the values used for the multi-select widget as the year range will decide what values to extract from the main data frame to display on the plot.

So far when I select a particular country it will just display the entire set of data it has for that country (i.e the country’s birth rate (y) against the country’s death rate (x) 1950 - 2021) however I would like the range slider to give the user that option to choose the range of data observed.

# Attempted call back at range slider
callbackrs = CustomJS(args={'data_source': data_source}, code="""
    var data = data_source.data;
    var range = cb_obj.value;
    data[df[(df['Year'] >= range[0]) & (df['Year'] <= range[1])];
    source.change.emit();
""")
# Define parameters for range slider
range_slider = RangeSlider(title='Year', start=1950, end=2021, step=1, value=(1950, 2021))
range_slider.js_on_change('value', callbackrs)
# JScallback for multi choice widget
callbackmc = CustomJS(args={'source': source, 'data_source': data_source}, code="""
    var data = data_source.data;
    var s_data = source.data;
    var selected = cb_obj.value;

    var Country = data['Country'];
    var death_rate_data = data['Death rate/1000'];
    var birth_rate_data = data['Birth rate/1000'];
    var net_data = data['Net population increase/1000'];

    var name = s_data['name']
    name.length = 0;
    var deathrate = s_data['deathrate'];
    deathrate.length = 0;
    var birthrate = s_data['birthrate'];
    birthrate.length = 0;
    var net = s_data['net'];
    net.length = 0;
    
    for (var i = 0; i < death_rate_data.length; i++) {
        if (selected.indexOf(Country[i]) >= 0) {
            name.push(Country[i]);
            deathrate.push(death_rate_data[i]);
            birthrate.push(birth_rate_data[i]);
            net.push(net_data[i]);
        }
    }
    source.change.emit();
""")


multi_choice = MultiChoice(title='Select Countries', value=[], options=data['Country'].unique().tolist())

multi_choice.js_on_change('value', callbackmc)

layout = Column(multi_choice, range_slider, p)
show(layout)

p.s I’m not the most familiar with JS callbacks and how to write them, however, I do know a decent amount of Javascript, so please bear with me.

Any assistance on this would be greatly appreciated

whether it would be a better idea to write separate callback functions or just put both into one.

I think it’s a matter of preference in this case, and either approach is workable.

However, there are various issues with the JS code, e.g.

data[df[(df['Year'] >= range[0]) & (df['Year'] <= range[1])];

is not feasible at all. Pandas and data frames are Python things, they do not exist in JavaScript. You will have to do your own filtering of the data using explicit loops. Also FYI

source.change.emit();

This is generally not necessary. The best way to approach this is construct a brand new data object and then assign is as a whole:

source.data = new_data_obj

Bokeh will automatically notice this and redraw. Most of these examples in the docs demonstrate this approach:

JavaScript callbacks — Bokeh 3.3.2 Documentation

Note that in the callbackmc you are also probably going to want to copy the columns before you modify them. Otherwise, if once you overwrite the original data, it is gone and subsequent callbacks will overwrite the already-overwritten date etc. which is usually not what is desired.

Hi Bryan thank you for getting back to me, just clarifying I see roughly what you’re trying to say by creating a new data object, but im not quite getting how source.data = new_data_obj will replace source.change.emit()

I do have a new idea, but im not sure how feasible it is,
But in essence, my range_slider widget should just be changing the actual data available to the multi-choice widget right? Since right now as im passing through the entire data frame in it will plot from 1950-2021 (all data available) so If I’m able to dynamically modify this data, it should allow me to plot within the range of data given by the range slider?

So I’m thinking that I create a new ColumnDataSource like this

df1 = df.drop(df.index)
df1

# Below is code for the actual plot
data = df
data1 = df1 # new
data_source = ColumnDataSource(data) 
source = ColumnDataSource(dict(name=[], deathrate=[], birthrate=[], net=[]))
data_source_1 = ColumnDataSource((df1 := df.drop(df.index))) # new 


# Defining the hover tool and figure 
hover = HoverTool(tooltips = [('Country/region','@name'),('Death rate(in thousands)', '@deathrate'), ('Birth rate(in thousands)', '@birthrate'), ('Net population increase(in thousands)', '@net')])
p = figure(title='Birth rate vs Death rate of countries (in thousands)', width=700, height=400, tools=[hover, 'wheel_zoom', 'pan'])
p.xaxis.axis_label = 'Death rate/1000'
p.yaxis.axis_label = 'Birth rate/1000'

Essentially here df1 will be the original data frame with no rows apart from the column names (so no data)

# Attempted call back at range slider
callbackrs = CustomJS(args={'data_source': data_source, 'data_source_1': data_source_1}, code="""
    new_data_obj = data_source.data
    var year_range = cb_obj.value
    var data = data_source.data
    var data1 = data_source_1.data
    
    for (int i = year_range[0]; i <= year_range[1]; i++) {
    /* 
    logic here is to push all the rows of data into the empty data_frame 
    if the year is between or equal to the 2 ranges given by the range slider
    not sure whether the syntax is correct for JS tho :/ 
    */ 
    data1.push(data[data['Year'] == i] 
    }
    
    source.change.emit();
    
""")
# Define parameters for range slider
range_slider = RangeSlider(title='Year', start=1950, end=2021, step=1, value=(1950, 2021))
range_slider.js_on_change('value', callbackrs)

The idea is now that given there should be values in df1 only between the year range selected by the slider and now I should be able to use this data source instead of the original one with all values for my multi-select widget which will do the actual plotting so that only the data between the selected year ranges will be made available to the callback

p.line('deathrate', 'birthrate', line_width=2, source=source)
# Line indicating where birth rate is equal to the death rate
p.line(x := [i for i  in range(0, 50)], x, alpha=0.2)

# JScallback for multi choice widget
# Passed in data_source_1 as source of plotting data instead of data_source 
callbackmc = CustomJS(args={'source': source, 'data_source_1': data_source_1}, code="""
    var data = data_source_1.data;
    var s_data = source.data;
    var selected = cb_obj.value;

    var Country = data['Country'];
    var death_rate_data = data['Death rate/1000'];
    var birth_rate_data = data['Birth rate/1000'];
    var net_data = data['Net population increase/1000'];

    var name = s_data['name']
    name.length = 0;
    var deathrate = s_data['deathrate'];
    deathrate.length = 0;
    var birthrate = s_data['birthrate'];
    birthrate.length = 0;
    var net = s_data['net'];
    net.length = 0;
    
    for (var i = 0; i < death_rate_data.length; i++) {
        if (selected.indexOf(Country[i]) >= 0) {
            name.push(Country[i]);
            deathrate.push(death_rate_data[i]);
            birthrate.push(birth_rate_data[i]);
            net.push(net_data[i]);
        }
    }
    source.change.emit();
""")


multi_choice = MultiChoice(title='Select Countries', value=[], options=data['Country'].unique().tolist())

multi_choice.js_on_change('value', callbackmc)

layout = Column(multi_choice, range_slider, p)
show(layout)

again not sure how viable this is, I don’t have a great understanding of how all this is supposed to function so any further explanation will be greatly appreciated.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.