holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.42k stars 484 forks source link

Questions about `Update Progressively` tutorial #6681

Open jrycw opened 3 months ago

jrycw commented 3 months ago

While exploring this example, I've made two interesting observations:

  1. The variable has_result is defined but not used. It appears we could directly rely on the boolean value of result instead of employing result.rx.pipe(bool). Therefore, has_result can be removed.

  2. The _show_submit_message function depends on the boolean values of result and is_running. If we change these two pn.rx values sequentially (the first two lines of run_classification), there is a very brief moment where the "Click Submit" message will be shown on the screen, right after result.rx.value = "" but before is_running.rx.value = True is executed. I'm wondering if there's a technique to update these two pn.rx values simultaneously or to defer or throttle these changes just before the line prediction = classify(None).

MarcSkovMadsen commented 3 months ago

Thx. I've fixed question 1 in #6682

Regarding question 2 I'm not aware that you can update two reactive expressions at once, i.e. update

result.rx.value = ""
is_running.rx.value = True

simultaniously. Is it possible @philippjfr?

Reproduce

You can see the UI flashing when the button is clicked 2nd and 3rd time

https://github.com/holoviz/panel/assets/42288570/f03d0038-ffb6-4ca4-a53e-76d76fb48d05

import random
from time import sleep
import panel as pn

pn.extension()

OPTIONS = ["Wind Turbine", "Solar Panel", "Battery Storage"]

# State
result = pn.rx("")
is_running = pn.rx(False)

# Classifier function
def classify(image):
    sleep(2)
    return random.choice(OPTIONS)

# Reactive expressions
def _show_submit_message(result, is_running):
    return not result and not is_running

show_submit_message = pn.rx(_show_submit_message)(result, is_running)

def run_classification(_):
    result.rx.value = ""
    is_running.rx.value = True
    prediction = classify(None)
    result.rx.value = f"It's a {prediction}"
    is_running.rx.value = False

# Components
click_submit = pn.pane.Markdown("Click Submit", visible=show_submit_message)
run = pn.widgets.Button(
    name="Submit", button_type="primary", on_click=run_classification
)
progress_message = pn.Row(
    pn.indicators.LoadingSpinner(
        value=True, width=25, height=25, align="center", margin=(5, 0, 5, 10)
    ),
    pn.panel("Running classifier ...", margin=0),
    visible=is_running,
)

# Layout
pn.Column(run, click_submit, result, progress_message).servable()
jrycw commented 3 months ago

@MarcSkovMadsen Thanks for the demonstration. I'm considering whether it would be possible to encapsulate these two status changes within a form to send them simultaneously.

jrycw commented 3 months ago

With the help of .rx.when, I've rewritten the exercise as follows, and I've included a marker # *** to indicate the changed lines.

import random
from time import sleep

import panel as pn

pn.extension()

OPTIONS = ["Wind Turbine", "Solar Panel", "Battery Storage"]

# State
result = pn.rx("")
is_running = pn.rx(False)

# Classifier function
def classify(image):
    sleep(2)
    return random.choice(OPTIONS)

# Reactive expressions
def _show_submit_message(result, is_running):
    return not result and not is_running

show_submit_message = pn.rx(_show_submit_message)(result, is_running)
gate = pn.rx(False) # ***
gated_expr = show_submit_message.rx.when(gate) # ***

def run_classification(_):
    result.rx.value = ""
    is_running.rx.value = True

    gate.rx.value = True  # ***
    gate.rx.value = False # ***

    prediction = classify(None)
    result.rx.value = f"It's a {prediction}"
    is_running.rx.value = False

    gate.rx.value = True  # ***
    gate.rx.value = False # ***

# Components
click_submit = pn.pane.Markdown("Click Submit", visible=gated_expr # ***)
run = pn.widgets.Button(
    name="Submit", button_type="primary", on_click=run_classification
)
progress_message = pn.Row(
    pn.indicators.LoadingSpinner(
        value=True, width=25, height=25, align="center", margin=(5, 0, 5, 10)
    ),
    pn.panel("Running classifier ...", margin=0),
    visible=is_running,
)

# Layout
pn.Column(run, click_submit, result, progress_message).servable()

it appears that we can utilize gate.rx.value = True to update the status changes simultaneously. However, immediately after each gate.rx.value = True, it seems necessary to set gate.rx.value = False to enable simultaneous updates in the next round.

MarcSkovMadsen commented 3 months ago

Thanks. I hope @philippjfr will also chime in so that we get the best approach documented.

philippjfr commented 3 months ago

I would write it like this personally:

import random
import time
import panel as pn

pn.extension()

OPTIONS = ["Wind Turbine", "Solar Panel", "Battery Storage"]

# Classifier function
def classify(image):
    time.sleep(2)
    return random.choice(OPTIONS)

def format_prediction(_):
    yield pn.Row(
        pn.indicators.LoadingSpinner(
            value=True, width=25, height=25, align="center", margin=(5, 0, 5, 10)
        ),
        pn.panel("Running classifier ...", margin=0)
    )
    prediction = classify(None)
    yield f"It's a {prediction}" 

# Components
run = pn.widgets.Button(name="Submit", button_type="primary")
result = pn.bind(format_prediction, run).rx.when(run, initial='Click submit')

# Layout
pn.Row(
    run,
    pn.pane.ParamRef(result)
)

If we were actually classifying some image then it should be:

pn.bind(format_prediction, image_input).rx.when(run, initial='Click submit')