Skip to content

Commit 69bade3

Browse files
shrutipatel31facebook-github-bot
authored andcommitted
Add toggle_label field to AnalysisCardBase for custom subtitle expansion text (facebook#5124)
Summary: Adds a serializable `toggle_label: str` field to `AnalysisCardBase` that allows analyses to customize the subtitle expand/collapse toggle button text. When non-empty, `toggle_label` replaces the default "See more" text with a context-specific label (e.g., "Expand to see annotated parameters."). The field is persisted to both SQA and JSON storage backends, and used by the notebook HTML template for rendering. Changes: - Add `toggle_label` field + constructor param (default "") to AnalysisCardBase - Update notebook `_to_html()` to use `self.toggle_label or "See more"` - Add `toggle_label` nullable column to SQAAnalysisCard - Update SQA encoder/decoder (all 8 card-type callsites) - Update JSON encoder for both card and group dicts - Unit tests Differential Revision: D98738752
1 parent da9a3aa commit 69bade3

8 files changed

Lines changed: 73 additions & 2 deletions

File tree

ax/core/analysis_card.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
<div class="card-header">
5959
<b>{title_str}</b>
6060
<div class="card-subtitle">{subtitle_str}</div>
61-
<button class="card-subtitle-toggle">See more</button>
61+
<button class="card-subtitle-toggle">{toggle_text}</button>
6262
</div>
6363
<div class="card-body">
6464
{body_html}
@@ -70,6 +70,7 @@
7070
var subtitle = card.querySelector('.card-subtitle');
7171
var toggle = card.querySelector('.card-subtitle-toggle');
7272
if (subtitle && toggle) {{
73+
var originalText = toggle.textContent;
7374
requestAnimationFrame(function() {{
7475
if (subtitle.scrollHeight > subtitle.clientHeight
7576
|| subtitle.scrollWidth > subtitle.clientWidth) {{
@@ -81,7 +82,7 @@
8182
subtitle.style.maxHeight = '1.4em';
8283
subtitle.style.whiteSpace = 'nowrap';
8384
subtitle.style.textOverflow = 'ellipsis';
84-
toggle.textContent = 'See more';
85+
toggle.textContent = originalText;
8586
}} else {{
8687
subtitle.style.maxHeight = 'none';
8788
subtitle.style.whiteSpace = 'normal';
@@ -154,6 +155,7 @@ class AnalysisCardBase(SortableBase, ABC):
154155

155156
title: str
156157
subtitle: str
158+
subtitle_toggle_label: str
157159

158160
_timestamp: datetime
159161

@@ -163,6 +165,7 @@ def __init__(
163165
title: str,
164166
subtitle: str,
165167
timestamp: datetime | None = None,
168+
subtitle_toggle_label: str = "",
166169
) -> None:
167170
"""
168171
Args:
@@ -175,10 +178,14 @@ def __init__(
175178
timestamp: The time at which the Analysis was computed. This can be
176179
especially useful when querying the database for the most recently
177180
produced artifacts.
181+
subtitle_toggle_label: Custom label for the subtitle expansion
182+
toggle. When non-empty, replaces the default "See more" text. Persisted to
183+
storage and used by both notebook and web UI rendering.
178184
"""
179185
self.name = name
180186
self.title = title
181187
self.subtitle = subtitle
188+
self.subtitle_toggle_label = subtitle_toggle_label
182189
self._timestamp = timestamp if timestamp is not None else datetime.now()
183190

184191
@abstractmethod
@@ -258,10 +265,12 @@ def _repr_html_(self) -> str:
258265
return plotlyjs_script + self._to_html(depth=0)
259266

260267
def _to_html(self, depth: int) -> str:
268+
toggle_text = self.subtitle_toggle_label or "See more"
261269
return html_card_template.format(
262270
title_str=self.title,
263271
subtitle_str=self.subtitle,
264272
body_html=self._body_html(depth=depth),
273+
toggle_text=toggle_text,
265274
)
266275

267276

@@ -282,6 +291,7 @@ def __init__(
282291
subtitle: str | None,
283292
children: Sequence[AnalysisCardBase],
284293
timestamp: datetime | None = None,
294+
subtitle_toggle_label: str = "",
285295
) -> None:
286296
"""
287297
Args:
@@ -294,12 +304,15 @@ def __init__(
294304
timestamp: The time at which the Analysis was computed. This can be
295305
especially useful when querying the database for the most recently
296306
produced artifacts.
307+
subtitle_toggle_label: Custom label for the subtitle expansion
308+
toggle. When non-empty, replaces the default "See more" text
297309
"""
298310
super().__init__(
299311
name=name,
300312
title=title,
301313
subtitle=subtitle if subtitle is not None else "",
302314
timestamp=timestamp,
315+
subtitle_toggle_label=subtitle_toggle_label,
303316
)
304317

305318
self.children = [
@@ -404,6 +417,7 @@ def __init__(
404417
df: pd.DataFrame,
405418
blob: str,
406419
timestamp: datetime | None = None,
420+
subtitle_toggle_label: str = "",
407421
) -> None:
408422
"""
409423
Args:
@@ -417,12 +431,15 @@ def __init__(
417431
timestamp: The time at which the Analysis was computed. This can be
418432
especially useful when querying the database for the most recently
419433
produced artifacts.
434+
subtitle_toggle_label: Custom label for the subtitle expansion
435+
toggle. When non-empty, replaces the default "See more" text
420436
"""
421437
super().__init__(
422438
name=name,
423439
title=title,
424440
subtitle=subtitle,
425441
timestamp=timestamp,
442+
subtitle_toggle_label=subtitle_toggle_label,
426443
)
427444

428445
self.df = df

ax/core/tests/test_analysis_card.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,25 @@ def test_not_applicable_card(self) -> None:
7878
blob="Explanation text.",
7979
)
8080
self.assertIn("Explanation text.", card._body_html(depth=0))
81+
82+
def test_subtitle_toggle_label_rendering(self) -> None:
83+
"""Verify subtitle_toggle_label controls toggle button text in HTML."""
84+
for label, expected_text in (
85+
("", "See more"),
86+
(
87+
"Expand to see annotated parameters.",
88+
"Expand to see annotated parameters.",
89+
),
90+
):
91+
with self.subTest():
92+
card = AnalysisCard(
93+
name="Test",
94+
title="Title",
95+
subtitle="A long subtitle",
96+
df=pd.DataFrame(),
97+
blob="blob",
98+
subtitle_toggle_label=label,
99+
)
100+
self.assertEqual(card.subtitle_toggle_label, label)
101+
html = card._repr_html_()
102+
self.assertIn(expected_text, html)

ax/storage/json_store/encoders.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def analysis_card_to_dict(card: AnalysisCard) -> dict[str, Any]:
8989
"df": card.df,
9090
"blob": card.blob,
9191
"timestamp": card._timestamp,
92+
"subtitle_toggle_label": card.subtitle_toggle_label,
9293
}
9394

9495

@@ -101,6 +102,7 @@ def analysis_card_group_to_dict(group: AnalysisCardGroup) -> dict[str, Any]:
101102
"subtitle": group.subtitle,
102103
"children": group.children,
103104
"timestamp": group._timestamp,
105+
"subtitle_toggle_label": group.subtitle_toggle_label,
104106
}
105107

106108

ax/storage/json_store/tests/test_json_store.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@
421421
subtitle="subtitle",
422422
df=pd.DataFrame({"a": [1, 2]}),
423423
blob="blob_str",
424+
subtitle_toggle_label="Expand to see details.",
424425
),
425426
),
426427
(
@@ -495,6 +496,7 @@
495496
blob="# md",
496497
),
497498
],
499+
subtitle_toggle_label="Expand to see children.",
498500
),
499501
),
500502
]

