CICarlier
CICarlier

Reputation: 23

Python callbacks with bokeh server - Select widget returning blank plots

I recently learned how to use bokeh and I am having trouble making my python callbacks work with the bokeh server.

Here is my code (I’m building a candlestick chart with weather data):

from bokeh.client import push_session
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, DatetimeTickFormatter, CDSView, BooleanFilter,HoverTool
from bokeh.models import Select
from bokeh.layouts import column, widgetbox
from make_datasets import hourly_all_winters


# Convert dataset to a column data source
source = ColumnDataSource(data={
    'date': hourly_all_winters['date'],
    'max': hourly_all_winters['max'],
    'min': hourly_all_winters['min'],
    'first': hourly_all_winters['first'],
    'last': hourly_all_winters['last'],
    'average': hourly_all_winters['mean'],
    'winter': hourly_all_winters['winter'],
})

# Create first plot and select only the box_zoom and reset tools
y_range = (-30, 30)
p = figure(y_range=y_range, x_axis_type="datetime", plot_width=950, plot_height=200,
            title=f"Daily temperature variations - winter {source.data['winter'][0]}",
            x_axis_label='Months', y_axis_label='Temperature in °C',
            tools="box_zoom,reset")
p.xaxis.formatter = DatetimeTickFormatter(months=['%B'])

# Create the range line glyph
p.segment('date', 'max', 'date', 'min', source=source, color="black")

# Create the ascending bars glyph - we need to create a view of our data with a boolean mask to only plot the data
# we want
booleans_inc = [True if last > first else False for last, first in zip(source.data['last'], source.data['first'])]
view_inc = CDSView(source=source, filters=[BooleanFilter(booleans_inc)])

booleans_dec = [True if last < first else False for last, first in zip(source.data['last'], source.data['first'])]
view_dec = CDSView(source=source, filters=[BooleanFilter(booleans_dec)])

w = 365 * 60 * 2000

p.vbar('date', w, 'first', 'last', source=source, view=view_inc, fill_color="#2c8cd1", line_color="#2c8cd1")
p.vbar('date', w, 'first', 'last', source=source, view=view_dec, fill_color="#F2583E", line_color="#F2583E")

# Create a hover tool so that we can see min, max, first and last values for each record
# over the plot
hover = HoverTool(tooltips=[("First", "@first{first.1f}"),
                            ("Last", "@last{last.1f}"),
                            ("Min", "@min{min.1f}"),
                            ("Max", "@max{max.1f}"), ], mode='vline')

p.add_tools(hover)

# Make a slider object
slider = Select(options=['', '2013-2014', '2014-2015', '2015-2016', '2016-2017', '2017-2018', '2018-2019'],
                value='', title='Winter')

def update_plot(attr, old, new):
    if new == '':
        source.data = ColumnDataSource(data={'date': hourly_all_winters['date'],
                                             'max': hourly_all_winters['max'],
                                             'min': hourly_all_winters['min'],
                                             'first': hourly_all_winters['first'],
                                             'last': hourly_all_winters['last'],
                                             'average': hourly_all_winters['mean'],
                                             'winter': hourly_all_winters['winter'],}).data
    else:
        new_source = ColumnDataSource(data={'date': hourly_all_winters[hourly_all_winters.winter == winter]['date'],
                      'max': hourly_all_winters[hourly_all_winters.winter == new]['max'],
                      'min': hourly_all_winters[hourly_all_winters.winter == new]['min'],
                      'first': hourly_all_winters[hourly_all_winters.winter == new]['first'],
                      'last': hourly_all_winters[hourly_all_winters.winter == new]['last'],
                      'average': hourly_all_winters[hourly_all_winters.winter == new]['mean'],
                      'winter': hourly_all_winters[hourly_all_winters.winter == new]['winter'],
                      })
        source.data = new_source.data

# Attach the callback to the 'value' property of slider
slider.on_change('value', update_plot)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(widgetbox(slider), p))

show(curdoc())

I tried both starting with my “2013-2014” data and the full dataset. Every time I run bokeh serve myapp.py I get the base visualization working fine, but selecting other periods via the dropdown only returns a blank plot. Any thoughts on what I may have missed? I also tried using push_session() to see if it’d help with no luck (and I understand it is soon to be deprecated anyway).

EDIT - this is a sample of the hourly_all_winters data:

   Year  Month  Day  first  last   min   max       mean       date     winter
0  2013     10    1   17.3  14.5  14.5  23.1  18.529167 2013-10-01  2013-2014
1  2013     10    2   14.8  14.5  13.9  24.0  18.545833 2013-10-02  2013-2014
2  2013     10    3   13.9  14.3  10.0  21.6  15.516667 2013-10-03  2013-2014
3  2013     10    4   14.7  14.3  13.6  16.1  14.900000 2013-10-04  2013-2014
4  2013     10    5   14.0  13.4  12.8  18.2  14.804167 2013-10-05  2013-2014

Upvotes: 0

Views: 988

Answers (1)

CICarlier
CICarlier

Reputation: 23

I got a nice answer from the bokeh community - so posting here the solution:

show(curdoc()) is not the right way to do it, so deleting that last line, then running bokeh serve --show myapp.py in a terminal should do the trick. source.data = new_source.data was my second mistake, it’s not possible to “re-home” a .data from one CDS to another. Starting in Bokeh 2.0 this will generate an immediate exception. (Currently leads to silent failure). The value for .data always needs to be a plain Python dict.

Updated code below worked:

from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, DatetimeTickFormatter, CDSView, BooleanFilter,HoverTool
from bokeh.models import Select
from bokeh.layouts import column, widgetbox

from make_datasets import hourly_all_winters


# Convert dataset to a column data source
source = ColumnDataSource(data={
    'date': hourly_all_winters['date'],
    'max': hourly_all_winters['max'],
    'min': hourly_all_winters['min'],
    'first': hourly_all_winters['first'],
    'last': hourly_all_winters['last'],
    'average': hourly_all_winters['mean'],
    'winter': hourly_all_winters['winter'],
})

# Create first plot and select only the box_zoom and reset tools
y_range = (-30, 30)
p = figure(y_range=y_range, x_axis_type="datetime", plot_width=950, plot_height=400,
            title=f"Daily temperature variations - winter {source.data['winter'][0]}",
            x_axis_label='Months', y_axis_label='Temperature in °C',
            tools="box_zoom,reset")
p.xaxis.formatter = DatetimeTickFormatter(months=['%B'])

# Create the range line glyph
p.segment('date', 'max', 'date', 'min', source=source, color="black")

# Create the ascending bars glyph - we need to create a view of our data with a boolean mask to only plot the data
# we want
booleans_inc = [True if last > first else False for last, first in zip(source.data['last'], source.data['first'])]
view_inc = CDSView(source=source, filters=[BooleanFilter(booleans_inc)])

booleans_dec = [True if last < first else False for last, first in zip(source.data['last'], source.data['first'])]
view_dec = CDSView(source=source, filters=[BooleanFilter(booleans_dec)])

w = 365 * 60 * 2000

p.vbar('date', w, 'first', 'last', source=source, view=view_inc, fill_color="#2c8cd1", line_color="#2c8cd1")
p.vbar('date', w, 'first', 'last', source=source, view=view_dec, fill_color="#F2583E", line_color="#F2583E")

# Create a hover tool so that we can see min, max, first and last values for each record
# over the plot
hover = HoverTool(tooltips=[("First", "@first{first.1f}"),
                            ("Last", "@last{last.1f}"),
                            ("Min", "@min{min.1f}"),
                            ("Max", "@max{max.1f}"), ], mode='vline')

p.add_tools(hover)

# Make a slider object
slider = Select(options=['', '2013-2014', '2014-2015', '2015-2016', '2016-2017', '2017-2018', '2018-2019'],
                value='', title='Winter')

def update_plot(attr, old, new):
    if new == '':
        source.data = ColumnDataSource(data={'date': hourly_all_winters['date'],
                                             'max': hourly_all_winters['max'],
                                             'min': hourly_all_winters['min'],
                                             'first': hourly_all_winters['first'],
                                             'last': hourly_all_winters['last'],
                                             'average': hourly_all_winters['mean'],
                                             'winter': hourly_all_winters['winter'],}).data
    else:
        new_source = {'date': hourly_all_winters[hourly_all_winters.winter == new]['date'],
                      'max': hourly_all_winters[hourly_all_winters.winter == new]['max'],
                      'min': hourly_all_winters[hourly_all_winters.winter == new]['min'],
                      'first': hourly_all_winters[hourly_all_winters.winter == new]['first'],
                      'last': hourly_all_winters[hourly_all_winters.winter == new]['last'],
                      'average': hourly_all_winters[hourly_all_winters.winter == new]['mean'],
                      'winter': hourly_all_winters[hourly_all_winters.winter == new]['winter'],
                      }
        source.data = new_source

# Attach the callback to the 'value' property of slider
slider.on_change('value', update_plot)
print(slider.value)

# put the button and plot in a layout and add to the document
curdoc().add_root(column(widgetbox(slider), p))

Upvotes: 1

Related Questions