Problems with updating a graph through a slider

What are you trying to do?
I want to plot a dataset and filter it using a slider based on the date retrieved from a dataframe.

What have you tried that did NOT work as expected?
The graph is only updated correctly the first time. On the second, third, or subsequent updates, it fails to update, and it shows everything as blank

I’m using Bokeh 2.4.3.

df = pd.DataFrame({
    'x': [17, 26, 22, 23, 18],'y': [39, 34, 16, 45, 4],
    'Date': pd.to_datetime(['04-12-2022', '02-12-2022', '05-12-2022', '05-12-2022', '06-12-2022'], format='%d-%m-%Y'),
    'Title': ['Title 1', 'Title 2', 'Title 3', 'Title 4', 'Title 5'],
    'topic_prediction': [3, 4, 0, 4, 2]
})


source = ColumnDataSource(data=dict(
    x=df['x'], y=df['y'],
    desc=df['topic_prediction'],
    titles=df['Title'],
    date=df['Date']
))

hover = HoverTool(tooltips=[
    ("Title", "@titles"),
    ("Date", "@date{%d-%m-%Y}")
], formatters={'@date': 'datetime'})

plot = figure(plot_width=300, plot_height=300, tools=[hover])
scatter = plot.scatter('x', 'y', size=5, source=source)

def date_sidebar(source=source, scatter=scatter):
    callback = CustomJS(args=dict(source=source, scatter=scatter), code="""
        const selected_date = new Date(cb_obj.value);
        const data = source.data;
        const x = data['x'];
        const y = data['y'];
        const desc = data['desc'];
        const titles = data['titles'];
        const dates = data['date'];

        const filtered_x = [];
        const filtered_y = [];
        const filtered_desc = [];
        const filtered_titles = [];
        const filtered_dates = [];

        for (let i = 0; i < dates.length; i++) {
            const date = new Date(dates[i]);
            if (date.toDateString() === selected_date.toDateString()) {
                filtered_x.push(x[i]);
                filtered_y.push(y[i]);
                filtered_desc.push(desc[i]);
                filtered_titles.push(titles[i]);
                filtered_dates.push(dates[i]);
            }
        }

        scatter.data_source.data = {
            x: filtered_x,
            y: filtered_y,
            desc: filtered_desc,
            titles: filtered_titles,
            date: filtered_dates
        };

        scatter.data_source.change.emit();
    """)
    return callback

slider_callback = date_sidebar()

date_slider = DateSlider(title='Select Date', start=df['Date'].min(), end=df['Date'].max(), value=df['Date'].min())

date_slider.js_on_change('value', slider_callback)

layout = column(date_slider, plot)

show(layout)

The problem is that each time you move the slider you update the contents of the ColumnDataSource to contain only the selected dates, so the data source can only ever get smaller. If you were using the original data to select the dates you want then it would be fine. So probably the easiest change in your example would be to have two ColumnDataSources, one for the original data and one for the displayed data.

However, I wouldn’t solve it this way. I would use a CDSView (Data sources — Bokeh 3.1.1 Documentation) to do the hard work for you. Here is my solution (works using Bokeh 3.1.1, will need modifying to work with 2.4.3):

from bokeh.models import CustomJS, ColumnDataSource, HoverTool, DateSlider, CDSView, BooleanFilter
from bokeh.plotting import figure, show, column
import pandas as pd


df = pd.DataFrame({
    'x': [17, 26, 22, 23, 18],'y': [39, 34, 16, 45, 4],
    'Date': pd.to_datetime(['04-12-2022', '02-12-2022', '05-12-2022', '05-12-2022', '06-12-2022'], format='%d-%m-%Y'),
    'Title': ['Title 1', 'Title 2', 'Title 3', 'Title 4', 'Title 5'],
    'topic_prediction': [3, 4, 0, 4, 2]
})

source = ColumnDataSource(data=dict(
    x=df['x'], y=df['y'],
    desc=df['topic_prediction'],
    titles=df['Title'],
    date=df['Date']
))

filter = BooleanFilter([True]*len(df))  # Display everything initially, not ideal.
view = CDSView(filter=filter)

hover = HoverTool(tooltips=[
    ("Title", "@titles"),
    ("Date", "@date{%d-%m-%Y}")
], formatters={'@date': 'datetime'})

plot = figure(
    width=300, height=300,
    tools=[hover])
scatter = plot.scatter('x', 'y', size=5, source=source, view=view)

print(df['Date'].min(), df['Date'].max())
date_slider = DateSlider(title='Select Date', start=df['Date'].min(), end=df['Date'].max(), value=df['Date'].min())
slider_callback = CustomJS(args=dict(source=source, filter=filter, date_slider=date_slider),
    code="""
    for (let i = 0; i < filter.booleans.length; i++)
      filter.booleans[i] = source.data.date[i] == date_slider.value
    source.change.emit();
    """)
date_slider.js_on_change('value', slider_callback)

layout = column(date_slider, plot)
show(layout)

Note that for 3 December there is no data, so the plot will be empty. Personally I would set the plot limits at the beginning rather than having them update to the data displayed.

2 Likes

I didn’t realize that the content of the ColumnDataSource was being updated and losing data. The approach of having two ColumnDataSources helped me solve the problem. Thank you very much! I’m using Bokeh 2.4.3 because I have encountered compatibility issues with other libraries, and I decided to stick with this version. Your approach has been very helpful to me.

1 Like

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