Search code examples
pythonpandashvplotholovizholoviz-panel

Scatter Plot Not Updating With Widget Selection in Python Panel Holoviz


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

Solution

  • 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()
    

    enter image description here