diff --git a/ax/core/analysis_card.py b/ax/core/analysis_card.py
index 1c76068cfe9..497c7ace31b 100644
--- a/ax/core/analysis_card.py
+++ b/ax/core/analysis_card.py
@@ -19,7 +19,10 @@
from plotly.offline import get_plotlyjs
# Simple HTML template for rendering a card with a title, subtitle, and body with
-# scrollable overflow.
+# scrollable overflow. Subtitles are rendered in a
with max-height overflow
+# so that any HTML content (tables, lists, etc.) is valid and clipped correctly
+# in the collapsed state. A "See more"/"See less" toggle appears when content
+# overflows (matching the web UI pattern).
html_card_template = """
+
"""
# Simple HTML template for rendering a *group* card with a title, subtitle, and
@@ -120,6 +155,7 @@ class AnalysisCardBase(SortableBase, ABC):
title: str
subtitle: str
+ subtitle_toggle_label: str
_timestamp: datetime
@@ -129,6 +165,7 @@ def __init__(
title: str,
subtitle: str,
timestamp: datetime | None = None,
+ subtitle_toggle_label: str = "",
) -> None:
"""
Args:
@@ -141,10 +178,14 @@ def __init__(
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
+ subtitle_toggle_label: Custom label for the subtitle expansion
+ toggle. When non-empty, replaces the default "See more" text. Persisted to
+ storage and used by both notebook and web UI rendering.
"""
self.name = name
self.title = title
self.subtitle = subtitle
+ self.subtitle_toggle_label = subtitle_toggle_label
self._timestamp = timestamp if timestamp is not None else datetime.now()
@abstractmethod
@@ -224,10 +265,12 @@ def _repr_html_(self) -> str:
return plotlyjs_script + self._to_html(depth=0)
def _to_html(self, depth: int) -> str:
+ toggle_text = self.subtitle_toggle_label or "See more"
return html_card_template.format(
title_str=self.title,
subtitle_str=self.subtitle,
body_html=self._body_html(depth=depth),
+ toggle_text=toggle_text,
)
@@ -248,6 +291,7 @@ def __init__(
subtitle: str | None,
children: Sequence[AnalysisCardBase],
timestamp: datetime | None = None,
+ subtitle_toggle_label: str = "",
) -> None:
"""
Args:
@@ -260,12 +304,15 @@ def __init__(
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
+ subtitle_toggle_label: Custom label for the subtitle expansion
+ toggle. When non-empty, replaces the default "See more" text
"""
super().__init__(
name=name,
title=title,
subtitle=subtitle if subtitle is not None else "",
timestamp=timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
self.children = [
@@ -370,6 +417,7 @@ def __init__(
df: pd.DataFrame,
blob: str,
timestamp: datetime | None = None,
+ subtitle_toggle_label: str = "",
) -> None:
"""
Args:
@@ -383,12 +431,15 @@ def __init__(
timestamp: The time at which the Analysis was computed. This can be
especially useful when querying the database for the most recently
produced artifacts.
+ subtitle_toggle_label: Custom label for the subtitle expansion
+ toggle. When non-empty, replaces the default "See more" text
"""
super().__init__(
name=name,
title=title,
subtitle=subtitle,
timestamp=timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
self.df = df
diff --git a/ax/core/tests/test_analysis_card.py b/ax/core/tests/test_analysis_card.py
index a092814e157..aee09ebea57 100644
--- a/ax/core/tests/test_analysis_card.py
+++ b/ax/core/tests/test_analysis_card.py
@@ -78,3 +78,25 @@ def test_not_applicable_card(self) -> None:
blob="Explanation text.",
)
self.assertIn("Explanation text.", card._body_html(depth=0))
+
+ def test_subtitle_toggle_label_rendering(self) -> None:
+ """Verify subtitle_toggle_label controls toggle button text in HTML."""
+ for label, expected_text in (
+ ("", "See more"),
+ (
+ "Expand to see annotated parameters.",
+ "Expand to see annotated parameters.",
+ ),
+ ):
+ with self.subTest():
+ card = AnalysisCard(
+ name="Test",
+ title="Title",
+ subtitle="A long subtitle",
+ df=pd.DataFrame(),
+ blob="blob",
+ subtitle_toggle_label=label,
+ )
+ self.assertEqual(card.subtitle_toggle_label, label)
+ html = card._repr_html_()
+ self.assertIn(expected_text, html)
diff --git a/ax/storage/json_store/encoders.py b/ax/storage/json_store/encoders.py
index d6f960ffdcc..d2f61e3192a 100644
--- a/ax/storage/json_store/encoders.py
+++ b/ax/storage/json_store/encoders.py
@@ -89,6 +89,7 @@ def analysis_card_to_dict(card: AnalysisCard) -> dict[str, Any]:
"df": card.df,
"blob": card.blob,
"timestamp": card._timestamp,
+ "subtitle_toggle_label": card.subtitle_toggle_label,
}
@@ -101,6 +102,7 @@ def analysis_card_group_to_dict(group: AnalysisCardGroup) -> dict[str, Any]:
"subtitle": group.subtitle,
"children": group.children,
"timestamp": group._timestamp,
+ "subtitle_toggle_label": group.subtitle_toggle_label,
}
diff --git a/ax/storage/json_store/tests/test_json_store.py b/ax/storage/json_store/tests/test_json_store.py
index 783bfce31d9..f73ab81256b 100644
--- a/ax/storage/json_store/tests/test_json_store.py
+++ b/ax/storage/json_store/tests/test_json_store.py
@@ -421,6 +421,7 @@
subtitle="subtitle",
df=pd.DataFrame({"a": [1, 2]}),
blob="blob_str",
+ subtitle_toggle_label="Expand to see details.",
),
),
(
@@ -495,6 +496,7 @@
blob="# md",
),
],
+ subtitle_toggle_label="Expand to see children.",
),
),
]
diff --git a/ax/storage/sqa_store/decoder.py b/ax/storage/sqa_store/decoder.py
index 9fb27215f44..c2426bffc10 100644
--- a/ax/storage/sqa_store/decoder.py
+++ b/ax/storage/sqa_store/decoder.py
@@ -1163,6 +1163,7 @@ def analysis_card_from_sqa(
analysis_card_sqa.title if analysis_card_sqa.title is not None else ""
)
subtitle = analysis_card_sqa.subtitle
+ subtitle_toggle_label = analysis_card_sqa.subtitle_toggle_label or ""
return AnalysisCardGroup(
name=analysis_card_sqa.name,
@@ -1170,12 +1171,14 @@ def analysis_card_from_sqa(
subtitle=subtitle,
children=children,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
title = none_throws(analysis_card_sqa.title)
subtitle = none_throws(analysis_card_sqa.subtitle)
blob = none_throws(analysis_card_sqa.blob)
blob_annotation = analysis_card_sqa.blob_annotation
+ subtitle_toggle_label = analysis_card_sqa.subtitle_toggle_label or ""
if blob_annotation == "not_applicable_state":
return NotApplicableStateAnalysisCard(
@@ -1185,6 +1188,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
if blob_annotation == "error":
return ErrorAnalysisCard(
@@ -1194,6 +1198,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
if blob_annotation == "plotly":
return PlotlyAnalysisCard(
@@ -1203,6 +1208,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
if blob_annotation == "markdown":
return MarkdownAnalysisCard(
@@ -1212,6 +1218,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
if blob_annotation == "healthcheck":
return HealthcheckAnalysisCard(
@@ -1221,6 +1228,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
if blob_annotation == "graphviz":
return GraphvizAnalysisCard(
@@ -1230,6 +1238,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
return AnalysisCard(
name=analysis_card_sqa.name,
@@ -1238,6 +1247,7 @@ def analysis_card_from_sqa(
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
blob=blob,
timestamp=analysis_card_sqa.timestamp,
+ subtitle_toggle_label=subtitle_toggle_label,
)
def _metric_from_sqa_util(self, metric_sqa: SQAMetric) -> Metric:
diff --git a/ax/storage/sqa_store/encoder.py b/ax/storage/sqa_store/encoder.py
index 63ab30eaa41..9bd980e426f 100644
--- a/ax/storage/sqa_store/encoder.py
+++ b/ax/storage/sqa_store/encoder.py
@@ -1264,6 +1264,7 @@ def analysis_card_to_sqa(
dataframe_json=None,
blob=None,
blob_annotation=None,
+ subtitle_toggle_label=analysis_card.subtitle_toggle_label or None,
)
for i, child_card in enumerate(analysis_card.children):
@@ -1304,4 +1305,5 @@ def analysis_card_to_sqa(
dataframe_json=card.df.to_json(),
blob=card.blob,
blob_annotation=blob_annotation,
+ subtitle_toggle_label=card.subtitle_toggle_label or None,
)
diff --git a/ax/storage/sqa_store/sqa_classes.py b/ax/storage/sqa_store/sqa_classes.py
index ee656e8483a..5547030d950 100644
--- a/ax/storage/sqa_store/sqa_classes.py
+++ b/ax/storage/sqa_store/sqa_classes.py
@@ -455,6 +455,9 @@ class SQAAnalysisCard(Base):
blob_annotation: Column[str | None] = Column(
String(NAME_OR_TYPE_FIELD_LENGTH), nullable=True
)
+ subtitle_toggle_label: Column[str | None] = Column(
+ "subtitle_toggle_label", String(LONG_STRING_FIELD_LENGTH), nullable=True
+ )
parent: Any = relationship(
"SQAAnalysisCard",
back_populates="children",
diff --git a/ax/storage/sqa_store/tests/test_sqa_store.py b/ax/storage/sqa_store/tests/test_sqa_store.py
index 07de5407c20..8e1f7eb7dda 100644
--- a/ax/storage/sqa_store/tests/test_sqa_store.py
+++ b/ax/storage/sqa_store/tests/test_sqa_store.py
@@ -3015,6 +3015,7 @@ def test_analysis_card(self) -> None:
subtitle="test_subtitle",
df=test_df,
blob="test blob",
+ subtitle_toggle_label="Expand to see details.",
)
markdown_analysis_card = MarkdownAnalysisCard(
name="test_markdown_analysis_card",
@@ -3038,6 +3039,7 @@ def test_analysis_card(self) -> None:
title="Small Group",
subtitle="This is a small group with just a few cards",
children=[base_analysis_card, markdown_analysis_card, plotly_analysis_card],
+ subtitle_toggle_label="Expand to see group children.",
)
big_group = AnalysisCardGroup(
name="big_group",
@@ -3067,12 +3069,14 @@ def test_analysis_card(self) -> None:
self.assertEqual(loaded_big_group.name, big_group.name)
self.assertEqual(loaded_big_group.title, big_group.title)
self.assertEqual(loaded_big_group.subtitle, big_group.subtitle)
+ self.assertEqual(loaded_big_group.subtitle_toggle_label, "")
loaded_big_group_plotly = assert_is_instance(
loaded_big_group.children[0], PlotlyAnalysisCard
)
self.assertEqual(loaded_big_group_plotly.name, plotly_analysis_card.name)
self.assertEqual(loaded_big_group_plotly.blob, plotly_analysis_card.blob)
+ self.assertEqual(loaded_big_group_plotly.subtitle_toggle_label, "")
loaded_small_group = assert_is_instance(
loaded_big_group.children[1], AnalysisCardGroup
@@ -3080,24 +3084,33 @@ def test_analysis_card(self) -> None:
self.assertEqual(loaded_small_group.name, small_group.name)
self.assertEqual(loaded_small_group.title, small_group.title)
self.assertEqual(loaded_small_group.subtitle, small_group.subtitle)
+ self.assertEqual(
+ loaded_small_group.subtitle_toggle_label,
+ "Expand to see group children.",
+ )
loaded_base = assert_is_instance(
loaded_small_group.children[0], AnalysisCard
)
self.assertEqual(loaded_base.name, base_analysis_card.name)
self.assertEqual(loaded_base.blob, base_analysis_card.blob)
+ self.assertEqual(
+ loaded_base.subtitle_toggle_label, "Expand to see details."
+ )
loaded_markdown = assert_is_instance(
loaded_small_group.children[1], MarkdownAnalysisCard
)
self.assertEqual(loaded_markdown.name, markdown_analysis_card.name)
self.assertEqual(loaded_markdown.blob, markdown_analysis_card.blob)
+ self.assertEqual(loaded_markdown.subtitle_toggle_label, "")
loaded_small_group_plotly = assert_is_instance(
loaded_small_group.children[2], PlotlyAnalysisCard
)
self.assertEqual(loaded_small_group_plotly.name, plotly_analysis_card.name)
self.assertEqual(loaded_small_group_plotly.blob, plotly_analysis_card.blob)
+ self.assertEqual(loaded_small_group_plotly.subtitle_toggle_label, "")
def test_delete_generation_strategy(self) -> None:
# GIVEN an experiment with a generation strategy