Dynamic widget options from a pandas DataFrame

I am trying to reproduce this example (Mr. @Jhsmit 's version) using a pandas.DataFrame where I can not assume that each consecutive widget filters down the DataFrame. Suppose you have a DataFrame that has lat, lon, value_1 and value_2 as well as these columns:

Unique values for column ‘station_id’:
[‘NOAA15’ ‘METOP-3’ ‘METOP-1’ ‘NOAA19’ ‘NOAA18’]

Unique values for column ‘channel’:
[28 29 30 31 32 33 34 35 36 37 38 39 40 41 42]

Unique values for column ‘orbit_number’:
[30434 30433 23820 55663 73903 93066 93065 23821 73904 55664 30436 30435
23822 55665 73905 30437 93068]

Unique values for column ‘scan_line_number’:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
25 26 27 28 29 30]

Example can be created using

import pandas as pd
import numpy as np

# Define the unique values for each column
channels = np.arange(28, 43)
station_ids = ['NOAA15', 'METOP-3', 'METOP-1', 'NOAA19', 'NOAA18']
orbit_numbers = np.array([30434, 30433, 23820, 55663, 73903, 93066, 93065, 23821, 73904, 55664,
                          30436, 30435, 23822, 55665, 73905, 30437, 93068])
scan_line_numbers = np.arange(1, 31)

# Generate all combinations of the unique values
channel_combinations = np.repeat(channels, len(station_ids) * len(orbit_numbers) * len(scan_line_numbers))
station_id_combinations = np.tile(np.repeat(station_ids, len(orbit_numbers) * len(scan_line_numbers)), len(channels))
orbit_number_combinations = np.tile(np.repeat(orbit_numbers, len(scan_line_numbers)), len(channels) * len(station_ids))
scan_line_number_combinations = np.tile(scan_line_numbers, len(channels) * len(station_ids) * len(orbit_numbers))

# Generate random float values for value_1 and value_2
value_1 = np.random.rand(len(channel_combinations))
value_2 = np.random.rand(len(channel_combinations))

# Create the example dataframe
data = {
    'channel': channel_combinations,
    'station_id': station_id_combinations,
    'orbit_number': orbit_number_combinations,
    'scan_line_number': scan_line_number_combinations,
    'value_1': value_1,
    'value_2': value_2
}

df = pd.DataFrame(data)

And i have written the following class in an attempt to filter it down but it does not work correctly :

