Tips and Tricks¶
Here, I will share random tips and tricks, but note the following are not officially endorsed by the maintainers; they’re simply things that worked the best for me.
The tips can be read chronologically, or jumped around, but I do try to put the less complex ones upfront and build up to the complex ones.
Litter prints and check both your terminal + browser console for debugging¶
If you are on using panel serve
, print
statements get passed to either terminal (for panel serve
) or browser console (for Jupyter after the first output). To get to the browser console on Chrome (and probably other browsers): Mouse-Right-Click
-> Inspect
-> Console
tab, or simply F12
-> Console
.
[1]:
import panel as pn
pn.extension()
button = pn.widgets.Button(name="Click")
def update(event):
num_clicks = event.obj.clicks
print(f"I was clicked {num_clicks} times!")
click_watch = button.param.watch(update, "clicks")
button
[1]:
It should look something like below.
[2]:
import panel as pn
pn.extension()
pn.pane.JPG("console.jpg")
[2]:
Use apply.opts
and param.value
for interactive updates¶
This is one way of doing it…
[3]:
import panel as pn
import holoviews as hv
pn.extension()
def update(alpha):
return hv.Curve(([0, 1, 2], [3, 4, 5])).opts(alpha=alpha)
widget = pn.widgets.FloatSlider(value=1)
plot = pn.bind(update, widget.param.value)
col = pn.Column(widget, plot)
But actually there’s no need for a chunky function; take advantage of apply.opts
!
[4]:
import panel as pn
import holoviews as hv
pn.extension()
widget = pn.widgets.FloatSlider(value=1)
plot = hv.Curve(([0, 1, 2], [3, 4, 5])).apply.opts(alpha=widget.param.value)
col = pn.Column(widget, plot)
Just remember the apply
and param
is important; without those the interactivity won’t work!
[5]:
import panel as pn
import holoviews as hv
pn.extension()
widget = pn.widgets.FloatSlider(value=1)
plot = hv.Curve(([0, 1, 2], [3, 4, 5])).opts(alpha=widget.value) # not interactive
col = pn.Column(widget, plot)
[6]:
import panel as pn
pn.extension()
pn.pane.GIF("use_apply.opts_and_param.value_for_interactive_updates.gif")
[6]:
Just to be clear, this works with other param
parameters like param.start
, param.stop
, param.options
, etc!
Use set_param
for simultaneous updates and duplicate computations¶
This is a poor way of updating multiple parameters simultaneously because select_update
gets triggered twice.
The reason is because, when options
gets updated from the click, the new options
are out of range from the original value
so value
is forced to update, triggering select_update
the first time, but then value
later gets manually updated to the last option, triggering select_update
again.
[7]:
import time
import panel as pn
pn.extension()
button = pn.widgets.Button(name="Click")
select = pn.widgets.Select(options=[0])
text = pn.widgets.StaticText(value=select.value)
def click_update(event):
options = [button.clicks + 1, button.clicks + 2]
select.options = options
select.value = options[-1]
def select_update(event):
time.sleep(1) # mimic expensive computation
text.value = str(event.new)
print("I was updated!")
click_watch = button.param.watch(click_update, "clicks")
select_watch = select.param.watch(select_update, "value")
col = pn.Column(button, select, text)
[8]:
import panel as pn
pn.extension()
pn.pane.GIF("use_set_param_for_simultaneous_updates_and_duplicate_computations_before.gif")
[8]:
Use set_param
to prevent the duplicate calls!
[9]:
import time
import panel as pn
pn.extension()
button = pn.widgets.Button(name="Click")
select = pn.widgets.Select(options=[0])
text = pn.widgets.StaticText(value=select.value)
def click_update(event):
select.param.set_param(
options=[button.clicks + 1, button.clicks + 2], value=button.clicks + 2
)
def select_update(event):
time.sleep(1) # mimic expensive computation
text.value = str(event.new)
print("I was updated!")
click_watch = button.param.watch(click_update, "clicks")
select_watch = select.param.watch(select_update, "value")
pn.Column(button, select, text)
[9]:
[10]:
import panel as pn
pn.extension()
pn.pane.GIF("use_set_param_for_simultaneous_updates_and_duplicate_computations_after.gif")
[10]:
Just be sure to prepend the param
namespace or else you’ll get a warning!
[11]:
import panel as pn
pn.extension()
select = pn.widgets.Select(options=[0])
select.set_param(options=[0, 1], value=1)
WARNING:param.Select: Use method 'set_param' via param namespace
Use value_throttled
for updates on mouse-up and expensive computations¶
If you have a slider, you may want to bind updates to value_throttled
instead of value
so that the plot updates on mouse-up, rather than every step to the desired value
. This is especially valuable if it takes long to update!
[12]:
import time
import numpy as np
import panel as pn
import holoviews as hv
pn.extension()
def update(func):
y = getattr(np, func)([0, 1, 2])
time.sleep(1)
return hv.Curve(([0, 1, 2], y)).opts(title=func)
widget = pn.widgets.DiscreteSlider(value="sin", options=["sin", "cos", "tan"])
plot = pn.bind(update, widget.param.value_throttled)
col = pn.Column(widget, plot)
[13]:
import panel as pn
pn.extension()
pn.pane.GIF("use_value_throttled_for_updates_on_mouse-up_and_expensive_computations.gif")
[13]:
Add a built-in loading indicator for expensive computations¶
Panel has a loading_indicator built-in for panel objects; great for keeping the app look functional!
[14]:
import time
import numpy as np
import panel as pn
import holoviews as hv
pn.extension()
pn.param.ParamMethod.loading_indicator = True
def update(func):
y = getattr(np, func)([0, 1, 2])
time.sleep(1)
return hv.Curve(([0, 1, 2], y))
widget = pn.widgets.DiscreteSlider(value="sin", options=["sin", "cos", "tan"])
plot = pn.bind(update, widget.param.value_throttled)
col = pn.Column(widget, plot)
[15]:
import panel as pn
pn.extension()
pn.pane.GIF("add_a_built-in_loading_indicator_for_expensive_computations.gif")
[15]:
Wrap DynamicMap around Panel methods to maintain extents and improve runtime¶
If you zoomed into the map and updated the slider’s value, the extent resets which makes it difficult to analyze it temporally.
[16]:
import panel as pn
import xarray as xr
import pandas as pd
import geoviews as gv
import cartopy.crs as ccrs
from random import randrange
pn.extension()
gv.extension("bokeh")
tiles = gv.tile_sources.EsriImagery()
ds = xr.tutorial.open_dataset("air_temperature")
slider = pn.widgets.DiscreteSlider(options=list(ds["time"].astype(str).values))
def plot(time):
df = ds.sel(time=time).to_dataframe()
df = df.sample(randrange(25))
points = gv.Points(df, ["lon", "lat"]).opts(
projection=ccrs.GOOGLE_MERCATOR, global_extent=True, color="white"
)
return tiles * points
points = pn.bind(plot, slider.param.value)
col = pn.Column(slider, points)
[17]:
import panel as pn
pn.extension()
pn.pane.GIF("wrap_dynamicmap_around_panel_methods_to_maintain_extents_and_improve_runtime_before.gif")
[17]:
If you wrap gv.DynamicMap
around points
, it no longer resets / flickers and it’s slightly more optimized because it doesn’t have to redraw the entire frame!
[18]:
import panel as pn
import xarray as xr
import pandas as pd
import geoviews as gv
import cartopy.crs as ccrs
from random import randrange
pn.extension()
gv.extension("bokeh")
tiles = gv.tile_sources.EsriImagery()
ds = xr.tutorial.open_dataset("air_temperature")
slider = pn.widgets.DiscreteSlider(options=list(ds["time"].astype(str).values))
def plot(time):
df = ds.sel(time=time).to_dataframe()
df = df.sample(randrange(25))
points = gv.Points(df, ["lon", "lat"]).opts(
projection=ccrs.GOOGLE_MERCATOR, global_extent=True, color="white"
)
return tiles * points
points = gv.DynamicMap(pn.bind(plot, slider.param.value))
col = pn.Column(slider, points)
[19]:
import panel as pn
pn.extension()
pn.pane.GIF("wrap_dynamicmap_around_panel_methods_to_maintain_extents_and_improve_runtime_after.gif")
[19]:
Remember to call value
from the param
namespace, e.g. param.value
or else it won’t be interactive!
However, the downside is that the built-in loading indicator doesn’t work with DynamicMaps.
One element per DynamicMap for flexibility¶
If you return an overlay, you lose flexibility. Here’s the original code that subsets the xr.Dataset
based on the x_range
and y_range
set.
[20]:
import cartopy.crs as ccrs
import holoviews as hv
import geoviews as gv
import numpy as np
import panel as pn
import xarray as xr
import time
gv.extension("bokeh")
pn.extension()
def update(x_range, y_range):
tile = gv.tile_sources.ESRI()
x1, x2 = x_range
y1, y2 = y_range
ds_sel = ds.sel(lat=slice(y2, y1), lon=slice(x1, x2), time="2013-01-01 00Z")
image = gv.Image(ds_sel, ["lon", "lat"], ["air"], crs=ccrs.PlateCarree()).opts(
projection=ccrs.GOOGLE_MERCATOR
)
time.sleep(0.5) # mimic expensive computation
corners = np.array([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])
polygon = gv.Polygons([{("Longitude", "Latitude"): corners}]).opts(
projection=ccrs.GOOGLE_MERCATOR, fill_alpha=0.5)
return tile * image * polygon
ds = xr.tutorial.open_dataset("air_temperature")
lon_slider = pn.widgets.RangeSlider(start=220, end=320)
lat_slider = pn.widgets.RangeSlider(start=16, end=74)
dmap = gv.DynamicMap(pn.bind(update, lon_slider, lat_slider))
dmap = dmap.opts(global_extent=True)
layout = pn.Row(pn.Column(lat_slider, lon_slider), dmap)
[21]:
import panel as pn
pn.extension()
pn.pane.GIF("one_element_per_dynamicmap_for_flexibility_before.gif")
[21]:
Here’s what I would do.
Since tile is “static” (or externally dynamic), I would move it out.
Create a separate update function for the
polygon
.Bind
value_throttled
toupdate_image
Now, users can adjust the bounds to their liking before the image gets subset and processed (which sometimes can be expensive)!
[22]:
import cartopy.crs as ccrs
import holoviews as hv
import geoviews as gv
import numpy as np
import panel as pn
import xarray as xr
import time
gv.extension("bokeh")
pn.extension()
def update_image(x_range, y_range):
ds_sel = ds.sel(
lat=slice(*y_range[::-1]), lon=slice(*x_range), time="2013-01-01 00Z"
)
image = gv.Image(ds_sel, ["lon", "lat"], ["air"], crs=ccrs.PlateCarree()).opts(
projection=ccrs.GOOGLE_MERCATOR
)
time.sleep(0.5) # mimic expensive computation
return image
def update_polygon(x_range, y_range):
x1, x2 = x_range
y1, y2 = y_range
corners = np.array([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])
polygon = gv.Polygons([{("Longitude", "Latitude"): corners}]).opts(
projection=ccrs.GOOGLE_MERCATOR, fill_alpha=0.5
)
return polygon
ds = xr.tutorial.open_dataset("air_temperature")
lon_slider = pn.widgets.RangeSlider(start=220, end=320)
lat_slider = pn.widgets.RangeSlider(start=16, end=74)
tile = gv.tile_sources.ESRI()
image_dmap = gv.DynamicMap(
pn.bind(
update_image, lon_slider.param.value_throttled, lat_slider.param.value_throttled
)
)
polygon_dmap = gv.DynamicMap(pn.bind(update_polygon, lon_slider, lat_slider))
overlay = (tile * image_dmap * polygon_dmap).opts(global_extent=True)
layout = pn.Row(pn.Column(lat_slider, lon_slider), overlay)
[23]:
import panel as pn
pn.extension()
pn.pane.GIF("one_element_per_dynamicmap_for_flexibility_after.gif")
[23]: