User Experience#
import time
import random
import param
import pandas as pd
import panel as pn
pn.extension()
The best practices described on this page serve as a checklist of items to keep in mind as you are developing your application. They include items we see users frequently get confused about or things that are easily missed but can make a big difference to the user experience of your application(s).
Note
Good: recommended, works.
Okay: works (with intended behavior), potentially inefficient.
Bad: Deprecated (may or may not work), just don’t do it.
Wrong: Not intended behavior, won’t really work.
Update params effectively#
Good#
Use obj.param.update
:
to update multiple parameters on an object simultaneously
as a context manager to temporarily set values, restoring original values on completion
def run(event):
with progress.param.update(
bar_color="primary",
active=True,
):
for i in range(0, 101):
time.sleep(0.01)
progress.value = i
button = pn.widgets.Button(name="Run", on_click=run)
progress = pn.indicators.Progress(value=100, active=False, bar_color="dark")
pn.Row(button, progress)
Okay#
The following shows setting parameters individually, which could be inefficient and may temporarily leave the object in an inconsistent state.
def run(event):
try:
progress.bar_color = "primary"
progress.active = True
for i in range(0, 101):
time.sleep(0.01)
progress.value = i
finally:
progress.bar_color = "dark"
progress.active = False
button = pn.widgets.Button(name="Run", on_click=run)
progress = pn.indicators.Progress(value=100, active=False, bar_color="dark")
pn.Row(button, progress)
Throttle slider callbacks#
Good#
When callbacks are expensive to run, you can prevent sliders from triggering too many callbacks, by setting throttled=True
. When throttled, callbacks will be triggered only once, upon mouse-up.
pn.extension(throttled=True)
def callback(value):
time.sleep(2)
return f"# {value}"
slider = pn.widgets.IntSlider(end=10)
output = pn.bind(callback, slider)
pn.Row(slider, output)
Good#
Alternatively, you can apply throttling only to the specific widgets with the most expensive callbacks, by binding to value_throttled
instead of value
.
def callback(value):
time.sleep(2)
return f"# {value}"
slider = pn.widgets.IntSlider(end=10)
output = pn.bind(callback, slider.param.value_throttled)
pn.Row(slider, output)
Bad#
Binding against value
can be really slow for expensive callbacks.
def callback(value):
time.sleep(2)
return f"# {value}"
slider = pn.widgets.IntSlider(end=10)
output = pn.bind(callback, slider.param.value)
pn.Row(slider, output)
Defer expensive operations#
Good#
It’s easy to defer the execution of all bound and displayed functions with pn.extension(defer_load=True)
(note this applies to served applications, not to interactive notebook environments):
pn.extension(defer_load=True, loading_indicator=True)
def onload():
time.sleep(5) # simulate expensive operations
return pn.Column(
"Welcome to this app!",
)
layout = pn.Column("Check this out!", onload)
# layout.show()
Okay#
If you need finer control, start by instantiating the initial layout with placeholder pn.Columns
, then populate it later in onload
.
import time
def onload():
time.sleep(1) # simulate expensive operations
layout[:] = ["Welcome to this app!"]
layout = pn.Column("Loading...")
display(layout)
pn.state.onload(onload)
Show indicator while computing#
Good#
Set loading=pn.state.param.busy
to overlay a spinner while processing to let the user know it’s working.
def process_load(event):
time.sleep(3)
button = pn.widgets.Button(name="Click me", on_click=process_load)
widget_box = pn.WidgetBox(button, loading=pn.state.param.busy, height=300, width=300)
widget_box
Good#
Set loading=True
to show a spinner while processing to let the user know it’s working.
def compute(event):
with layout.param.update(loading=True):
time.sleep(3)
layout.append("Computation complete!")
button = pn.widgets.Button(name="Compute", on_click=compute)
layout = pn.Column("Click below to compute", button)
layout
Okay#
You can also wrap a try/finally
to do the same thing.
def compute(event):
try:
layout.loading = True
time.sleep(3)
layout.append("Computation complete!")
finally:
layout.loading = False
button = pn.widgets.Button(name="Compute", on_click=compute)
layout = pn.Column("Click below to compute", button)
layout
Manage exceptions gracefully#
Good#
Use:
try
block to update values on successexcept
block to update values on exceptionfinally
block to update values regardless
def compute(divisor):
try:
busy.value = True
time.sleep(1)
output = 1 / divisor
text.value = "Success!"
except Exception as exc:
output = "Undefined"
text.value = f"Error: {exc}"
finally:
busy.value = False
return f"Output: {output}"
busy = pn.widgets.LoadingSpinner(width=10, height=10)
text = pn.widgets.StaticText()
slider = pn.widgets.IntSlider(name="Divisor")
output = pn.bind(compute, slider)
layout = pn.Column(pn.Row(busy, text), slider, output)
layout
Cache values for speed#
Good#
Wrap your callback with a pn.cache
decorator so that values are automatically cached so that expensive computations are not repeated.
@pn.cache
def callback(value):
time.sleep(2)
return f"# {value}"
slider = pn.widgets.IntSlider(end=3)
output = pn.bind(callback, slider.param.value_throttled)
pn.Row(slider, output)
Okay#
Or, manually handle the cache yourself with pn.state.cache
.
def callback(value):
output = pn.state.cache.get(value)
if output is None:
time.sleep(2)
output = f"# {value}"
pn.state.cache[value] = output
return output
slider = pn.widgets.IntSlider(end=3)
output = pn.bind(callback, slider.param.value_throttled)
pn.Row(slider, output)
Preserve axes ranges on update#
Good#
When you are working with HoloViews or hvPlot in Panel, you can prevent the plot from resetting to its original axes ranges when zoomed in by wrapping it with hv.DynamicMap
.
import numpy as np
import holoviews as hv
hv.extension("bokeh")
data = []
def add_point(clicks):
data.append((np.random.random(), (np.random.random())))
return hv.Scatter(data)
button = pn.widgets.Button(name="Add point")
plot = hv.DynamicMap(pn.bind(add_point, button.param.clicks))
pn.Column(button, plot)
Okay#
If you want the object to be completely refreshed, simply drop hv.DynamicMap
. If it’s a long computation, it’s good to set loading_indicator=True
.
import numpy as np
import holoviews as hv
hv.extension("bokeh")
pn.extension(defer_load=True, loading_indicator=True)
data = []
def add_point(clicks):
data.append((np.random.random(), (np.random.random())))
return hv.Scatter(data)
button = pn.widgets.Button(name="Add point")
plot = pn.bind(add_point, button.param.clicks)
pn.Column(button, plot)
FlexBox instead of Column/Row#
Good#
pn.FlexBox
automatically moves objects to another row/column, depending on the space available.
rcolor = lambda: "#%06x" % random.randint(0, 0xFFFFFF)
pn.FlexBox(
pn.pane.HTML(str(5), styles=dict(background=rcolor()), width=1000, height=100),
pn.pane.HTML(str(5), styles=dict(background=rcolor()), width=1000, height=100)
)
Okay#
pn.Column
/pn.Row
will overflow if the content is too long/wide.
rcolor = lambda: "#%06x" % random.randint(0, 0xFFFFFF)
pn.Row(
pn.pane.HTML(str(5), styles=dict(background=rcolor()), width=1000, height=100),
pn.pane.HTML(str(5), styles=dict(background=rcolor()), width=1000, height=100)
)
Reuse objects for efficiency#
Good#
Imagine Panel components as placeholders and use them as such, rather than re-creating them on update.
def randomize(event):
df_pane.object = pd.DataFrame(np.random.randn(10, 3), columns=list("ABC"))
button = pn.widgets.Button(name="Compute", on_click=randomize)
df_pane = pn.pane.DataFrame()
button.param.trigger("clicks") # initialize
pn.Column(button, df_pane)
Okay#
If your callback returns a Panel object rather than the underlying object being displayed, you’ll end up instantiating the pn.pane.DataFrame
on every click (which is typically slower and will often have distracting flickering).
def randomize(clicks):
return pn.pane.DataFrame(pd.DataFrame(np.random.randn(10, 3), columns=list("ABC")))
button = pn.widgets.Button(name="Compute")
df_pane = pn.bind(randomize, button.param.clicks)
button.param.trigger("clicks") # initialize
pn.Column(button, df_pane)