Reactive Expressions#
In this section you will learn about pn.rx
. pn.rx
extends the concepts from pn.bind
that your learned in the previous section.
Note
You might feel some repetition from the previous section on pn.bind
. We do this on purpose to enable you to compare and contrast. pn.rx
is an extension of pn.bind
that provides more power and flexibility. pn.bind
has been the core API in Panel for a long time, so you will meet it across our documentation and community sites, and thus its very important to learn.
pn.rx
will enable you to build more complicated applications using a more flexible and maintainable architecture.
Embrace pn.rx
#
pn.rx
allows you to treat any object as a reactive expression. This means we can do things like multiplying a widget (representing the wind speed) with a float value (representing the efficiency) and then format the result all without writing callbacks:
import panel as pn
pn.extension()
wind_speed = pn.widgets.FloatSlider(
value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = 0.3
power = wind_speed.rx() * efficiency
power_text = pn.rx(
"Wind Speed: {wind_speed} m/s, "
"Efficiency: {efficiency}, "
"Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
pn.Column(power_text).servable()
You will notice how adding power_text
to the Column
displays both the widget and the bound function.
To separate the widget and bound function we can use the power_text
as a reference when we construct a Markdown
pane:
import panel as pn
pn.extension()
wind_speed = pn.widgets.FloatSlider(
value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = 0.3
power = wind_speed.rx() * efficiency
power_text = pn.rx(
"Wind Speed: {wind_speed} m/s, "
"Efficiency: {efficiency}, "
"Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
power_md = pn.pane.Markdown(power_text)
pn.Column(wind_speed, power_md).servable()
You can of course write expressions with multiple widgets. Lets make the efficiency
a widget:
import panel as pn
pn.extension()
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 = wind_speed.rx() * efficiency.rx()
power_text = pn.rx(
"Wind Speed: {wind_speed} m/s, "
"Efficiency: {efficiency}, "
"Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
power_md = pn.pane.Markdown(power_text)
pn.Column(wind_speed, efficiency, power_md).servable()
Crafting Interactive Forms#
Forms are powerful tools for collecting user inputs. With .rx.when
you can easily defer some calculation (i.e. the form submission) until some event (such as a button click) is triggered:
import panel as pn
pn.extension()
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))"
)
submit = pn.widgets.Button(name="Submit", button_type="primary")
power = wind_speed.rx() * efficiency.rx()
power_text = pn.rx(
"Wind Speed: {wind_speed} m/s, "
"Efficiency: {efficiency}, "
"Power Generation: {power:.1f} kW"
).format(
wind_speed=wind_speed,
efficiency=efficiency,
power=power
).rx.when(submit)
pn.Column(
wind_speed, efficiency, submit, pn.pane.Markdown(power_text)
).servable()
Try changing some of the inputs and clicking the submit Button. Try again. Notice how the text is only updated when we click the submit Button - we used .rx.when
to achieve this effect.
Harnessing Throttling for Performance#
To prevent excessive updates and ensure smoother performance, you can apply throttling (.value_throttled
). This limits the rate at which certain actions or events occur, maintaining a balanced user experience:
import panel as pn
pn.extension()
wind_speed = pn.widgets.FloatSlider(
value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = 0.3
power = wind_speed.param.value_throttled.rx() * efficiency
power_text = pn.rx(
"Wind Speed: {wind_speed} m/s, "
"Efficiency: {efficiency}, "
"Power Generation: {power:.1f} kW"
).format(
wind_speed=wind_speed.param.value_throttled,
efficiency=0.3,
power=power
)
pn.Column(
wind_speed, pn.pane.Markdown(power_text)
).servable()
Try dragging the slider. Notice that the text is only updated when you release the mouse.
Triggering Side Effects with .watch
#
When you need to trigger additional tasks in response to user actions, using .watch
comes in handy:
import panel as pn
pn.extension()
# Declare state of application
is_stopped = pn.rx(True)
rx_name = is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine")
submit = pn.widgets.Button(name=rx_name)
def toggle_wind_turbine(clicked):
is_stopped.rx.value = not is_stopped.rx.value
submit.rx.watch(toggle_wind_turbine)
pn.Column(submit).servable()
Here we store the state of the windturbine in a separate rx
variable, whenever the submit button is clicked we toggle the state.
Tip
When building apps using functions we recommend this approach to storing state. Applications are easier to reason about when the application displays according to the state instead of being changed directly.
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 asyncrounously in a separate thread:
import asyncio
import time
import panel as pn
pn.extension()
is_stopped = pn.rx(True)
submit = pn.widgets.Button(
name=is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine"),
)
async def start_stop_wind_turbine(clicked):
with submit.param.update(loading=True, disabled=True):
await asyncio.to_thread(time.sleep, 1)
is_stopped.rx.value = not is_stopped.rx.value
submit.rx.watch(start_stop_wind_turbine)
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.
Recommended Reading#
We do recommend you study the explanation document about reactivity in Panel and the ReactiveExpr
reference guide to learn more about displaying reactive expressions in Panel.
Recap#
You’ve now unlocked the power of interactivity in your Panel applications:
some_widget.rx()
: for seamless updates based on widget values.pn.rx(some_function)(widget_1, widget_2)
: for seamless updates based on widget values.pn.rx(some_task, some_widget).rx.watch()
: 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!