Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the panel preloader solution (progress bar, etc.)? #487

Closed
lastmeta opened this issue Jun 20, 2019 · 5 comments
Closed

What is the panel preloader solution (progress bar, etc.)? #487

lastmeta opened this issue Jun 20, 2019 · 5 comments
Assignees
Labels
type: enhancement Minor feature or improvement to an existing feature
Milestone

Comments

@lastmeta
Copy link

lastmeta commented Jun 20, 2019

We are using Panel to serve data retrieved in real-time from a database. The user clicks on a Run button and a query gets executed against a database and returns results so it can be displayed in the browser. (For our company's internal use).

Some queries can take several seconds to run. we would like to first display a spinner or a progress bar, or at least a "please wait" html element when someone clicks on the button, which disappears once the page is updated with a table of the data (or other visualization).

So far we cannot figure out the best way to do this. However, it seems so elementary I'm sure there must be a Panel best practice. If not, perhaps one should be developed?

I'll give you our code structure so you can see how we're trying to fit it in:

import os
import warnings
import datetime as dt

from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Button, DataTable, TableColumn, NumberFormatter
import param
import panel as pn

...

class BaseReport(param.Parameterized):
    """
    Base class for producing tabular OLAP cube based reports
    """
    run = param.Action(lambda x: x.param.trigger('run'), label='Run')     # --NOTICE--

    def __init__(self, **params):
        super(BaseReport, self).__init__(**params)
        self.init = True
        self.source = ColumnDataSource()
        self.columns = ColumnDataSource()
        self.columns.data = {'report':[self.__class__.name],
                             'columns':[' ']}
        self.download = Button(label=u"Download \u21E9", width=100,
                               button_type='primary')
        self.download.callback = CustomJS(
            args=dict(source=self.source, cols=self.columns),
            code=get_download_callback())
        self.run.width = 100
        self.run_button = pn.widgets.Button(                        # --NOTICE--
            button_type='primary',
            name='Run', 
            margin=0)
        run_format = pn.Param(
            self.param, 
            widgets={'run': self.run_button},                         # --NOTICE--
            show_name=False,
            width=100)
        self.actions = pn.Pane(pn.Row(
            run_format,
            pn.Pane(self.download, margin=0)),
            margin=(0, 0, 0, 0))
        if self.__class__.__dict__.get('where', None) is None:
            self.where = []

        ...

    @param.depends('run')                                       # --NOTICE--
    def table(self):
        ''' The tabular data as an HTML table'''
        if self.init:
            # Show nothing if report has not been run
            self.init = False
            self.download.disabled=True
            return pn.pane.Markdown('')
        self.download.disabled = False
        # WE WOULD LOVE TO HAVE THE SPINNER ENGAGE HERE, BEFORE get_data() IS CALLED...
        self.source.data = self.get_data().data
        columns = [TableColumn(field=item, title=item)
                   if item not in self.formats.keys()
                   else TableColumn(field=item, title=item,
                                    formatter=NumberFormatter(format=self.formats.get(item, '0,0')))
                   for item in self.columns.data['columns']]
        return pn.pane.Bokeh(DataTable(
            source=self.source, columns=columns, height=750, sizing_mode='stretch_both'))

    def serve(self):
        # Layout
        widgets = [getattr(self, item) for item in self.widget_list]
        gspec = pn.GridSpec(sizing_mode='stretch_both')
        gspec[0, 0:30] = pn.Row(pn.pane.Markdown(f'## {self.report_title}',
                                                 style={'color': '#428bca'}),
                                pn.layout.HSpacer(),
                                pn.Column(self.actions, pn.layout.VSpacer()))
        gspec[1, 0:30] = pn.pane.HTML('<hr>')
        gspec[2:15, 0:30] = pn.Row(pn.WidgetBox(*widgets), self.table,
                                pn.layout.VSpacer())
        gspec[15, 0:30] = pn.pane.HTML('<hr>')
        return gspec.servable()

One simple feature that could be used to solve this kind of stuff might be to allow the programmer to add an id or onclick or other arbitrary items to widgets and other panel elements. If I can add an id="execute_query" or onclick="showSpinner()" to the run button then I could have some javascript on the client side that would handle it.

I'm not a web developer so I don't know if that would actually work or if its the right way to do things, but in my tiny amount of experience other frameworks like Ruby on Rails and Phoenix allow you to add arbitrary items into your HTML tags, which I've found useful.

@philippjfr philippjfr added the type: enhancement Minor feature or improvement to an existing feature label Jul 25, 2019
@philippjfr philippjfr added this to the v0.7.0 milestone Jul 25, 2019
@jbogaardt
Copy link

Similar/simpler question - how does someone add a javascript callback to a panel Button?

Bokeh solution is straight forward:

from bokeh.models.widgets import Button
from bokeh.io import show
from bokeh.models import CustomJS

button = Button(callback=CustomJS(code='console.log("Hello browser")'))
show(button)

I cannot figure out the pn.widgets.Button equivalent

@flothesof
Copy link

Hi all,
I have a similar problem, except that I'm not fetching data in a database, but need time to make a measurement with a connected device.
I know in advance how long I will have to wait (e.g. 60 or 120 seconds) and I'm looking for a way to show the remaining time to the user.
Has any of you @ProperName @jbogaardt been able to find a working solution for this?
Thank you
Florian

@jbogaardt
Copy link

jbogaardt commented Oct 8, 2019

We have a hacky solution that falls outside of what we could figure out with panel. Since our panel app is sitting within a larger flask app, we had a but more freedom but basically we used Javascript to do it. We did this:

  1. bind an onclick() method to our button from flask when the panel app initially loads
  2. The onclick method creates a spinner (div) in the html page that shows up right when it is clicked (purely JS) with no waiting. This spinner is given the same class (to identify it) as what panel will render and is in the same place in the html page.
  3. When the database is done loading, panel.serve() fully replaces the spinner with the rendered results.

@xavArtley
Copy link
Collaborator

xavArtley commented Oct 8, 2019

This PR should solve your primary problem : #665

One simple feature that could be used to solve this kind of stuff might be to allow the programmer to add an id or onclick or other arbitrary items to widgets and other panel elements. If I can add an id="execute_query" or onclick="showSpinner()" to the run button then I could have some javascript on the client side that would handle it.

Else something like that could do the trick:

import panel as pn
pn.extension()
cd_code ="""
if (!target.remaining_time){
    target.margin = [5, 10]
    target.width = 200
    target.remaining_time = 1000*10 //10s
    target.text = ((target.remaining_time)/1000.).toString() + ' s remaining'
var x = setInterval(() => {   
    if(target.remaining_time > 1000) {
        target.remaining_time = target.remaining_time - 1000
        target.text = ((target.remaining_time)/1000.).toString() + ' s remaining'
    } else {
        target.text = ''
        target.margin = [0, 0, 0, 0]
        target.width = 0
        target.remaining_time = 0
        clearInterval(x)
    }
  
}, 1000);
}


"""
b = pn.widgets.Button(name='Click me')
t = pn.widgets.StaticText(value='', width=0, width_policy='fixed', margin=0)
b.jslink(t,code={'clicks':cd_code})
pn.Row(b, t)

@flothesof
Copy link

Thank you for the jslink solution, @xavArtley. This works nicely for my use case. Great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

5 participants