Using Panel Server and interactive widget, I cannot get the constant obtained from the widget to be saved once I close the widget

I have a piece of panel code that sort of works. My goal is to some how save the constant CBW_Int that I pick from the panel server. I cannot seem to pass this on automatically so that I could then use this constant after the interactive widget work. Is there a way to do this:

import panel as pn
import matplotlib.pyplot as plt
import numpy as np
from bokeh.models.widgets import Button

CBW_Int = 0.25 # Define CBW_Int as a global variable

CBW_Int_slider = pn.widgets.FloatSlider(name=‘CBW_Intercept’, start=0, end=0.5, step=0.01, value=CBW_Int)

def cbw_int_plot(plot=True):
global CBW_Int # Access the global CBW_Int variable

fig = plt.figure(figsize=(6, 6))
plt.title('Vsh vs.CBWa', color='blue')
plt.plot(np.arange(10), np.arange(10) * CBW_Int, "k-", label="")
plt.xlim(0.0, 1)
plt.ylim(0.0, 1.0)
plt.ylabel('CBWa [v/v]', color='blue')
plt.xlabel('Vsh [v/v]', color='blue')
plt.grid()

if plot:
    plt.show()
else:
    plt.close(fig)

return fig

def button_callback(event):
global CBW_Int

CBW_Int = CBW_Int_slider.value

# Save the new CBW_Int value to a file
with open("new_cbw_int.txt", "w") as f:
    f.write(str(CBW_Int))

#pn.io.server_stop()
pn.io.serve(pn).stop()

button = Button(label=“Stop”, button_type=“success”)
button.on_click(button_callback)

def plot_update():
fig = cbw_int_plot(plot=False)
return pn.pane.Matplotlib(fig)

def app():
return pn.Column(
button,
pn.Row(CBW_Int_slider),
plot_update,
)

pn.serve({“localhost:”: app}, port=5006)

Hi,
The way you describe it it seems you want to have the User-Input (setting CBW_int) in a separate python process from the actual work python app (using the CBW_Int).
Is there a specific reason for this ?

It’s much more common to just combine user-input and data-processing/plotting in one python app (be it via a bokeh/panel serve or in a jupyter notebook, or …) to allow user-interactions/updates/plotting …

If on the other hand what you try to achieve is just first showing the user some widgets to select/change parameters and then remove/hide those widgets and just create/show some plots, …

Hi Johann,

Yes, I bring up the widget temporarily just to get the slope of the line to fit the data. For the code in this example I am not plotting the data and only showing the fit line from the interactive widget.

The value obtained from this widget does get used later on after displaying the widget after I close the widget.

For a lack of being able to save this constant value to memory, I am just saving the CWW_Int constant in a text file upon a button push. I will then read the file later on in the program when I need to retrieve this constant for my work.

I have 4 more modules with interactive panel widgets and slide bars in the process where I need to save the slide bar values from the interactive widgets too complete my analysis, Again, I write these values to a text file so that they can be retrieved when needed,

The Panel interactive widgets in Jupyter Notebooks are passed on easily, but it is this panel server method that is causing the issues.

Thanks

Hi,
Hmm, I’m not seasoned enough in python/panel to help with how you’d just spin a panel/bokeh server to get some input for your own app that runs completely separately and kill that panel/bokeh server and continue with your work.

I usually just do the actual “work” in the callbacks and keeping the UI open for further interaction with the user. Similar to most of the examples in the docs etc. A basic example with configs across 3 classes.

import param 
import panel as pn

# random set of classes holding sets of configs with defaults
class ObserverConfig(param.Parameterized):
    # define config parameters
    angle = param.Number(default=0.25, bounds=(0, 0.5), step=0.01, doc='Viewing Angle')

class ObjectConfig(param.Parameterized):
    # define config parameters
    size = param.Selector(default='L', objects=['S', 'M', 'L', 'XL'], doc='Size of object')
    weight = param.Number(doc='Weight of object')

class EnvironmentConfig(param.Parameterized):
    # define config parameters
    temperature = param.Number(default=20, bounds=(-20, 50), step=0.1, doc='Temperature ')

# define a simple app
class MyApp(param.Parameterized):

    def __init__(self):
        self.observer_conf = ObserverConfig()
        self.object_conf = ObjectConfig()
        self.environment_conf = EnvironmentConfig()

    def view(self):
        dowork_button = pn.widgets.Button(name='Do Work', button_type='success')
        dowork_button.on_click(self.do_work)

        self.ui_column = pn.Column(
            self.observer_conf, self.object_conf, self.environment_conf,
            dowork_button)
        return self.ui_column
        
    def do_work(self, event=None):
        # clear the UI. We may want to use it later to display something else
        self.ui_column.clear()

        print('Doing work')
        print(f'angle={self.observer_conf.angle}')
        print(f'size={self.object_conf.size}')

        print('Doing more work')

