Skip to content

Card callback not called when collapsed by default #8198

@singharpit94

Description

@singharpit94

I am using panel 1.7.5, bokeh 3.7.3 and ipywidgets-bokeh 1.6.0 and notice a weird issue with cards collapsed by default when using with ipywidgets.

When I have the card collapsed by default, the widgets are not updated correctly see gif 1 but when I expand the card explicitly after the application load, widgets are updated as expected see gif 2.

GIF1

Image

GIF2

Image

Related code for reproducing the issue

import pandas as pd
import panel as pn
import param
import ipywidgets as widgets

# Configuration
TITLE = "Simple Panel Dashboard"

pn.extension()

# Sample data
data = pd.DataFrame({
    "Name": ["Alice", "Bob", "Cathy"],
    "City": ["NY", "LA", "NY"]
})

# Column names
NAME_COL = "Name"
CITY_COL = "City"
class TestDash(param.Parameterized):

    city = param.ObjectSelector()

    def __init__(self, df):
        super().__init__()
        self.df = df
        
        # Initialize cards as None - will be created in build_widgets
        self.card_1 = None
        self.card_2 = None
        
        # Status indicator for card collapsed state
        self.card_status_indicator = None

        self.build_widgets()


    def build_widgets(self):
        """Builds all panel widgets."""
        cities = ["All"] + list(self.df[CITY_COL].unique())
        self.filter_options_dict = {CITY_COL: cities}

        self.city_filter = pn.widgets.Select.from_param(
            self.param.city, name="City", options=cities, value="All",
            sizing_mode="stretch_width"
        )

        self.selectors_dict = {CITY_COL: self.city_filter}
        self.selectors_dependents_dict = {CITY_COL: []}
        
    #_________________________________________________________________________#
    #SECTION - Data Filtering
    def filter_data(self, filter_on_dict):

        df = self.df.copy()
        for col, widget in filter_on_dict.items():
            if widget.value != "All":
                # only filter if widget is not set to "everything"
                df = df[df[col] == widget.value]

        return df


    def filter_display_data(self) -> pd.DataFrame:
        # Filter using all selectors in the dashboard
        df = self.filter_data(self.selectors_dict)

        # Save the result as an instance variable for use elsewhere
        self.filtered_df = df.reset_index(drop=True).copy()

        return df

    #!SECTION - END Data Filtering
    # Object Builders

    def contain_objects(self):

        self.build_widgets_content()
        
        # Create cards if they don't exist yet
        if self.card_1 is None:
            self.card_1 = pn.Card(
                self.widget_card_1,
                styles={"background": "WhiteSmoke"},
                width=400,
                title="Card 1 - Selector Values",
                collapsed=True
            )
            # Watch the collapsed property of the first card
            self.card_1.param.watch(self._on_card_collapsed, ['collapsed'])
        else:
            # Update the content of existing card
            self.card_1.objects = [self.widget_card_1]
            
        if self.card_2 is None:
            self.card_2 = pn.Card(
                self.widget_card_2,
                styles={"background": "LightBlue"},
                width=400,
                title="Card 2 - Filtered Data",
                collapsed=False
            )
        else:
            # Update the content of existing card
            self.card_2.objects = [self.widget_card_2]
            
        # Create status indicator if it doesn't exist
        if self.card_status_indicator is None:
            self.card_status_indicator = pn.pane.HTML(
                self._get_card_status_html(),
                styles={"background": "#f0f0f0", "padding": "10px", "border-radius": "5px"}
            )

        return [
            # Row with city filter
            pn.Row(
                pn.pane.HTML("<h3>Filter:</h3>"),
                self.city_filter,
                sizing_mode="stretch_width"
            ),
            # Status indicator showing card state
            pn.Row(
                pn.pane.HTML("<h4>Card Status:</h4>"),
                self.card_status_indicator,
                sizing_mode="stretch_width"
            ),
            # Row for cards
            pn.Row(
                self.card_1,
                self.card_2,
                sizing_mode="stretch_width"
            )
        ]

    def build_widgets_content(self):
        """Build ipywidgets content for the cards."""
        
        # Card 1: Display selector values
        selector_values = {}
        for key, widget in self.selectors_dict.items():
            selector_values[key] = widget.value
        
        display_text = "<h4>Current Filter Settings:</h4><ul>"
        for key, value in selector_values.items():
            display_text += f"<li><strong>{key}:</strong> {value}</li>"
        display_text += "</ul>"
        
        self.widget_card_1 = widgets.HTML(value=display_text)
        
        data_text = ''
        
        if hasattr(self, 'filtered_df') and not self.filtered_df.empty:

            data_text += self.filtered_df.to_html(index=False, classes='table table-striped')
        
        self.widget_card_2 = widgets.HTML(value=data_text)

    def _get_card_status_html(self):
        """Generate HTML for the card status indicator."""
        if self.card_1 is None:
            status = "Card not initialized"
            color = "#999"
            icon = "⚪"
        elif self.card_1.collapsed:
            status = "Card 1 is COLLAPSED 📦"
            color = "#ff6b6b"
            icon = "🔒"
        else:
            status = "Card 1 is EXPANDED 📂"
            color = "#51cf66"
            icon = "🔓"
            
        return f"""
        <div style="display: flex; align-items: center; gap: 10px;">
            <span style="font-size: 20px;">{icon}</span>
            <span style="color: {color}; font-weight: bold; font-size: 16px;">{status}</span>
        </div>
        """
    
    def _on_card_collapsed(self, event):
        """Callback function that gets triggered when card collapsed state changes."""
        print(f"Card collapsed state changed! New value: {event.new}")
        
        # Update the status indicator
        if self.card_status_indicator is not None:
            self.card_status_indicator.object = self._get_card_status_html()
        
        # You can add any other logic here based on the collapsed state
        if event.new:  # Card is now collapsed
            print("Card 1 was collapsed - you can trigger any action here!")
            # Example: Change card 2 background when card 1 is collapsed
            if self.card_2 is not None:
                self.card_2.styles = {"background": "#ffcccc"}
        else:  # Card is now expanded
            print("Card 1 was expanded - you can trigger any action here!")
            # Example: Restore card 2 background when card 1 is expanded
            if self.card_2 is not None:
                self.card_2.styles = {"background": "LightBlue"}

    # Callbacks
    @param.depends(
        "city",
        watch = True
    )
    def update_dashboard(self, *events):

        if not self.dashboard.objects and not events:
            #NOTE - This needs to stay, and stops the flickering of objects when
            #       widgets were cleared.
            return

        self.dashboard.loading=True

        # Update Widgets before filtering data
        self.filter_display_data()

        self.dashboard.clear()

        self.dashboard.extend(self.contain_objects())

        self._updating=False
        self.dashboard.loading=False


    #SECTION - Building Containers
    #ANCHOR - Main Container
    def __panel__(self,):

        if hasattr(self,'dashboard'):
            return self.dashboard

        self.dashboard = pn.Column(
            pn.pane.HTML(f"<h1>{TITLE}</h1>"),
            sizing_mode="stretch_width"
        )

        self.filter_display_data()

        objects = self.contain_objects()
        self.dashboard.extend(objects)

        return self.dashboard

# Create and serve the app
def create_app():
    """Create the simple Panel application."""
    dashboard = TestDash(data)
    return dashboard.__panel__()

# Run the app
if __name__.startswith("bokeh"):
    create_app().servable()
elif __name__ == "__main__":
    # For local development
    app = create_app()
    app.show(port=5007
)

Can you please suggest a fix here or fix in upstream if needed?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions