Card callback not called when collapsed by default
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?
This also seems to recreate it. By the looks of it, the document first becomes idle when opening the card, blocking everything else.
import panel as pn
import ipywidgets
pn.extension()
current_city = "All"
def create_widget_content():
value=f"<strong>Current City:</strong> {current_city}"
return ipywidgets.HTML(value=value)
def update_card(event):
global current_city
current_city = event.new
card.objects = [create_widget_content()]
city_select = pn.widgets.Select(
name="City Filter",
value="All",
options=["All", "NY", "LA"]
)
city_select.param.watch(update_card, 'value')
card = pn.Card(create_widget_content(), title="card", collapsed=True)
pn.Column(city_select, card).servable()
Even simpler reproducer:
import panel as pn
import ipywidgets
pn.extension()
current_city = "All"
def create_widget_content():
value=f"<strong>Current City:</strong> {current_city}"
return ipywidgets.HTML(value=value)
def update_card(event):
card.objects = [event.new]
city_select = pn.widgets.Select(
name="City Filter",
value="All",
options=["All", "NY", "LA"]
)
city_select.param.watch(update_card, 'value')
card = pn.Card(create_widget_content(), title="card", collapsed=True)
pn.Column(city_select, card).servable()
Hi there, gave this a check and seems to me that the main reason this happens is since the card contents doesn't get rendered if the card is in a collapsed state when created due to the following line:
https://github.com/holoviz/panel/blob/8eb0b14f18eb239007a07a21a15c9471c409b460/panel/models/card.ts#L109-L111
My guess is that when the card is created with collapsed=True, since is not rendered and is not being marked as non-visible (child_view.mode.visible = false) unless you manually uncollapse the content, it will not get an initial render so you can end up with erros related to trying to update the unrendered card child element:
Uncaught Error: reference p1055 isn't known
Following that, seems like if around
https://github.com/holoviz/panel/blob/8eb0b14f18eb239007a07a21a15c9471c409b460/panel/models/card.ts#L109-L117
Things get change to something like:
for (const child_view of this.child_views.slice(1)) {
child_view.render()
child_view.r_after_render()
this.shadow_el.appendChild(child_view.el)
child_view.model.visible = !this.model.collapsed
}
The card contents is able to be properly updated:
- Using the latest code snippet to reproduce the issue:
- Using the original OP code example to reproduce the issue:
If the fix suggested above makes sense let me know to open a PR with it!
Edit: Here a branch with the proposed change in case someone wants to give the possible fix a check - https://github.com/dalthviz/panel/tree/issue-8198 - https://github.com/holoviz/panel/compare/main...dalthviz:issue-8198
@dalthviz Thanks for looking into it, if you could please open your branch as a PR and we can iterate from there.
Done @philippjfr ! Created PR #8274