class ObservationVisualizer(param.Parameterized):
    def __init__(self, **params):
        self.plot_button = pnw.Button(name="Plot")
        self.ON_Values = []
        self.CHANNEL_Values = []
        self.hvplot_object = None
        self.SLN_Values = []
        self.df = merged_df.copy()
        self.Specie_Select = pnw.Select(
            name="Specie Select",
            options=[
                "brightness_temperature",
                "altitude",
                "satellite_zenith_angle",
                "solar_zenith_angle",
                "solar_azimuth_angle",
                "satellite_azimuth_angle",],
            value="brightness_temperature",
        )
        self.SID_Values = sorted(list(self.df.station_id.unique()))
        self.SID_Select = pnw.CheckButtonGroup(
            name="Station ID Select",
            options=self.SID_Values,
            value=[self.SID_Values[0]],
        )
        self.subset = self.df.loc[self.df.station_id == self.SID_Values[0]]
        self.cmap = pnw.Select(
            name="Select Color Map", options=CMAP_OPTIONS, value="jet"
        )
        self.alpha = pnw.FloatSlider(
            name="Opacity",
            start=0.0,
            end=1.0,
            step=0.1,
            value=0.8,
            value_throttled=True,
        )
        self.SID_Values = sorted(list(self.df.station_id.unique()))
        self.SID_Select = pnw.CheckButtonGroup(
            name="Station ID Select",
            options=self.SID_Values,
            value=[self.SID_Values[0]],
        )
        self.SLN_Values = sorted(list(self.subset.scan_line_number.unique()))
        self.SLN_Slider = pn.widgets.IntRangeSlider(
            name="Scan Line Numbers Range Slider",
            start=self.SLN_Values[0].item(),
            end=self.SLN_Values[-1].item(),
            step=1,
        )
        self.CHANNEL_Values = sorted(list(self.subset.orbit_number.unique()))
        self.CHANNEL_SELECT = pn.widgets.IntRangeSlider(
            name="Channel Range Slider",
            start=self.CHANNEL_Values[0].item(),
            end=self.CHANNEL_Values[-1].item(),
            step=1,
        )
        self.ON_Values = sorted(list(self.subset.orbit_number.unique()))
        self.ON_Select = pnw.MultiChoice(
            name="Orbit Number Select", options=self.ON_Values, value=self.ON_Values
        )
        super().__init__(**params)

    @param.depends("SID_Select.value", watch=True)
    def on_station_id_update(self):
        self.subset = self.df.loc[self.df["station_id"].isin(self.SID_Select.value)]
        self.SLN_Values = self.subset.scan_line_number.unique()
        self.SLN_Slider.start = int(self.SLN_Values.min())
        self.SLN_Slider.end = int(self.SLN_Values.max())
        self.SLN_Slider.value = (int(self.SLN_Values.min()), int(self.SLN_Values.max()))
        self.CHANNEL_Values = self.subset.channel.unique()
        self.CHANNEL_SELECT.start = int(self.CHANNEL_Values.min())
        self.CHANNEL_SELECT.end = int(self.CHANNEL_Values.max())
        self.CHANNEL_SELECT.value = (
            int(self.CHANNEL_Values.min()),
            int(self.CHANNEL_Values.max()),
        )
        self.ON_Values = sorted(list(self.subset.orbit_number.unique()))
        self.ON_Select.options = self.ON_Values
        self.ON_Select.value = self.ON_Values
        
    @param.depends("ON_Select.value", watch=True)
    def on_orbit_number_update(self):
        self.subset = self.df.loc[self.df["orbit_number"].isin(self.ON_Select.value)]
        self.SLN_Values = self.subset.scan_line_number.unique()
        self.SLN_Slider.start = int(self.SLN_Values.min())
        self.SLN_Slider.end = int(self.SLN_Values.max())
        self.SLN_Slider.value = (int(self.SLN_Values.min()), int(self.SLN_Values.max()))
        self.CHANNEL_Values = self.subset.channel.unique()
        self.CHANNEL_SELECT.start = int(self.CHANNEL_Values.min())
        self.CHANNEL_SELECT.end = int(self.CHANNEL_Values.max())
        self.CHANNEL_SELECT.value = (
            int(self.CHANNEL_Values.min()),
            int(self.CHANNEL_Values.max()),
        )

    @param.depends(
        "SID_Select.value",
        "SLN_Slider.value",
        "CHANNEL_SELECT.value",
        "ON_Select.value",
        "Specie_Select.value",
        watch=True,
    )
    def update_subset(self):
        try:
            print(["lon", "lat", self.Specie_Select.value])
            self.subset = self.df.loc[
                self.df["station_id"].isin(self.SID_Select.value)
                & self.df["orbit_number"].isin(self.ON_Select.value)
                & self.df["scan_line_number"].isin(
                    list(
                        range(
                            int(self.SLN_Slider.value[0]),
                            int(self.SLN_Slider.value[1]) + 1,
                        )
                    )
                )
                & self.df["channel"].isin(
                    list(
                        range(
                            int(self.CHANNEL_SELECT.value[0]),
                            int(self.CHANNEL_SELECT.value[1]) + 1,
                        )
                    )
                ),
                ["lon", "lat", self.Specie_Select.value],
            ]
            print(self.subset.shape)
        except:
            print('oups')
            pn.state.notifications.send(
                "Subset invalid!!!",
                background="red",
                icon='<i class="fas fa-burn"></i>',
                duration=4000,
            )

        pn.state.notifications.success("update_subset")

    @param.depends("plot_button.value", watch=True)
    def plot_button_on_click(self):
        self.hvplot_object = self.subset.hvplot.points(
            x="lon",
            y="lat",
            rasterize=True,
            dynspread=True,
            height=800,
            coastline=True,
            cmap="jet",
            title=f"{self.subset.shape[0]:,d} observations!!!",
        )
        return self.hvplot_object


