Add Interactivity with pn.bind#

Both Streamlit and Panel are reactive frameworks that respond dynamically to user interactions within your application. However, their operational mechanics differ significantly:

In Streamlit:

  • The script runs once when a user visits the page.

  • The script reruns from the top to bottom upon any user interaction.

In Panel:

  • The script runs once when a user visits the page.

  • Only specific, bound functions are rerun based on user interactions.

Panel’s interactivity architecture enables you to develop and maintain larger and more complex apps efficiently.

Introduction#

Panel’s pn.bind function allows you to bind functions to widgets, creating what we refer to as bound functions.

To use pn.bind, follow these steps:

# 1. Define your function(s)
def my_func(value):
    return ...
# 2. Define your widgets
slider = pn.widgets.IntSlider(...)
# 3. Bind your function to the widget(s)
my_bound_func = pn.bind(my_func, value=slider)
# 4. Layout and display your bound functions and widgets
pn.Column(slider, my_bound_func)

Once you’ve set up your functions and widgets as described, they will automatically update in response to user interactions.

Migration Steps#

Consider the following strategies for effective migration:

  • Transition your core business logic into functions. This includes data loading, transformations, calculations, plot creations, complex computations like galaxy mass estimations, model training, and inference tasks.

  • Enhance interactivity by using pn.bind to associate your functions with widgets.

    • For updates that occur multiple times during function execution, consider using generator functions (yield).

  • Use visual feedback to indicate activity, as detailed in the Show Activity Section.

Examples#

Basic Interactivity Example#

Learn how to adapt your code for a straightforward result update that occurs once the execution completes.

Streamlit Basic Interactivity Example#

import matplotlib.pyplot as plt
import numpy as np
import streamlit as st

from matplotlib.figure import Figure

data = np.random.normal(1, 1, size=100)
fig = Figure(figsize=(8,4))
ax = fig.subplots()
bins = st.slider(value=20, min_value=10, max_value=30, step=1, label="Bins")
ax.hist(data, bins=bins)

st.pyplot(fig)

Streamlit Basic Interactivity Example

In Streamlit, the entire script is rerun from top to bottom when the bins slider is adjusted.

Panel Basic Interactivity Example#

import panel as pn
import numpy as np

from matplotlib.figure import Figure

pn.extension(sizing_mode="stretch_width", template="bootstrap")

def plot(data, bins):
    fig = Figure(figsize=(8,4))
    ax = fig.subplots()
    ax.hist(data, bins=bins)
    return fig

data = np.random.normal(1, 1, size=100)
bins_input = pn.widgets.IntSlider(value=20, start=10, end=30, step=1, name="Bins")
bplot = pn.bind(plot, data=data, bins=bins_input)

pn.Column(bins_input, bplot).servable()

Panel Basic Interactivity Example

Panel updates more swiftly and smoothly by only rerunning the plot function when the bins slider is changed.

Multiple Updates Example#

Discover how to handle scenarios where a function needs to update the UI multiple times during its execution.

Streamlit Multiple Updates Example#

import random
import time

import streamlit as st

def calculation_a():
    time.sleep(1.5)
    return random.randint(0, 100)

def calculation_b():
    time.sleep(3.5)
    return random.randint(-100, 0)

st.write("# Calculation Runner")
option = st.radio("Which calculation would you like to perform?", ("A", "B"))
st.write("You chose: ", option)
if st.button("Press to run calculation"):
    with st.spinner("Running... Please wait!"):
        time_start = time.perf_counter()
        result = calculation_a() if option == "A" else calculation_b()
        time_end = time.perf_counter()
    st.write(f"""
Done!

Result: {result}

The function took {time_end - time_start:1.1f} seconds to complete
"""
    )
else:
    st.write(f"Calculation {option} did not run yet")

Streamlit multiple updates example

Panel Multiple Updates Example#

In Panel, utilize a generator function to provide incremental updates during the function’s execution.

import time
import random

import panel as pn

pn.extension(sizing_mode="stretch_width", template="bootstrap")
pn.state.template.param.update(site="Panel", title="Calculation Runner")

def notify_choice(calculation):
    return f"You chose: {calculation}"

