I want a Python Panel app with dynamic plots, and different plots depending on which menu/button is selected from a sidepanel, so I started from this great starting point: https://discourse.holoviz.org/t/multi-page-app-documentation/3108/2
Each page had a sine & cosine plot which updated based on widget values, using @pn.depends
.
Then I updated the code so that the sine plot would be a scatter plot, and the widget would be a selection drop-down menu instead of a slider. But now that scatter plot is not updating when I update the selection drop-down widget. What am I doing wrong? Clearly I’m misunderstanding something about what @pn.depends()
is doing or not doing. Any help would be super appreciated!
Full code (app.py
) below:
import pandas as pd
import panel as pn
import holoviews as hv
import hvplot.pandas
import numpy as np
import time
from datetime import datetime
pn.extension()
template = pn.template.FastListTemplate(title='My Dashboard')
# load detailed data
durations = np.random.randint(0, 10, size=10)
activity_codes = ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3', 'C3']
activity_categories = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C']
df = pd.DataFrame({'Duration': durations,
'ActivityCode': activity_codes,
'ActivityCategory': activity_categories})
# Page 1 Widget Controls
ac_categories = ['A', 'B', 'C']
ac_cat_radio_button = pn.widgets.Select(name='Activity Category', options=ac_categories)
# Page 1 Plotting Code
@pn.depends(ac_cat=ac_cat_radio_button)
def scatter_detail_by_ac(df, ac_cat):
print(f"ac_cat is {ac_cat}")
print(f"ac_cat.value is {ac_cat.value}")
df_subset = df.loc[df.ActivityCategory==ac_cat.value]
print(f"number of records in df_subset to scatter plot: {len(df_subset):,}")
return df_subset.hvplot.scatter(x='ActivityCode', y='Duration')
freq2 = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
phase2 = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)
@pn.depends(freq=freq2, phase=phase2)
def cosine(freq, phase):
xs = np.linspace(0,np.pi)
return hv.Curve((xs, np.cos(xs*freq+phase))).opts(
responsive=True, min_height=400)
page = pn.Column(sizing_mode='stretch_width')
content1 = [
pn.Row(ac_cat_radio_button), #grouping_vars_radio_button),
scatter_detail_by_ac(df, ac_cat_radio_button),
]
content2 = [
pn.Row(freq2, phase2),
hv.DynamicMap(cosine),
]
link1 = pn.widgets.Button(name='Scatter')
link2 = pn.widgets.Button(name='Cosine')
template.sidebar.append(link1)
template.sidebar.append(link2)
template.main.append(page)
def load_content1(event):
template.main[0].objects = content1
def load_content2(event):
template.main[0].objects = content2
link1.on_click(load_content1)
link2.on_click(load_content2)
template.show()
I serve this Panel app locally by running (from shell):
panel serve app.py --autoreload
Also, here's the library versions I'm using, from my requirements.in
(which I pip-compile
into a requirements.txt/lockfile):
panel==v1.0.0rc6
pandas==1.5.3
holoviews==1.16.0a2
hvplot
pandas-gbq>=0.19.1
The issue you encountered is that when you mark a function with pn.depends
then you need to pass it as is to Panel, which will take care of re-executing it and re-rendering its output anytime one of the listed widgets in the pn.depends
decorator is updated. Here's a simplified version of your code, fixing the issue you had:
import pandas as pd
import panel as pn
import hvplot.pandas
import numpy as np
pn.extension()
# load detailed data
durations = np.random.randint(0, 10, size=10)
activity_codes = ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3', 'C3']
activity_categories = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C']
df = pd.DataFrame({'Duration': durations,
'ActivityCode': activity_codes,
'ActivityCategory': activity_categories})
# Page 1 Widget Controls
ac_categories = ['A', 'B', 'C']
ac_cat_radio_button = pn.widgets.Select(name='Activity Category', options=ac_categories)
from functools import partial
# Page 1 Plotting Code
@pn.depends(ac_cat=ac_cat_radio_button)
def scatter_detail_by_ac(df=df, ac_cat=None):
df_subset = df.loc[df.ActivityCategory==ac_cat]
print(f"number of records in df_subset to scatter plot: {len(df_subset):,}")
return df_subset.hvplot.scatter(x='ActivityCode', y='Duration')
pn.Column(ac_cat_radio_button, scatter_detail_by_ac)
Generally Panel users are now recommended to use pn.bind
instead of pn.depends
when they want to add interactivity in their apps. Its behavior is similar to functools.partial
which makes it easier to approach, specially if you're already acquainted with functools.partial
.
Now when it comes more specifically to data apps, such as yours, you can also leverage hvplot.interactive
which is an API with which you can replace pipeline values by Panel widgets. I re-wrote your cool app using this API:
import pandas as pd
import panel as pn
import hvplot.pandas
import numpy as np
pn.extension(sizing_mode='stretch_width')
# load detailed data
durations = np.random.randint(0, 10, size=10)
activity_codes = ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3', 'C3']
activity_categories = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C']
df = pd.DataFrame({'Duration': durations,
'ActivityCode': activity_codes,
'ActivityCategory': activity_categories})
# App 1
w_ac_cat = pn.widgets.Select(name='Activity Category', options=['A', 'B', 'C'])
dfi = df.interactive()
dfi = dfi.loc[dfi.ActivityCategory == w_ac_cat]
app1 = dfi.hvplot.scatter(x='ActivityCode', y='Duration', responsive=True, min_height=400)
# App 2
def cosine(freq, phase):
xs = np.linspace(0, np.pi)
return pd.DataFrame(dict(y=np.cos(xs*freq+phase)), index=xs)
w_freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
w_phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)
dfi_cosine = hvplot.bind(cosine, w_freq, w_phase).interactive()
app2 = pn.Column(
pn.Row(*dfi_cosine.widgets()),
dfi_cosine.hvplot(responsive=True, min_height=400).output()
)
# Template
page = pn.Column(sizing_mode='stretch_width')
link1 = pn.widgets.Button(name='Scatter')
link2 = pn.widgets.Button(name='Cosine')
template = pn.template.FastListTemplate(
title='My Dashboard', main=[page], sidebar=[link1, link2]
)
def load_content1(event):
template.main[0][:] = [app1]
def load_content2(event):
template.main[0][:] = [app2]
link1.on_click(load_content1)
link2.on_click(load_content2)
template.show()