app = ObservationVisualizer()

sidebar_components = Column(
    app.SID_Select,
    app.ON_Select,
    app.SLN_Slider,
    app.CHANNEL_SELECT,
    app.Specie_Select,
    app.plot_button,
    loading_indicator=True,
).servable(target="main")

main_components = Column(
    app.plot_button_on_click,
    loading_indicator=True,
).servable(target="main")

Row(sidebar_components, main_components)

I just want to understand how to use Param to filter the DataFrame and use the new unique values in each widget’s respective columns to populate them so that I can filter it down even further. I have seen the Continents and Countries example but I am not sure how to implement it without hardcoding some stuff. Thank you for your help everyone.

StuckDuckDF,

Not sure if you’ve figured it out in the meantime.
If I get it right, you want the filter-widgets to automatically adjust to show the value ranges of the filtered DF.

There are a couple of ‘hurdles’ to really make it work:

  • when you update the filter widgets with the current “range” of the filtered down values you need to avoid the param watchers to automatically trigger another filtering (but still allow panel to update the widget UI details)
  • how to ensure that you support filtering down in multiple iterations but also to allow to back-out some filtering-down

Here is Class that allows to filter a DF and displays the result in a tabulator widget.
The comments include some implementation details. the DF columns are hardcoded as parameters in the class, but you could also dynamically build and link them to the class via param.add_parameter(). The rest of the code is already pretty independent of the DF columns.

def DEBUG(message):
    if True:
        print(message)
    
class HardcodedDataframeFilter(param.Parameterized):
    
    df = param.DataFrame(default=None, doc='Source Dataframe')
    df_filtered = param.DataFrame(default=None, doc='Filtered Dataframe')
    
    reset_filters = param.Action(lambda self: self.reset_filtering())

    channel = param.Range(allow_None=True)
    station_id = param.ListSelector()
    orbit_number = param.Range()
    scan_line_number = param.Range(allow_None=True)
    value_1 = param.Range(allow_None=True)
    value_2 = param.Range(allow_None=True)

    _watcher_filters = param.Parameter(default=None, doc='Internal, Watcher reference')   
    ui = param.Parameter(default=None, precedence=-1, doc='Panel that holds the filters and table')
    
    @param.depends('df', on_init=True, watch=True)
    def reset_filtering(self):
        ''' Resets the DF filtering and the filters. Runs whenever DF changes and during __init__
        '''
        DEBUG('reset_filtering()')
        if not isinstance(self.df, pd.DataFrame):
            return
            
        self.df_filtered = self.df
        self.adjust_filters(init_value_range=True)
        
    def adjust_filters(self, init_value_range=False, *args, **kwargs):
        ''' Sets the values/value-ranges on the filters based on the *current*
            filtered DF values. This allows the Widget-values to autoadjust to show
            the current values of eg. column 'x' after filtering based on column 'y'. 
            Alternatively you may prefer to just keep the widgets as is, then a stripped down
            version of this method can be just merged into the 'reset_filtering()' method
            (and you can base it of self.df, not self.df_filtered)
        '''
        DEBUG(f'adjust_filters(init_value_range={init_value_range}, args={args}, kwargs={kwargs}')

        # We need to update the filter values but avoid that that is triggering the 'apply_filters'.
        # But at the same time we need to ensure that the filter-widgets UI stuff gets updated by panel.
        # - we can't use "with param.parameterized.discard_events(self), because that would 
        #   also block the widgets from being updated
        # - easiest is to just remove our method specific watcher and re-enable it afterwards again
        if self._watcher_filters is not None:
            self.param.unwatch(self._watcher_filters)

        # adjust filter values and, if requested the value-ranges as well. 
        # setting value_ranges is only expected to be done when setting/changing the DF. 
        # in case the DF gets changed we need to cover for the scenario where the currently
        # set range-of-values (and hence the value) has a wider range than the new incoming DF
        for filter_name, filter_obj in self.param.objects().items():

            if filter_name in self.df.columns:
                if isinstance(filter_obj, param.Range):
                    filtered_min_max = (self.df_filtered[filter_name].min(), self.df_filtered[filter_name].max())
                    if init_value_range:
                        orig_min_max = (self.df[filter_name].min(), self.df[filter_name].max())
                        filter_obj.bounds = filter_obj.softbounds = (None, None)   
                        setattr(self, filter_name, filtered_min_max)
                        filter_obj.bounds = filter_obj.softbounds = orig_min_max 
                    else:
                        setattr(self, filter_name, filtered_min_max)
                    
                elif isinstance(filter_obj, param.ListSelector):
                    filtered_available_entries = np.unique(self.df_filtered[filter_name]).tolist()
                    if init_value_range:
                        orig_available_entries = np.unique(self.df[filter_name]).tolist()
                        setattr(self, filter_name, [])
                        filter_obj.objects = orig_available_entries
                    setattr(self, filter_name, filtered_available_entries)
                    
                else:
                    print(f'TRAP: Unexpected filter_obj: {filter}')

        # re-enable watcher if UI is already up and running
        if self.ui is not None:
            self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=list(self.df.columns))
            
    def apply_filter(self, *args, **kwargs):
        ''' Reads and applies the filter. 
            Triggered by changes to the parameter value, typically through the linked widget. 
        '''
        DEBUG(f'apply_filter(args={args}, kwargs={kwargs})')
        
        # The USER has changed a FILTER. In order to support the different use-cases:
        # a) simply filter once with one filter
        # b) filter down multiple times with same/different filters
        # c) back-out some filterting down
        # ....
        # we either need to remember and redo  filter changes the User did 
        # or alternatively always just apply the user adjusted and automatically adjusted 
        # filters (more DF filtering effort, but less coding)
        df_newly_filtered = self.df

        for column in self.df.columns:
            param_obj = getattr(self.param, column, None)

            if isinstance(param_obj, param.Range):
                start, end = getattr(self, column)
                df_newly_filtered = df_newly_filtered[ (df_newly_filtered[column] >= start) 
                                                     & (df_newly_filtered[column] <= end)]
            elif isinstance(param_obj, param.ListSelector): 
                selected_values = getattr(self, column)
                df_newly_filtered = df_newly_filtered[df_newly_filtered[column].isin(selected_values)]

        self.df_filtered = df_newly_filtered
        print(f'df_filtered.shape={self.df_filtered.shape}')

        # adjust filter values to reflect the value-ranges on the filtered df-subset
        # may be optional
        self.adjust_filters()
 
        
    def view(self):
        ''' 
        '''
        DEBUG('view()')
        if self.df is None:
            # nothing to do yet (no DF and hence no filters set)
            return
        
        # create the UI container the same throught lifecycle, just clear content if
        # redoing everything
        if self.ui is None:
            self.ui = pn.Row()
        else:
            self.ui.clear()

        self.ui.extend([
            pn.Column(
                pn.widgets.Button.from_param(self.param.reset_filters), 
                pn.Param(self.param, parameters=list(self.df.columns))), 
            pn.widgets.Tabulator.from_param(
                self.param.df_filtered,
                header_filters=True, disabled=True, pagination='remote'),
            ])

        # activate Watcher to handle Filter Widget changes:
        self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=list(self.df.columns))
        
        return self.ui

