I am building a data analysis web app that loads in some data based on user inputs, then generates a series of visuals to display the data for the user. I am building the app in Python using the Panel, Param, and Bokeh libraries - Param to control the user inputs, Bokeh to generate interactive visuals, and Panel to generate and pull together the application. The app comes up as expected (minimal reproducible example below), however when I change the user input and re-generate my data, the Bokeh graphs will shrink to having no size. This only happens after clicking the button to re-generate data three times - clicking again after this point will fix the issue with the figures and the cycle of 3s will start over again.
The below example illustrates the issue I have been running into:
import random
import param
import panel as pn
import numpy as np
import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
pn.extension()
def generate_fake_data(n: int):
"""
Sample data generation function
"""
start_x_value = np.random.randint(low=0, high=int(1e3))
step_x_size = np.random.randint(low=1, high=10)
end_x_value = start_x_value + step_x_size * n
x_values = list(range(start_x_value, end_x_value, step_x_size))
y_values = random.sample(range(0, end_x_value), n)
d = {'x': x_values, 'y': y_values}
return pd.DataFrame(d, index=range(0, n))
class DataFigureGeneration(param.Parameterized):
"""
Sample class used to house dataframes and figure generation functions
"""
# example of a non-empty starting dataframe
data_df_1 = param.DataFrame(
pd.DataFrame(
{'x': [1, 2, 3, 4, 5], 'y': [8, 4, 8, 7, 1]}
),
label='DF1'
)
# example of an empty starting dataframe
data_df_2 = param.DataFrame(
pd.DataFrame([np.nan]),
label='DF2'
)
def __init__(self):
super().__init__()
def generate_fig_1(self):
p = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right',
x_axis_type='datetime',
title='DF1'
)
data_source = ColumnDataSource(self.data_df_1)
line = p.line(x='x', y='y', color='orange', name='DF1', source=data_source)
return p
def generate_fig_2(self):
p = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right',
x_axis_type='datetime',
title='DF2'
)
data_source = ColumnDataSource(self.data_df_2)
line = p.line(x='x', y='y', color='blue', name='DF2', source=data_source)
return p
class MainApp(param.Parameterized):
"""
Sample class used to receive user inputs, respond to changes, and generate end-user app
"""
n = param.Integer(
default=5,
bounds=(1, 100)
)
view = param.ClassSelector(
class_=DataFigureGeneration,
default=DataFigureGeneration()
)
generate_data_button = param.Action(
lambda x: x.param.trigger('generate_data_button'),
label='Generate Data'
)
data_df_3 = param.DataFrame(
pd.DataFrame([np.nan]),
label='DF3'
)
reload_tracker = param.Integer(default=0)
data_generated_status = param.Boolean(False)
def __init__(self):
super().__init__()
# example of a non-empty starting figure, using the DataFigureGeneration object
self.df_1_fig = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right'
)
data_source = ColumnDataSource(self.view.data_df_1)
self.df_1_fig.line(x='x', y='y', color='orange', source=data_source)
# example of an empty starting figure
self.df_2_fig = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right'
)
# example of an dataframe/figure that does not use the DataFigureGeneration class
self.df_3_fig = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right'
)
@param.depends('generate_data_button', watch=True)
def generate_data(self):
self.view.data_df_1 = generate_fake_data(self.n)
self.view.data_df_2 = generate_fake_data(self.n)
self.data_df_3 = generate_fake_data(self.n)
if not self.data_generated_status:
self.data_generated_status = True
self.reload_tracker += 1
def generate_fig_3(self):
"""
identical generation function to the one in DataFigureGeneration, in the MainApp class instead
"""
p = figure(
height=600,
width=1200,
tools='box_zoom,wheel_zoom,reset,xpan',
toolbar_location='right',
x_axis_type='datetime',
title='DF3'
)
data_source = ColumnDataSource(self.data_df_3)
line = p.line(x='x', y='y', color='blue', name='DF3', source=data_source)
return p
def params_view(self):
n_widget = pn.Param(
self.param,parameters=['n'],
widgets={'n': pn.widgets.IntInput},
show_name=False
)
data_button = pn.Param(
self.param,
parameters=['generate_data_button'],
widgets={'generate_data_button': {
'type': pn.widgets.Button, 'button_style': 'solid', 'button_type': 'primary'}
},
show_name=False
)
view = pn.Column(
n_widget,
data_button,
)
return view
@param.depends('reload_tracker')
def bokeh_view(self):
if self.data_generated_status:
self.df_1_fig = self.view.generate_fig_1()
# self.df_2_fig = self.view.generate_fig_2()
self.df_3_fig = self.generate_fig_3()
view = pn.Column(
pn.pane.Bokeh(self.df_1_fig),
pn.pane.Bokeh(self.view.generate_fig_2()),
pn.pane.Bokeh(self.generate_fig_3())
)
else:
self.df_1_fig = self.view.generate_fig_1()
view = pn.Column(
pn.pane.Bokeh(self.df_1_fig),
pn.pane.Bokeh(self.df_2_fig),
pn.pane.Bokeh(self.df_3_fig)
)
return view
def panel_view(self):
view = pn.Row(
self.params_view,
self.bokeh_view
)
return view
app = MainApp()
app.panel_view().servable()
I have tried a few different approaches, such as moving the data and figure generation around to different sections of the code, but nothing has been successful in fixing the issue. When I inspect the generated figure objects for the class when the Bokeh graph has disappeared, I have noticed that the 'height' and 'width' will be None, but I have not figured out why that is occurring. The underlying dataframes exist even when the figure will not correctly show. My hunch is that the issue lies with the bokeh_view function but I am unable to figure out what needs to be different. Where am I going wrong here?
EDIT: Manually setting the size of the Bokeh pane (e.g. height=600, width=1200) solves this issue. I'd love to understand why this is necessary, however, if anyone has any insight.
This appears to be an issue with browser-server communication, as outlined here. I also took a second look at the panel bokeh reference and found that they supply row from bokeh.layouts into pn.pane.Bokeh(). This worked for me and is probably the most consistent way to handle the issue.