React to User Input#

Welcome to the interactive world of Panel! In this section, you’ll learn how to make your Panel applications come alive by reacting to user input. We’ll explore how to bind widgets to a function and add side effects using the watch parameter in Panel.

Embrace pn.bind#

The pn.bind method is your gateway to interactive Panel applications. It enables you to build interactive components that respond to user inputs simply by binding widgets to functions. Let’s dive into an example:

import panel as pn

pn.extension()

def calculate_power(wind_speed, efficiency):
    power_generation = wind_speed * efficiency
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power_generation:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)

efficiency = 0.3

power = pn.bind(
    calculate_power, wind_speed=wind_speed, efficiency=efficiency
)

pn.Column(wind_speed, power).servable()

As you interact with the slider, notice how the displayed power generation dynamically updates, reflecting changes in wind speed.

You can of course bind multiple widgets. Let’s make the efficiency a widget:

import panel as pn

pn.extension()

def calculate_power(wind_speed, efficiency):
    power_generation = wind_speed * efficiency
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power_generation:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")

power = pn.bind(
    calculate_power, wind_speed=wind_speed, efficiency=efficiency
)

pn.Column(wind_speed, efficiency, power).servable()

Using References#

Bound functions can be displayed directly as we have done above or they can be used as references when passed to a Panel component. This approach is usually more efficient since we only have to update the specific parameter:

import panel as pn

pn.extension()

def calculate_power(wind_speed, efficiency):
    power_generation = wind_speed * efficiency
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power_generation:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")

power = pn.bind(
    calculate_power, wind_speed=wind_speed, efficiency=efficiency
)

power_md = pn.pane.Markdown(power)

pn.Column(wind_speed, efficiency, power_md).servable()

Note how we pass the bound function as an argument to the Markdown pane. This way the Markdown pane only has to send the updated text.

Crafting Interactive Forms#

Forms are powerful tools for collecting user inputs. With Panel, you can easily create forms and process them after they are submitted:

import panel as pn

pn.extension()

def calculate_power(wind_speed, efficiency):
    power_generation = wind_speed * efficiency
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power_generation:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")

power = pn.bind(
    calculate_power, wind_speed=wind_speed, efficiency=efficiency
)

submit = pn.widgets.Button(name="Submit", button_type="primary")

def result(clicked):
    if clicked:
        return power()
    return "Click Submit"

result = pn.pane.Markdown(pn.bind(result, submit))

pn.Column(
    wind_speed, efficiency, submit, result
).servable()

Notice how the text is updated only when the Submit Button is clicked.

Harnessing Throttling for Performance#

To prevent excessive updates and ensure smoother performance, you can apply throttling by binding the value_throttled parameter. This limits the rate at which certain actions or events occur, maintaining a balanced user experience:

import panel as pn

from time import sleep

pn.extension()

def calculate_power(wind_speed, efficiency):
    power_generation = wind_speed * efficiency
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power_generation:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)

efficiency = 0.3

calculate_power_bnd = pn.bind(
    calculate_power, wind_speed=wind_speed.param.value_throttled, efficiency=efficiency
)

power_md = pn.pane.Markdown(calculate_power_bnd)

pn.Column(wind_speed, power_md).servable()

Try dragging the slider. Notice that the calculate_power function is only run when you release the mouse.

Binding to bound functions#

Bound functions can themselves be bound to other functions. This can help you break down you reactivity into smaller, reusable steps.

import panel as pn

pn.extension()

def power_generation(wind_speed, efficiency):
    return wind_speed * efficiency

def format_power_gen(wind_speed, efficiency, power):
    return (
        f"Wind Speed: {wind_speed} m/s, "
        f"Efficiency: {efficiency}, "
        f"Power Generation: {power:.1f} kW"
    )

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)

efficiency = 0.3

power = pn.bind(power_generation, wind_speed, efficiency)

power_text = pn.bind(format_power_gen, wind_speed, efficiency, power)

pn.Column(wind_speed, power, power_text).servable()

Warning

Binding to bound functions can help you to quickly explore your data, but it can be inefficient as the results are calculated from scratch for each call.

Try changing the power_generation function to:

def power_generation(wind_speed, efficiency):
    print(wind_speed, efficiency)
    return wind_speed * efficiency

Try dragging the wind_speed slider. Notice that the power_generation function is called twice every time you change the wind_speed value.

To solve this problem you should add caching (pn.cache) or use reactive expressions (pn.rx). You will learn about reactive expressions in the next section.

Triggering Side Effects with watch#

When you need to trigger additional tasks in response to user actions, setting watch comes in handy:

import panel as pn

pn.extension()

submit = pn.widgets.Button(name="Start the wind turbine")

def start_stop_wind_turbine(clicked):
    if submit.clicks % 2:
        submit.name = "Start the wind turbine"
    else:
        submit.name = "Stop the wind turbine"

pn.bind(start_stop_wind_turbine, submit, watch=True)

pn.Column(submit).servable()

Warning

In the example provided, our side effect directly modifies the UI by altering the name of the Button. However, this approach indicates poor architectural design.

It’s advisable to avoid directly updating the UI through side effects. Instead, focus on updating the application’s state, allowing the UI to respond automatically to any changes in the state. The concept of state will be explored further in the subsequent section.

If your task is long running you might want to disable the Button and add a loading indicator while the task is running.

import time
import panel as pn

pn.extension()

submit = pn.widgets.Button(name="Start the wind turbine")

def start_stop_wind_turbine(clicked):
    with submit.param.update(loading=True, disabled=True):
        time.sleep(2)
        if bool(submit.clicks%2):
            submit.name = "Start the wind turbine"
        else:
            submit.name = "Stop the wind turbine"

pn.bind(start_stop_wind_turbine, submit, watch=True)

pn.Column(submit).servable()

Keep the UI responsive with threads or processes#

To keep your UI and server responsive while the long running, blocking task is running you might want to run it asynchronously in a separate thread:

from time import sleep

import asyncio

import panel as pn

pn.extension()

submit = pn.widgets.Button(name="Start the wind turbine")

async def start_stop_wind_turbine(clicked):
    with submit.param.update(loading=True, disabled=True):
        result = await asyncio.to_thread(sleep, 5)

        if submit.clicks % 2:
            submit.name = "Start the wind turbine"
        else:
            submit.name = "Stop the wind turbine"

pn.bind(start_stop_wind_turbine, submit, watch=True)

pn.Column(submit).servable()

Note

In the example we use a asyncio.to_thread this should work great if your blocking task releases the GIL while running. Tasks that request data from the web or read data from files typically do this. Some computational methods from Numpy, Pandas etc. also release the GIL.

If your long running task does not release the GIL you may have to use a ProcessPoolExecutor instead. This introduces some overhead though.

Recap#

You’ve now unlocked the power of interactivity in your Panel applications:

  • pn.bind(some_function, widget_1, widget_2): for seamless updates based on widget values.

  • pn.bind(some_task, some_widget, watch=True): for triggering tasks in response to user actions.

  • Throttling ensures smoother performance by limiting update frequency.

  • Utilizing async and threading keeps your UI responsive during long-running tasks.

Now, let your imagination run wild and craft dynamic, engaging Panel applications!

Resources#

How-to#

Explanation#

External#