hard = HardcodedDataframeFilter(df=df)
hard.view()
1 Like

@johann Thanks for posting this! Your solution has been a huge help on the project I just started working on. However, I’ve run into a roadblock: I’m trying to extend this by adding a declarative function for plotting the filtered data using Plotly. I can get the plots to show up with the original data but not react/update to the filtered dataframe. Could you take a look at this and point me in the right direction of the solution? Thanks in advance!

import panel as pn
import hvplot.pandas
import plotly_express as px
import pandas as pd
import numpy as np
import param

pn.extension(design='material')
pn.extension('plotly', 'tabulator')

csv_file = ("Generic - Mult Txns 20230927.csv")
data = pd.read_csv(csv_file, parse_dates=["Invoice_Date"])



# https://discourse.holoviz.org/t/dynamic-widget-options-from-a-pandas-dataframe/5538
def DEBUG(message):
    if True:
        print(message)
    
class HardcodedDataframeFilter(param.Parameterized):
    
    df = param.DataFrame(default=None, doc='Source Dataframe')
    df_filtered = param.DataFrame(default=None, doc='Filtered Dataframe')
    
    reset_filters = param.Action(lambda self: self.reset_filtering())
    
    event = param.Event()

    Vendor = param.ListSelector()
    Region = param.ListSelector()
    Location = param.ListSelector()
    Level1 = param.ListSelector()
    Level2 = param.ListSelector()
    #Level3 = param.ListSelector()             # WHEN I ADD IN LEVEL3 IT PRODUCES AN ERROR ABOUT COMPARING STR AND FLOAT VALUES
    Category = param.ListSelector()
    ItemCode = param.ListSelector()
    Vendor_ItemCode = param.ListSelector()
    ItemUnique = param.ListSelector()
    UniqueItemCode = param.ListSelector()
    flag_txn = param.ListSelector()

    _watcher_filters = param.Parameter(default=None, doc='Internal, Watcher reference')   
    ui = param.Parameter(default=None, precedence=-1, doc='Panel that holds the filters and table')
    
    
    # set up parameter watching tied to the plotting functions
    #def __init__(self, **params) :
    #    super().__init__(**params)
    #    self.param.watch_values(self.px_area_pi, parameter_names=['df_filtered'])
    #    self.param.watch_values(self.px_area_amt, parameter_names=['df_filtered'])
    #    self.param.watch_values(self.px_area_qty, parameter_names=['df_filtered'])
    #    self.param.watch_values(self.px_area_pos, parameter_names=['df_filtered'])
        
    
    
    @param.depends('df', on_init=True, watch=True)
    def reset_filtering(self):
        ''' Resets the DF filtering and the filters. Runs whenever DF changes and during __init__
        '''
        DEBUG('reset_filtering()')
        if not isinstance(self.df, pd.DataFrame):
            return
            
        self.df_filtered = self.df
        self.adjust_filters(init_value_range=True)
        
    def adjust_filters(self, init_value_range=False, *args, **kwargs):
        ''' Sets the values/value-ranges on the filters based on the *current*
            filtered DF values. This allows the Widget-values to autoadjust to show
            the current values of eg. column 'x' after filtering based on column 'y'. 
            Alternatively you may prefer to just keep the widgets as is, then a stripped down
            version of this method can be just merged into the 'reset_filtering()' method
            (and you can base it of self.df, not self.df_filtered)
        '''
        DEBUG(f'adjust_filters(init_value_range={init_value_range}, args={args}, kwargs={kwargs}')

        # We need to update the filter values but avoid that that is triggering the 'apply_filters'.
        # But at the same time we need to ensure that the filter-widgets UI stuff gets updated by panel.
        # - we can't use "with param.parameterized.discard_events(self), because that would 
        #   also block the widgets from being updated
        # - easiest is to just remove our method specific watcher and re-enable it afterwards again
        if self._watcher_filters is not None:
            self.param.unwatch(self._watcher_filters)

        # adjust filter values and, if requested the value-ranges as well. 
        # setting value_ranges is only expected to be done when setting/changing the DF. 
        # in case the DF gets changed we need to cover for the scenario where the currently
        # set range-of-values (and hence the value) has a wider range than the new incoming DF
        for filter_name, filter_obj in self.param.objects().items():

            if filter_name in self.df.columns:
                if isinstance(filter_obj, param.Range):
                    filtered_min_max = (self.df_filtered[filter_name].min(), self.df_filtered[filter_name].max())
                    if init_value_range:
                        orig_min_max = (self.df[filter_name].min(), self.df[filter_name].max())
                        filter_obj.bounds = filter_obj.softbounds = (None, None)   
                        setattr(self, filter_name, filtered_min_max)
                        filter_obj.bounds = filter_obj.softbounds = orig_min_max 
                    else:
                        setattr(self, filter_name, filtered_min_max)
                    
                elif isinstance(filter_obj, param.ListSelector):
                    filtered_available_entries = np.unique(self.df_filtered[filter_name]).tolist()
                    if init_value_range:
                        orig_available_entries = np.unique(self.df[filter_name]).tolist()
                        setattr(self, filter_name, [])
                        filter_obj.objects = orig_available_entries
                    setattr(self, filter_name, filtered_available_entries)
                    
                else:
                    print(f'TRAP: Unexpected filter_obj: {filter}')

        # re-enable watcher if UI is already up and running
        if self.ui is not None:
            #self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=list(self.df.columns))
            self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=['Vendor', 
                                                                                                'Region',
                                                                                                'Location',
                                                                                                'Level1',
                                                                                                'Level2',
                                                                                                'Category',
                                                                                                'ItemCode',
                                                                                                'Vendor_ItemCode',
                                                                                                'ItemUnique',
                                                                                                'UniqueItemCode',
                                                                                                'flag_txn'])
            
    def apply_filter(self, *args, **kwargs):
        ''' Reads and applies the filter. 
            Triggered by changes to the parameter value, typically through the linked widget. 
        '''
        DEBUG(f'apply_filter(args={args}, kwargs={kwargs})')
        
        # The USER has changed a FILTER. In order to support the different use-cases:
        # a) simply filter once with one filter
        # b) filter down multiple times with same/different filters
        # c) back-out some filterting down
        # ....
        # we either need to remember and redo  filter changes the User did 
        # or alternatively always just apply the user adjusted and automatically adjusted 
        # filters (more DF filtering effort, but less coding)
        df_newly_filtered = self.df

        for column in self.df.columns:
            param_obj = getattr(self.param, column, None)

            if isinstance(param_obj, param.Range):
                start, end = getattr(self, column)
                df_newly_filtered = df_newly_filtered[ (df_newly_filtered[column] >= start) 
                                                     & (df_newly_filtered[column] <= end)]
            elif isinstance(param_obj, param.ListSelector): 
                selected_values = getattr(self, column)
                df_newly_filtered = df_newly_filtered[df_newly_filtered[column].isin(selected_values)]

        self.df_filtered = df_newly_filtered
        print(f'df_filtered.shape={self.df_filtered.shape}')

        # adjust filter values to reflect the value-ranges on the filtered df-subset
        # may be optional
        self.adjust_filters()
        
        return self.df_filtered
    
    
    
        
        
    def view(self):
        ''' 
        '''
        DEBUG('view()')
        if self.df is None:
            # nothing to do yet (no DF and hence no filters set)
            return
        
        # create the UI container the same throught lifecycle, just clear content if
        # redoing everything
        if self.ui is None:
            self.ui = pn.Row()
        else:
            self.ui.clear()

        self.ui.extend([
            pn.Column(
                pn.widgets.Button.from_param(self.param.reset_filters), 
                pn.Param(self.param, parameters=list(self.df.columns))),
            pn.widgets.Tabulator.from_param(
                self.param.df_filtered,
                header_filters=True, disabled=True, pagination='remote'),
            ])

        # activate Watcher to handle Filter Widget changes:
        #self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=list(self.df.columns))
        self._watcher_filters = self.param.watch_values(self.apply_filter, parameter_names=['Vendor', 
                                                                                            'Region',
                                                                                            'Location',
                                                                                            'Level1',
                                                                                            'Level2',
                                                                                            'Category',
                                                                                            'ItemCode',
                                                                                            'Vendor_ItemCode',
                                                                                            'ItemUnique',
                                                                                            'UniqueItemCode',
                                                                                            'flag_txn'])
        
        return self.ui
    
    
    
    #@param.depends(param.df_filtered, watch = True, on_init = True)
    #@param.depends(_watcher_filters, on_init=True, watch=True)
    def px_area_pi(self) :
        title = 'sku_index_val'
        fig = px.area(self.df_filtered, x = 'Invoice_Date', y = 'sku_index_val')
        fig.update_layout(
            title_text = title,
            template = 'plotly_dark',
            autosize = True
            )
        fig_responsive = pn.pane.Plotly(fig, config = {'responsive' : True})
        return fig_responsive
    
    #@param.depends(_watcher_filters.param.value, on_init=True, watch=True)
    def px_area_amt(self) :
        title = 'order_amt'
        fig = px.area(self.df_filtered, x = 'Invoice_Date', y = 'order_amt')
        fig.update_layout(
            title_text = title,
            template = 'plotly_dark',
            autosize = True
            )
        fig_responsive = pn.pane.Plotly(fig, config = {'responsive' : True})
        return fig_responsive
    
    #@param.depends(_watcher_filters.param.value, on_init=True, watch=True)
    def px_area_qty(self) :
        title = 'order_qty'
        fig = px.area(self.df_filtered, x = 'Invoice_Date', y = 'order_qty')
        fig.update_layout(
            title_text = title,
            template = 'plotly_dark',
            autosize = True
            )
        fig_responsive = pn.pane.Plotly(fig, config = {'responsive' : True})
        return fig_responsive
    
    #@param.depends(_watcher_filters.param.value, on_init=True, watch=True)
    def px_area_pos(self) :
        title = 'POconcat'
        fig = px.area(self.df_filtered, x = 'Invoice_Date', y = 'POconcat')
        fig.update_layout(
            title_text = title,
            template = 'plotly_dark',
            autosize = True
            )
        fig_responsive = pn.pane.Plotly(fig, config = {'responsive' : True})
        return fig_responsive
    
    
    #@param.depends(df_filtered, watch = True)
    #def hvplot_view_area(self, plot_col) :
    #    df = self.df_filtered
    #    return df.hvplot.area(x='Invoice_Date', y = plot_col, responsive = True)
    
    
    
    
    
    
    

hard = HardcodedDataframeFilter(df=data)

# Define dashboard layout & content
dashboard_title = 'Spend Dashboard'

# create template based on FastGridTemplate -- allows end user to move and resize plots 
ACCENT = "#BB2649"
RED = "#D94467"
GREEN = "#5AD534"
       
template = pn.template.FastGridTemplate(
    title = dashboard_title,
    accent_base_color=ACCENT,
    header_background=ACCENT,
    prevent_collision=True,
    save_layout=True,
    theme_toggle=False,
    #theme='dark',
    row_height=160
)


# Add plots to template
template.main[0:3, 0:12] = hard.view()
template.main[3:6, 0:12] = hard.px_area_pi()
template.main[6:9, 0:6] = hard.px_area_amt()
#template.main[6:9, 6:12] = hard.px_area_qty()

# The template does not display in a notebook so we only output it when in a server context.
if pn.state.served :
    template.servable()

Hi @cjagoe

Welcome to the community.

Could you try positing a reproducible example by including the data or loading it from a url in the code? A minimum example would also make it easier to try to help.

Thanks.