Panel: independent loading indicators

Hello there,

The Panel application I am working on has three relatively independent sections.
In the examples below, it is similar in structure to test_loading6.

The data are taking time to load and visual clues to the end user are necessary.
Doing so through pane.state.param.busy make all sections look like they are being updated
at the same time while it is often not the case.

While trying to correct for this, I realised that my assumptions on the inner working of panel were incorrect.

I have a few questions regarding the way Panel dependencies work.

To my surprise, it seems fairly equivalent to put the parameter dependency on the loading function
(load_data1 in the below) or on the display function. In test_loading1, both functions are decorated and that works. Is there a guideline regarding how this should be done ?

In test_loading3, I am displaying the spinner through the standard mechanism and this works as expected.

In test_loading4, I am trying another approach with the intent of finding a way of decoupling the display of spinners. Yet, once the spinner is displayed, there are no further updates of the display.

test_loading5 is an attempt to force the redisplay of one part the panel through a periodic callback. I have tried several other approaches with no more luck.

Any suggestion would be welcome.

Kind regards,

Trad.

import panel as pn
import pandas as pd
import hvplot.pandas
import param
import numpy as np
import time
import winsound
import loguru



class test_loading_base(param.Parameterized):

    p1 = param.ObjectSelector(default = 2, objects=range(1,10))

    def __init__(self):
        self.pause = 3
        self.width_panel=400
        self.result = {}
        self.load_data1()
        print(pn.state.cache[2])


    @pn.depends('p1')
    def load_data1(self):
        time.sleep(self.pause)
        if self.p1 not in self.result:
            self.result[self.p1] =  pd.DataFrame(np.random.rand(self.p1,self.p1))
            pn.state.cache[self.p1] = self.result[self.p1]
        pass


class test_loading0(test_loading_base):

    @pn.depends('p1')
    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         )

    def panel(self):
        return self.section1()

class test_loading1(test_loading_base):
    # Works
    def result1(self,busy=True):

        self.load_data1()
        return pn.widgets.DataFrame(pn.state.cache[self.p1])

    @pn.depends('p1')
    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.result1
                         )

    def panel(self):
        return self.section1()

class test_loading2(test_loading_base):
    # Works too

    def result1(self):
        self.load_data1()
        winsound.Beep(4400,2)
        return pn.widgets.DataFrame(pn.state.cache[self.p1])

    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.result1
                         )
    def panel(self):
        return self.section1()


class test_loading3(test_loading_base):
    # works also

    def result1(self):
        self.load_data1()
        winsound.Beep(4400,2)
        return pn.widgets.DataFrame(pn.state.cache[self.p1])

    @pn.depends(pn.state.param.busy)
    def spin_or_result1(self,busy):
        if busy:
            return pn.indicators.LoadingSpinner(value=True)
        else:
            return self.result1()

    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.spin_or_result1
                         )

    def panel(self):
        return self.section1()


class test_loading4(test_loading_base):
    # THIS DOES NOT WORK

    def result1(self):
        self.load_data1()
        winsound.Beep(4400,2)
        return pn.widgets.DataFrame(pn.state.cache[self.p1])

    @pn.depends(pn.state.param.busy)
    def spin_or_result1(self,busy):
        if self.p1 not in pn.state.cache:
            return pn.indicators.LoadingSpinner(value=True)
        else:
            return self.result1()

    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.spin_or_result1)

    def panel(self):
        return self.section1()


class test_loading5 (param.Parameterized):
    # does not work either

    p1 = param.ObjectSelector(default = 2, objects=range(1,10))
    busy1 = param.Boolean(default = True)

    def __init__(self):
        self.pause = 3
        self.width_panel=400
        self.display_result1 = pn.indicators.LoadingSpinner(value=True)
        loguru.logger.info(f'Entering __init__')
        self.results = {}
        self.cb = pn.state.add_periodic_callback(self.spin_or_result1,
                               period=500,
                               count=None,
                               timeout=None,
                               start=False)
        self.load_data1()
        self.cb.start()
        loguru.logger.info(f'Exiting __init__')

    @pn.depends('p1')
    def load_data1(self):
        self.busy1 = True
        loguru.logger.info(f'Entering load_data1 - busy1: {self.busy1} ')
        time.sleep(self.pause)
        if self.p1 not in self.results:
            self.results[self.p1] =  pd.DataFrame(np.random.rand(self.p1,self.p1))
            pn.state.cache[self.p1] = self.results[self.p1]
        self.busy1 = False
        loguru.logger.info(f'Exiting load_data1 - busy1: {self.busy1} ')
        pass

    def spin_or_result1(self):
        busy = self.busy1
        if busy:
            self.display_result1 = pn.indicators.LoadingSpinner(value=True)
        else:
            self.display_result1 = pn.widgets.DataFrame(pn.state.cache[self.p1])

    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.display_result1)

    def panel(self):
        return self.section1()

class test_loading6(test_loading_base):

    p2 = param.ObjectSelector(default = 10, objects=range(10,20))

    def key2(self):
       return  'p2-'+str(self.p2)

    def load_data2(self):
        time.sleep(self.pause)
        if not self.key2() in pn.state.cache:
            self.result[self.key2()] =  pd.DataFrame(np.random.rand(self.p2,self.p2))
            pn.state.cache[self.key2()] = self.result[self.key2()]

    def result1(self):
        self.load_data1()
        winsound.Beep(4400,2)
        return pn.widgets.DataFrame(pn.state.cache[self.p1])

    def result2(self):
        winsound.Beep(6600,30)
        self.load_data2()
        return pn.widgets.DataFrame(pn.state.cache[self.key2()])

    @pn.depends(pn.state.param.busy)
    def spin_or_result1(self,busy):
        #if self.p1 not in pn.state.cache:
        if busy:
            return pn.indicators.LoadingSpinner(value=True)
        else:
            return self.result1()


    @pn.depends(pn.state.param.busy)
    def spin_or_result2(self,busy):
        #if self.key2() not in pn.state.cache:
        if busy:
            return pn.indicators.LoadingSpinner(value=True)
        else:
            return self.result2()


    @pn.depends('p1')
    def section1(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 1:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p1,
                         self.spin_or_result1)

    @pn.depends('p2')
    def section2(self):
        loguru.logger.info('in func section1')
        return pn.Column(pn.layout.Divider(width=self.width_panel),
                         pn.Row('# Section 2:'),
                         pn.layout.Divider(width=self.width_panel),
                         self.param.p2,
                         self.spin_or_result2)

    def panel(self):
        return pn.Column(self.section1(), self.section2())

test = test_loading6()
test.panel().show()

# test2 = test_loading5()
# test2.panel().show()

Hi @Trad

Great questions. I would recommend posting the question in the HoloViz/ Panel discourse forum https://discourse.holoviz.org/.

There would be more Panel users and the solution could help build the community knowledge base.

1 Like

Hi Marc,

I had indeed overlooked the Panel Discourse forum.
I have posted my question there.

Thank you for pointing me to the Panel community forum.

Kind regards.

1 Like