ax/storage/sqa_store/decoder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,19 +1163,22 @@ def analysis_card_from_sqa(
11631163
analysis_card_sqa.title if analysis_card_sqa.title is not None else ""
11641164
)
11651165
subtitle = analysis_card_sqa.subtitle
1166+
subtitle_toggle_label = analysis_card_sqa.subtitle_toggle_label or ""
11661167

11671168
return AnalysisCardGroup(
11681169
name=analysis_card_sqa.name,
11691170
title=title,
11701171
subtitle=subtitle,
11711172
children=children,
11721173
timestamp=analysis_card_sqa.timestamp,
1174+
subtitle_toggle_label=subtitle_toggle_label,
11731175
)
11741176

11751177
title = none_throws(analysis_card_sqa.title)
11761178
subtitle = none_throws(analysis_card_sqa.subtitle)
11771179
blob = none_throws(analysis_card_sqa.blob)
11781180
blob_annotation = analysis_card_sqa.blob_annotation
1181+
subtitle_toggle_label = analysis_card_sqa.subtitle_toggle_label or ""
11791182

11801183
if blob_annotation == "not_applicable_state":
11811184
return NotApplicableStateAnalysisCard(
@@ -1185,6 +1188,7 @@ def analysis_card_from_sqa(
11851188
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
11861189
blob=blob,
11871190
timestamp=analysis_card_sqa.timestamp,
1191+
subtitle_toggle_label=subtitle_toggle_label,
11881192
)
11891193
if blob_annotation == "error":
11901194
return ErrorAnalysisCard(
@@ -1194,6 +1198,7 @@ def analysis_card_from_sqa(
11941198
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
11951199
blob=blob,
11961200
timestamp=analysis_card_sqa.timestamp,
1201+
subtitle_toggle_label=subtitle_toggle_label,
11971202
)
11981203
if blob_annotation == "plotly":
11991204
return PlotlyAnalysisCard(
@@ -1203,6 +1208,7 @@ def analysis_card_from_sqa(
12031208
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
12041209
blob=blob,
12051210
timestamp=analysis_card_sqa.timestamp,
1211+
subtitle_toggle_label=subtitle_toggle_label,
12061212
)
12071213
if blob_annotation == "markdown":
12081214
return MarkdownAnalysisCard(
@@ -1212,6 +1218,7 @@ def analysis_card_from_sqa(
12121218
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
12131219
blob=blob,
12141220
timestamp=analysis_card_sqa.timestamp,
1221+
subtitle_toggle_label=subtitle_toggle_label,
12151222
)
12161223
if blob_annotation == "healthcheck":
12171224
return HealthcheckAnalysisCard(
@@ -1221,6 +1228,7 @@ def analysis_card_from_sqa(
12211228
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
12221229
blob=blob,
12231230
timestamp=analysis_card_sqa.timestamp,
1231+
subtitle_toggle_label=subtitle_toggle_label,
12241232
)
12251233
if blob_annotation == "graphviz":
12261234
return GraphvizAnalysisCard(
@@ -1230,6 +1238,7 @@ def analysis_card_from_sqa(
12301238
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
12311239
blob=blob,
12321240
timestamp=analysis_card_sqa.timestamp,
1241+
subtitle_toggle_label=subtitle_toggle_label,
12331242
)
12341243
return AnalysisCard(
12351244
name=analysis_card_sqa.name,
@@ -1238,6 +1247,7 @@ def analysis_card_from_sqa(
12381247
df=read_json(StringIO(analysis_card_sqa.dataframe_json)),
12391248
blob=blob,
12401249
timestamp=analysis_card_sqa.timestamp,
1250+
subtitle_toggle_label=subtitle_toggle_label,
12411251
)
12421252

12431253
def _metric_from_sqa_util(self, metric_sqa: SQAMetric) -> Metric:

ax/storage/sqa_store/encoder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,7 @@ def analysis_card_to_sqa(
12641264
dataframe_json=None,
12651265
blob=None,
12661266
blob_annotation=None,
1267+
subtitle_toggle_label=analysis_card.subtitle_toggle_label or None,
12671268
)
12681269

12691270
for i, child_card in enumerate(analysis_card.children):
@@ -1304,4 +1305,5 @@ def analysis_card_to_sqa(
13041305
dataframe_json=card.df.to_json(),
13051306
blob=card.blob,
13061307
blob_annotation=blob_annotation,
1308+
subtitle_toggle_label=card.subtitle_toggle_label or None,
13071309
)

ax/storage/sqa_store/sqa_classes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ class SQAAnalysisCard(Base):
455455
blob_annotation: Column[str | None] = Column(
456456
String(NAME_OR_TYPE_FIELD_LENGTH), nullable=True
457457
)
458+
subtitle_toggle_label: Column[str | None] = Column(
459+
"subtitle_toggle_label", String(LONG_STRING_FIELD_LENGTH), nullable=True
460+
)
458461
parent: Any = relationship(
459462
"SQAAnalysisCard",
460463
back_populates="children",

ax/storage/sqa_store/tests/test_sqa_store.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,6 +3015,7 @@ def test_analysis_card(self) -> None:
30153015
subtitle="test_subtitle",
30163016
df=test_df,
30173017
blob="test blob",
3018+
subtitle_toggle_label="Expand to see details.",
30183019
)
30193020
markdown_analysis_card = MarkdownAnalysisCard(
30203021
name="test_markdown_analysis_card",
@@ -3038,6 +3039,7 @@ def test_analysis_card(self) -> None:
30383039
title="Small Group",
30393040
subtitle="This is a small group with just a few cards",
30403041
children=[base_analysis_card, markdown_analysis_card, plotly_analysis_card],
3042+
subtitle_toggle_label="Expand to see group children.",
30413043
)
30423044
big_group = AnalysisCardGroup(
30433045
name="big_group",
@@ -3067,37 +3069,48 @@ def test_analysis_card(self) -> None:
30673069
self.assertEqual(loaded_big_group.name, big_group.name)
30683070
self.assertEqual(loaded_big_group.title, big_group.title)
30693071
self.assertEqual(loaded_big_group.subtitle, big_group.subtitle)
3072+
self.assertEqual(loaded_big_group.subtitle_toggle_label, "")
30703073

30713074
loaded_big_group_plotly = assert_is_instance(
30723075
loaded_big_group.children[0], PlotlyAnalysisCard
30733076
)
30743077
self.assertEqual(loaded_big_group_plotly.name, plotly_analysis_card.name)
30753078
self.assertEqual(loaded_big_group_plotly.blob, plotly_analysis_card.blob)
3079+
self.assertEqual(loaded_big_group_plotly.subtitle_toggle_label, "")
30763080

30773081
loaded_small_group = assert_is_instance(
30783082
loaded_big_group.children[1], AnalysisCardGroup
30793083
)
30803084
self.assertEqual(loaded_small_group.name, small_group.name)
30813085
self.assertEqual(loaded_small_group.title, small_group.title)
30823086
self.assertEqual(loaded_small_group.subtitle, small_group.subtitle)
3087+
self.assertEqual(
3088+
loaded_small_group.subtitle_toggle_label,
3089+
"Expand to see group children.",
3090+
)
30833091

30843092
loaded_base = assert_is_instance(
30853093
loaded_small_group.children[0], AnalysisCard
30863094
)
30873095
self.assertEqual(loaded_base.name, base_analysis_card.name)
30883096
self.assertEqual(loaded_base.blob, base_analysis_card.blob)
3097+
self.assertEqual(
3098+
loaded_base.subtitle_toggle_label, "Expand to see details."
3099+
)
30893100

30903101
loaded_markdown = assert_is_instance(
30913102
loaded_small_group.children[1], MarkdownAnalysisCard
30923103
)
30933104
self.assertEqual(loaded_markdown.name, markdown_analysis_card.name)
30943105
self.assertEqual(loaded_markdown.blob, markdown_analysis_card.blob)
3106+
self.assertEqual(loaded_markdown.subtitle_toggle_label, "")
30953107

30963108
loaded_small_group_plotly = assert_is_instance(
30973109
loaded_small_group.children[2], PlotlyAnalysisCard
30983110
)
30993111
self.assertEqual(loaded_small_group_plotly.name, plotly_analysis_card.name)
31003112
self.assertEqual(loaded_small_group_plotly.blob, plotly_analysis_card.blob)
3113+
self.assertEqual(loaded_small_group_plotly.subtitle_toggle_label, "")
31013114

31023115
def test_delete_generation_strategy(self) -> None:
31033116
# GIVEN an experiment with a generation strategy

0 commit comments

Comments
 (0)