def calculation_a():
    time.sleep(2)
    return random.randint(0, 100)

def calculation_b():
    time.sleep(1)
    return random.randint(0, 100)

def run_calculation(running, calculation):
    if not running:
        yield "Calculation did not run yet"
        return # This will break the execution

    calc = calculation_a if calculation == "A" else calculation_b
    with run_input.param.update(loading=True):
        yield pn.indicators.LoadingSpinner(
            value=True, size=50, name='Running... Please Wait!'
        )
        time_start = time.perf_counter()
        result = calc()
        time_end = time.perf_counter()
        yield f"""
        Done!

        Result: {result}

        The function took {time_end - time_start:1.1f} seconds to complete
        """

calculation_input = pn.widgets.RadioBoxGroup(name="Calculation", options=["A", "B"])
run_input = pn.widgets.Button(
    name="Press to run calculation",
    icon="caret-right",
    button_type="primary",
    width=250,
)
pn.Column(
    "Which calculation would you like to perform?",
    calculation_input,
    pn.bind(notify_choice, calculation_input),
    run_input,
    pn.bind(run_calculation, run_input, calculation_input),
).servable()

Panel Multiple Updates Example

The use of pn.indicators.LoadingSpinner effectively signals ongoing activity during the calculation process.

Panel Multiple Updates Alternative Indicator Example#

Instead of using a visual indicator, another approach involves modifying the .disabled and .loading parameters of the calculation_input and run_input to reflect the current state of the operation dynamically.

import time
import random

import panel as pn

pn.extension(template="bootstrap")
pn.state.template.param.update(site="Panel", title="Calculation Runner")

def notify_choice(calculation):
    return f"You chose: {calculation}"

def calculation_a():
    time.sleep(2)
    return random.randint(0, 100)

def calculation_b():
    time.sleep(1)
    return random.randint(0, 100)

def run_calculation(running, calculation):
    if not running:
        return "Calculation did not run yet"

    calc = calculation_a if calculation == "A" else calculation_b
    with run_input.param.update(loading=True), calculation_input.param.update(disabled=True):
        time_start = time.perf_counter()
        result = calc()
        time_end = time.perf_counter()
        return f"""
        Done!

        Result: {result}

        The function took {time_end - time_start:1.1f} seconds to complete.
        """

calculation_input = pn.widgets.RadioBoxGroup(name="Calculation", options=["A", "B"])
run_input = pn.widgets.Button(
    name="Press to run calculation",
    icon="caret-right",
    button_type="primary",
    width=250,
)
pn.Column(
    "Which calculation would you like to perform?",
    calculation_input,
    pn.bind(notify_choice, calculation_input),
    run_input,
    pn.bind(run_calculation, run_input, calculation_input),
).servable()

Panel Multiple Updates Alternative Example

Multiple Results Example#

In some scenarios, such as with Large (AI) Language Models, you may need to output multiple results sequentially as they become available.

Streamlit Multiple Results Example#

import random
import time

import streamlit as st

run = st.button("Run model")

def model():
    time.sleep(1)
    return random.randint(0, 100)

if not run:
    st.write("The model has not run yet")
else:
    with st.spinner("Running..."):
        for i in range(10):
            result = model()
            st.write(f"Result {i}: {result}")

Streamlit Multiple Results Example

Panel Multiple Results Example#

Panel utilizes a generator function to dynamically display multiple results from a single execution process as they become ready.

import random
import time

import panel as pn

pn.extension(sizing_mode="stretch_width", template="bootstrap")

def model():
    time.sleep(1)
    return random.randint(0, 100)

def results(running):
    if not running:
        yield layout
        return

    layout.clear()
    for i in range(10):
        layout.append(loading)
        yield layout
        result = model()
        result = pn.panel(f"Result {i}: {result}")
        layout[-1] = result
        yield layout

run_input = pn.widgets.Button(name="Run model")
loading = pn.widgets.LoadingSpinner(value=True, size=50, name="Running... Please Wait!")
layout = pn.Column("Calculation did not run yet")

pn.Column(run_input, pn.bind(results, run_input)).servable()

Panel Multiple Results Example