Python 面板/参数应用程序,每 3 次重新加载一次,散景图消失

Python Panel/Param application with disappearing Bokeh graphs every 3rd reload

提问人:Christian Sayers 提问时间:10/23/2023 最后编辑:Christian Sayers 更新时间:11/22/2023 访问量:46

问:

我正在构建一个数据分析 Web 应用程序,该应用程序根据用户输入加载一些数据,然后生成一系列视觉对象来显示用户的数据。我正在使用 Panel、Param 和 Bokeh 库在 Python 中构建应用程序 - Param 用于控制用户输入,Bokeh 用于生成交互式视觉效果,Panel 用于生成和组合应用程序。该应用程序按预期出现(下面的最小可重现示例),但是当我更改用户输入并重新生成数据时,散景图将缩小到没有大小。这仅在单击按钮重新生成数据三次后发生 - 在此点之后再次单击将解决数字问题,并且 3 秒的循环将重新开始。

下面的例子说明了我遇到的问题:

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()

我尝试了几种不同的方法,例如将数据和图形生成移动到代码的不同部分,但都没有成功解决这个问题。当我在散景图消失时检查为类生成的图形对象时,我注意到“高度”和“宽度”将为 None,但我还没有弄清楚为什么会发生这种情况。即使图形无法正确显示,基础数据帧也存在。我的预感是问题出在bokeh_view功能上,但我无法弄清楚需要有什么不同。我哪里出了问题?

编辑:手动设置散景窗格的大小(例如高度=600,宽度=1200)可以解决此问题。但是,如果有人有任何见解,我很想了解为什么这是必要的。

python-3.x 散景

评论


答:

0赞 ChuckLewis 11/18/2023 #1

这似乎是浏览器与服务器通信的问题,如此处所述。我还再次查看了面板散景引用,发现它们将 bokeh.layouts 中的行提供给 pn.pane.Bokeh()。这对我有用,可能是处理问题的最一致方法。