-
-
Notifications
You must be signed in to change notification settings - Fork 575
Description
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
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
Labels
No labels