app = MyApp()
app.view().show()

Having said this, the following links seem to touch on how to get more control over the server that’s launched:
https://panel.holoviz.org/how_to/server/programmatic.html

Hope someone more experienced can help.

I really like how you show your code. When I copy my code into the reply box it looses all formatting. How Do you do that?

Hi Johann,

Your code might just work. I am going to try it with my app and see if your method just might help. Thanks.

Hi Johann,

I used your idea and it works. Thank you.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')

import param
import panel as pn

# random set of classes holding sets of configs with defaults
class ObserverConfig(param.Parameterized):
    # define config parameters
    cbw_int = param.Number(default=0.25, bounds=(0, 0.5), step=0.01, doc='CBW Intercept')
    #cbw_int_calculated = param.Number(default=0.0, bounds=(0, 1), doc='Calculated CBW Intercept')

    @param.depends('cbw_int', watch=True)
    def cbw_int_plot(self):
        fig = plt.figure(figsize=(6, 6))
        plt.title('Vsh vs. CBWa', color='blue')
        plt.plot(np.arange(10), np.arange(10) * self.cbw_int, "k-", label="")
        plt.xlim(0.0, 1)
        plt.ylim(0.0, 1.0)
        plt.ylabel('CBWa [v/v]', color='blue')
        plt.xlabel('Vsh [v/v]', color='blue')
        plt.grid()
        
        # Calculate cbw_int and save it to cbw_int_calculated parameter
        self.cbw_int_calculated = 0.5 * self.cbw_int

        return fig

# define a simple app
class MyApp(param.Parameterized):
    def __init__(self):
        self.observer_conf = ObserverConfig()

    def view(self):
        dowork_button = pn.widgets.Button(name='Do Work', button_type='success')
        dowork_button.on_click(self.do_work)

        self.ui_column = pn.Column(
            pn.Param(self.observer_conf.param['cbw_int']),  # Slider for cbw_int
            self.observer_conf.cbw_int_plot,
            dowork_button
        )
        return self.ui_column

    def do_work(self, event=None):
        # clear the UI. We may want to use it later to display something else
        self.ui_column.clear()

        print('Doing work')
        print(f'CBW_Int ={self.observer_conf.cbw_int}')
        #print(f'calculated CBW_Int ={self.observer_conf.cbw_int_calculated}')

        print('Doing more work')

app = MyApp()
app.view().show()
#app.stop()

I have one more tweak to the code to finally obtain the constant of cbw_int from the interactive widget cross plot:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')

import param
import panel as pn

# random set of classes holding sets of configs with defaults
class ObserverConfig(param.Parameterized):
    # define config parameters
    cbw_int = param.Number(default=0.25, bounds=(0, 0.5), step=0.01, doc='CBW Intercept')
    ########cbw_int_calculated = param.Number(default=0.0, bounds=(0, 1), doc='Calculated CBW Intercept')

    @param.depends('cbw_int', watch=True)
    def cbw_int_plot(self):
        fig = plt.figure(figsize=(6, 6))
        plt.title('Vsh vs. CBWa', color='blue')
        plt.plot(np.arange(10), np.arange(10) * self.cbw_int, "k-", label="")
        plt.xlim(0.0, 1)
        plt.ylim(0.0, 1.0)
        plt.ylabel('CBWa [v/v]', color='blue')
        plt.xlabel('Vsh [v/v]', color='blue')
        plt.grid()
        
        # Calculate cbw_int and save it to cbw_int_calculated parameter
        #####self.cbw_int_calculated = 0.5 * self.cbw_int

        return fig

# define a simple app
class MyApp(param.Parameterized):
    def __init__(self):
        self.observer_conf = ObserverConfig()
        self.cbw_int = self.observer_conf.cbw_int  # Store cbw_int as an attribute

    def view(self):
        dowork_button = pn.widgets.Button(name='Do Work', button_type='success')
        dowork_button.on_click(self.do_work)

        self.ui_column = pn.Column(
            pn.Param(self.observer_conf.param['cbw_int']),  # Slider for cbw_int
            self.observer_conf.cbw_int_plot,
            dowork_button
        )
        return self.ui_column

    def do_work(self, event=None):
        # Update app.cbw_int with the value obtained from the slider
        self.cbw_int = self.observer_conf.cbw_int
        
        # clear the UI. We may want to use it later to display something else
        self.ui_column.clear()

        print('Doing work')
        print(f'CBW_Int = {self.cbw_int}')  # Access updated cbw_int from the class attribute
        #######print(f'calculated angle={self.observer_conf.cbw_int_calculated}')

        print('Doing more work')

