Search code examples
javascriptpythoncallbackbokeh

Python/Bokeh - how the change data source by filtering rows by column value from dict with Select, callback and CustomJS/js_on_change


The problem should lie in the callback function. Unfortunately, I have no experience in JS. I took this part from the dataframe-js library but it is not working. The idea is to have a dashboard with two graphs for Rate1 and Rate2 and a dropdown menu for the categories for both Rates.

import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS, Select
from bokeh.plotting import figure, output_file
from bokeh.layouts import gridplot
from bokeh.io import show

d = {'Category': ['Cat1', 'Cat2', 'Cat3', 'Cat1', 'Cat2', 'Cat3', 'Cat1', 'Cat2', 'Cat3'], 
'Rate1': [1, 4, 3, 3, 7, 4, 9, 2, 6], 'Rate2': [3, 4, 6, 1, 9, 6, 8, 2, 1], 
'Date': ['2021-06-21', '2021-06-21', '2021-06-21', '2021-06-22', '2021-06-22', '2021-06-22', '2021-06-23', '2021-06-23', '2021-06-23']}

df = pd.DataFrame(data=d)

output_file("with_dropdown_list.html")

category_default = "Cat1"
unique_categories = list(df.Category.unique())

source = ColumnDataSource(data={'y1': df.loc[df['Category'] == category_default].Rate1,
                                'y2': df.loc[df['Category'] == category_default].Rate2,
                                'date': df.loc[df['Category'] == category_default].Date})

output_file("time_series.html")

s1 = figure(title=category_default, x_axis_type="datetime", plot_width=500, plot_height=500)
s1.line(y='y1', x='date', source=source)

s2 = figure(title=category_default, x_axis_type="datetime", plot_width=500, plot_height=500)
s2.line(y='y2', x='date', source=source)

callback1 = CustomJS(args = {'source': source, 'data': data},
                    code = """source.data['y1'] = data['y1'].filter(row => row.get('Category') == cb_obj.value);""")

callback2 = CustomJS(args = {'source': source, 'data': data},
                    code = """source.data['y2'] = data['y2'].filter(row => row.get('Category') == cb_obj.value);""")

select1 = Select(title='Category Selection', value=category_default, options=unique_categories)
select1.js_on_change('value', callback1)

select2 = Select(title='Category Selection', value=category_default, options=unique_categories)
select2.js_on_change('value', callback2)

p = gridplot([[s1, s2], [select1, select2]])

show(p)

Solution

  • Possibly the quickest way to do this will be to create a dictionary that maps the values from each category to the appropriate Rate (either rate1 or rate2 depending on the plot). You can do this by creating a wide dataset where each row represents a unique date:

    df = pd.DataFrame(data=d).astype({"Date": "datetime64[ns]"}).pivot("Date", "Category", ["Rate1", "Rate2"])
    
    print(df)
               Rate1           Rate2          
    Category    Cat1 Cat2 Cat3  Cat1 Cat2 Cat3
    Date                                      
    2021-06-21     1    4    3     3    4    6
    2021-06-22     3    7    4     1    9    6
    2021-06-23     9    2    6     8    2    1
    

    Now that the data is set up, we can easily use a for-loop to create each plot instead of manually specifying both (full code):

    import pandas as pd
    
    from bokeh.models import ColumnDataSource, CustomJS, Select
    from bokeh.plotting import figure
    from bokeh.layouts import gridplot
    from bokeh.io import show
    
    d = {
        'Category': ['Cat1', 'Cat2', 'Cat3', 'Cat1', 'Cat2', 'Cat3', 'Cat1', 'Cat2', 'Cat3'], 
        'Rate1': [1, 4, 3, 3, 7, 4, 9, 2, 6], 'Rate2': [3, 4, 6, 1, 9, 6, 8, 2, 1], 
        'Date': ['2021-06-21', '2021-06-21', '2021-06-21', '2021-06-22', '2021-06-22', '2021-06-22', '2021-06-23', '2021-06-23', '2021-06-23']
    }
    
    df = pd.DataFrame(data=d).astype({"Date": "datetime64[ns]"}).pivot("Date", "Category", ["Rate1", "Rate2"])
    
    category_default = "Cat1"
    unique_categories = list(df.columns.levels[1])
    
    plot_figures = []
    selectors = []
    
    for y_name in ["Rate1", "Rate2"]:
        subset = df[y_name]  # select the appropriate "Rate" subset
        subset_data = subset.to_dict("list")  # dictionary-format: {'Cat1': [values...], 'Cat2': [values...], 'Cat3': [values...]}
    
        source = ColumnDataSource({
            "date": subset.index,   # subset.index are the dates
            "rate": subset_data[category_default]
        })
    
        p = figure(title=y_name, x_axis_type="datetime", plot_width=500, plot_height=500)
        p.line(y="rate", x='date', source=source)
    
        select = Select(title='Category Selection', value=category_default, options=unique_categories)
    
        callback = CustomJS(
            args={"subset_data": subset_data, "source": source},
            code="""
                source.data['rate'] = subset_data[cb_obj.value];
                source.change.emit();
            """)
        select.js_on_change("value", callback)
    
        plot_figures.append(p)
        selectors.append(select)
    
    
    p = gridplot([plot_figures, selectors])
    
    show(p)
    

    Default Rendered Plots

    Default Rendered Plots

    Updating Category to Cat2 & Cat3

    Updating Category to Cat2 & Cat3