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.
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
)
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
GIF2
Related code for reproducing the issue
Can you please suggest a fix here or fix in upstream if needed?