app = MyApp()
app.view().show()

and then later on in the program you can print this constant using the following command:

# Access the updated cbw_int attribute outside the class after interacting with the slider
print(app.cbw_int)

Hi,
for the formatted code I use the “</>” from the “Reply window / Formatting section”.
It adds the following:

type or paste code here

Thanks for all the tips. Your code example works well for my application.

Hi,

some comments:

  • I usually try to avoid making copies of attributes/parameters in other classes, because as the apps get more complex it gets confusing and keeping them in sync may be a pain (also the watcher/depends function provided by “param” makes that somehow easy to do).
    Instead I usually access them via multiple hops or create methods to get&set them

  • the reason why using “param” is so cool is because
    a) it in the background links the actual param.parameter with the panel/widget details. So when you update the param.parameter it updates the widget displayed and vice versa. So you usually don’t need to link/bind the 2
    b) Panel automatically recognizes param.Parameterized classes and param.Parameters defined there, so this gives you quite some flexibility into turning them into widgets. the Panel doc is very good, but the flexibility can be overwhelming at first. So here the basics with an example.

import panel as pn
import param

class MySubUI(param.Parameterized):

    x = param.Number()
    y = param.Integer(default=3, step=1, bounds=(-5,5))

    # internal parameters, set precedence to -1 to automatically exclude
    # from widget generations by panel/param
    _z = param.Number(precedence=-1)

    
    def view(self):
        # Option 1:
        # just display all the parameters as widgets with the default widget mapping
        # panel has implemented. Panel will figure it all out by recognizing this is 
        # a param.Parameterized class)
        option1 = pn.Column(self)

        # Option 2:
        # Use pn.Param to get some flexility which parameters to include, 
        # the layout to use, and specify attributes for parameters and much more ... 
        option2 = pn.Param(self.param, default_layout=pn.Row, parameters=['y'], 
                           widgets={'y': pn.widgets.IntInput})

        # Option 3: 
        # Build widgets individually using the parameter definitions. 
        # useful if you want to build more complex layouts or need access to some widgets
        # directly
        w_x = pn.widgets.FloatInput.from_param(self.param.x)
        w_z = pn.widgets.FloatInput.from_param(self.param._z, height=200)
        option3 = pn.Column(
                        w_x, 
                        pn.Row(self.param.y, w_z))

        return pn.Column(
                    '## Option1:', option1, 
                    '## Option2:', option2, 
                    '## Option3:', option3)
app = MySubUI()
app.view()
  • once you use param, you get some cool parameter features as well:
  1. get RUN time value checking for free. Much less init code (if any)
  2. You can also get automatic defaults and set values in instances. Also you can “watch” parameters for changes and do things (this is what panel uses heavily behind the scenes).
  3. and, very nice you don’t have to rely on UI at all. Like you can very easily set your self.observer_conf.cbw_int directly in the SW or a CLI, or jupyter without having to use the widgets (even if you display them)
  4. param also allows you to serialize the parameters (within limits) into json files or other formats - I found that quite useful to save some user-defined settings for reuse in the next session.
  5. you can easily define your own “Parameters” for customized types of values, …
  • Last but not least I’m not sure what you use your plots for. If you mostly use them for “papers”, I guess you’re fine. Otherwise I’d recommend to take a look at “bokeh”, which has very good interactivity and is ideal for browser based plots. It also is quite easy to learn and integrates extremely well with panel. If you plot a lot of pandas/xarray stuff, hvplot is a nice higher-level interface, integrates into pandas.plot. And if you like abstract plotting and data-handling holoviews will be worth to look at (it also allows to easily switch between different plotting libraries, even on the fly).

  • Lastly not sure if your plotting/data analysis is very structured. May be worth to look into panel “Pipelines”. I stayed away from them as app is more a freeflow, flexibile data analysis app with no clear flow.

have fun

Thank you very much. I have moved many of my applications from the normal Panel version to Panel + Param per your suggestions when I need to use the server, and they work very well. The actual applications can be found at the following GitHub repository:

In this repository there is a Jupyter Notebook and a Geolog project in a zip file. The Geolog project requires the server version where we use Panel + Param. Panel works well in a Jupyter Notebook and rarely use a served widget in this application.