Skip to content

Commit 20194e8

Browse files
ENH - Adding a parameter to select the default TableReport tab to show (#1737)
Co-authored-by: Jérôme Dockès <[email protected]>
1 parent 101a926 commit 20194e8

File tree

8 files changed

+189
-10
lines changed

8 files changed

+189
-10
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ New features
3030
- :meth:`DataOp.skb.full_report` now accepts a new parameter, title, that is displayed
3131
in the html report.
3232
:pr:`1654` by :user:`Marie Sacksick <MarieSacksick>`.
33+
- :class:`TableReport` now includes the ``open_tab`` parameter, which lets the
34+
user select which tab should be opened when the ``TableReport`` is
35+
rendered. :pr:`1737` by :user:`Riccardo Cappuzzo<rcap107>`.
36+
3337

3438
Changes
3539
-------

skrub/_reporting/_data/templates/report.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ if (customElements.get('skrub-table-report') === undefined) {
490490
constructor(elem, exchange) {
491491
super(elem, exchange);
492492
this.tabs = new Map();
493+
let preSelectedTab = null;
493494
this.elem.querySelectorAll("button[data-role='tab']").forEach(
494495
tab => {
495496
const panel = tab.getRootNode().getElementById(tab.dataset
@@ -499,6 +500,10 @@ if (customElements.get('skrub-table-report') === undefined) {
499500
this.firstTab = tab;
500501
}
501502
this.lastTab = tab;
503+
// Check for pre-selected tab
504+
if (tab.hasAttribute('data-is-selected')) {
505+
preSelectedTab = tab;
506+
}
502507
tab.addEventListener("click", () => this.selectTab(tab));
503508
// See forwardKeyboardEvent for details about captureKeys
504509
tab.dataset.captureKeys = "ArrowRight ArrowLeft";
@@ -508,7 +513,8 @@ if (customElements.get('skrub-table-report') === undefined) {
508513
.onKeyDown(
509514
unwrapSkrubKeyDown(event)));
510515
});
511-
this.selectTab(this.firstTab, false);
516+
// Select pre-selected tab if exists, otherwise select first tab
517+
this.selectTab(preSelectedTab || this.firstTab, false);
512518
}
513519

514520
selectTab(tabToSelect, focus = true) {

skrub/_reporting/_data/templates/tabs.html

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@
1515
<div class="tab-list tab-list-scroller" data-manager="TabList" >
1616
<div>
1717
<button type="button" data-target-panel-id="dataframe-sample-panel"
18-
data-role="tab" data-is-selected data-test="sample-tab" class="tab"
18+
data-role="tab" {% if open_panel_id == "dataframe-sample-panel" %}data-is-selected{% endif %} data-test="sample-tab" class="tab"
1919
title="{{ table_title }}">Table</button>
2020
</div>
2121
<div>
2222
<button type="button" data-target-panel-id="summary-statistics-panel"
23-
data-role="tab" data-test="summary-statistics-tab" class="tab"
23+
data-role="tab" {% if open_panel_id == "summary-statistics-panel" %}data-is-selected{% endif %} data-test="summary-statistics-tab" class="tab"
2424
title="{{ stats_title }}">Stats</button>
2525
</div>
2626
{% if not minimal_report_mode %}
2727
<div>
2828
<button type="button" data-target-panel-id="column-summaries-panel"
29-
data-role="tab" data-test="summaries-tab" class="tab"
29+
data-role="tab" {% if open_panel_id == "column-summaries-panel" %}data-is-selected{% endif %} data-test="summaries-tab" class="tab"
3030
title="{{ column_summaries_title }}">Distributions</button>
3131
</div>
3232
<div>
3333
<button type="button" data-target-panel-id="column-associations-panel"
34-
data-role="tab" class="tab" {% if associations_warning %}
34+
data-role="tab" {% if open_panel_id == "column-associations-panel" %}data-is-selected{% endif %} class="tab" {% if associations_warning %}
3535
data-has-warning {% endif %} data-test="associations-tab"
3636
title="{{ associations_title }}">
3737
<div class="warning-sign">
@@ -45,19 +45,20 @@
4545
</div>
4646
</div>
4747

48-
<div class="tab-panel" id="dataframe-sample-panel" data-test="sample-panel">
48+
<div class="tab-panel" id="dataframe-sample-panel" {% if open_panel_id != "dataframe-sample-panel" %}data-hidden{% endif %}
49+
data-test="sample-panel">
4950
{% include "dataframe-sample.html" %}
5051
</div>
51-
<div class="tab-panel" id="summary-statistics-panel" data-hidden
52+
<div class="tab-panel" id="summary-statistics-panel" {% if open_panel_id != "summary-statistics-panel" %}data-hidden{% endif %}
5253
data-test="summary-statistics-panel">
5354
{% include "summary-statistics.html" %}
5455
</div>
5556
{% if not minimal_report_mode %}
56-
<div class="tab-panel" id="column-summaries-panel" data-hidden
57+
<div class="tab-panel" id="column-summaries-panel" {% if open_panel_id != "column-summaries-panel" %}data-hidden{% endif %}
5758
data-test="summaries-panel">
5859
{% include "column-summaries.html" %}
5960
</div>
60-
<div class="tab-panel" id="column-associations-panel" data-hidden
61+
<div class="tab-panel" id="column-associations-panel" {% if open_panel_id != "column-associations-panel" %}data-hidden{% endif %}
6162
data-test="associations-panel">
6263
{% include "column-associations.html" %}
6364
</div>

skrub/_reporting/_html.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717

1818
_HIGH_ASSOCIATION_THRESHOLD = 0.9
1919

20+
_TAB_NAME_TO_ID = {
21+
"table": "dataframe-sample-panel",
22+
"stats": "summary-statistics-panel",
23+
"distributions": "column-summaries-panel",
24+
"associations": "column-associations-panel",
25+
}
26+
2027
_FILTER_NAMES = {
2128
"first_10": "First 10",
2229
"high_association": "High similarity",
@@ -108,7 +115,13 @@ def _get_column_filters(summary):
108115
return filters
109116

110117

111-
def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=False):
118+
def to_html(
119+
summary,
120+
standalone=True,
121+
column_filters=None,
122+
minimal_report_mode=False,
123+
open_tab="table",
124+
):
112125
"""Given a dataframe summary, generate the HTML string.
113126
114127
Parameters
@@ -128,13 +141,19 @@ def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=F
128141
minimal_report_mode : bool
129142
Whether to turn on the minimal mode, which hides the 'distributions'
130143
and 'associations' tabs.
144+
open_tab : str, default="table"
145+
The tab that will be displayed by default when the report is opened.
146+
Must be one of "table", "stats", "distributions", or "associations".
131147
132148
Returns
133149
-------
134150
str
135151
The report as a string (containing HTML).
136152
"""
137153
column_filters = column_filters if column_filters is not None else {}
154+
155+
open_panel_id = _TAB_NAME_TO_ID[open_tab]
156+
138157
jinja_env = _get_jinja_env()
139158
if standalone:
140159
template = jinja_env.get_template("standalone-report.html")
@@ -153,6 +172,7 @@ def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=F
153172
"base64_column_filters": _b64_encode(column_filters),
154173
"report_id": f"report_{secrets.token_hex()[:8]}",
155174
"minimal_report_mode": minimal_report_mode,
175+
"open_panel_id": open_panel_id,
156176
"config": _config.get_config(),
157177
}
158178
)

skrub/_reporting/_table_report.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ class TableReport:
113113
114114
export SKB_MAX_ASSOCIATION_COLUMNS=30
115115
116+
open_tab : str, default="table"
117+
The tab that will be displayed by default when the report is opened.
118+
Must be one of "table", "stats", "distributions", or "associations".
119+
120+
* "table": Shows a sample of the dataframe rows
121+
* "stats": Shows summary statistics for all columns
122+
* "distributions": Shows plots of column distributions
123+
* "associations": Shows column associations and similarities
124+
116125
See Also
117126
--------
118127
patch_display :
@@ -187,6 +196,7 @@ def __init__(
187196
verbose=1,
188197
max_plot_columns=None,
189198
max_association_columns=None,
199+
open_tab="table",
190200
):
191201
if isinstance(dataframe, np.ndarray):
192202
if dataframe.ndim == 1:
@@ -204,6 +214,15 @@ def __init__(
204214
)
205215

206216
n_rows = max(1, n_rows)
217+
218+
# Validate open_tab parameter
219+
valid_tabs = ["table", "stats", "distributions", "associations"]
220+
if open_tab not in valid_tabs:
221+
raise ValueError(
222+
f"'open_tab' must be one of {valid_tabs}, got {open_tab!r}."
223+
)
224+
self.open_tab = open_tab
225+
207226
self._summary_kwargs = {
208227
"order_by": order_by,
209228
"max_top_slice_size": -(n_rows // -2),
@@ -247,6 +266,9 @@ def _set_minimal_mode(self):
247266
self._to_html_kwargs["minimal_report_mode"] = True
248267
self.max_association_columns = 0
249268
self.max_plot_columns = 0
269+
# In minimal mode, fall back to 'table' if user selected unavailable tabs
270+
if self.open_tab in ["distributions", "associations"]:
271+
self.open_tab = "table"
250272

251273
def _display_subsample_hint(self):
252274
self._summary["is_subsampled"] = True
@@ -284,6 +306,7 @@ def html(self):
284306
self._summary,
285307
standalone=True,
286308
column_filters=self.column_filters,
309+
open_tab=self.open_tab,
287310
**self._to_html_kwargs,
288311
)
289312

@@ -299,6 +322,7 @@ def html_snippet(self):
299322
self._summary,
300323
standalone=False,
301324
column_filters=self.column_filters,
325+
open_tab=self.open_tab,
302326
**self._to_html_kwargs,
303327
)
304328

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
describe('test open_tab parameter', () => {
2+
// Map each open_tab value to its corresponding tab/panel selectors and test file
3+
const tabs = {
4+
'sample': { tab: 'sample-tab', panel: 'sample-panel', file: 'employee_salaries.html' },
5+
'stats': { tab: 'summary-statistics-tab', panel: 'summary-statistics-panel', file: 'open_tab_stats.html' },
6+
'distributions': { tab: 'summaries-tab', panel: 'summaries-panel', file: 'open_tab_distributions.html' },
7+
'associations': { tab: 'associations-tab', panel: 'associations-panel', file: 'open_tab_associations.html' },
8+
};
9+
10+
// Extract all tab and panel selectors for validation
11+
const allTabs = Object.values(tabs).map(t => t.tab);
12+
const allPanels = Object.values(tabs).map(t => t.panel);
13+
14+
// Generate a test case for each open_tab value
15+
Object.entries(tabs).forEach(([name, { tab, panel, file }]) => {
16+
it(`opens on the ${name} tab when open_tab="${name}"`, () => {
17+
cy.visit(`_reports/${file}`);
18+
cy.get('skrub-table-report').shadow().as('report');
19+
20+
// Verify the correct tab is selected and its panel is visible
21+
cy.get('@report').find(`[data-test="${tab}"]`).should('have.data', 'isSelected');
22+
cy.get('@report').find(`[data-test="${panel}"]`).should('be.visible');
23+
24+
// Verify all other tabs are not selected
25+
allTabs.filter(t => t !== tab).forEach(t => {
26+
cy.get('@report').find(`[data-test="${t}"]`).should('not.have.data', 'isSelected');
27+
});
28+
29+
// Verify all other panels are not visible
30+
allPanels.filter(p => p !== panel).forEach(p => {
31+
cy.get('@report').find(`[data-test="${p}"]`).should('not.be.visible');
32+
});
33+
});
34+
});
35+
});

skrub/_reporting/js_tests/make-reports

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,22 @@ if __name__ == "__main__":
109109

110110
html = TableReport(df).html()
111111
(reports_dir / "multi_index.html").write_text(html, encoding="UTF-8")
112+
113+
#
114+
# Reports with different open_tab settings for testing
115+
# -----------------------------------------------------
116+
#
117+
118+
df = datasets.fetch_employee_salaries().X
119+
120+
# Report opening on stats tab
121+
html = TableReport(df, open_tab="stats").html()
122+
(reports_dir / "open_tab_stats.html").write_text(html, encoding="UTF-8")
123+
124+
# Report opening on distributions tab
125+
html = TableReport(df, open_tab="distributions").html()
126+
(reports_dir / "open_tab_distributions.html").write_text(html, encoding="UTF-8")
127+
128+
# Report opening on associations tab
129+
html = TableReport(df, open_tab="associations").html()
130+
(reports_dir / "open_tab_associations.html").write_text(html, encoding="UTF-8")

skrub/_reporting/tests/test_table_report.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,73 @@ def test_polars_df_no_pyarrow():
428428
"Computing pairwise associations is not available for Polars dataframes "
429429
"when PyArrow is not installed" in html_snippet
430430
)
431+
432+
433+
@skip_polars_installed_without_pyarrow
434+
def test_open_tab_parameter(df_module):
435+
"""Test the open_tab parameter functionality"""
436+
df = df_module.make_dataframe(
437+
{
438+
"A": [1, 2, 3, 4, 5],
439+
"B": ["a", "b", "c", "d", "e"],
440+
}
441+
)
442+
443+
# Test open behavior (should be 'table')
444+
report1 = TableReport(df)
445+
assert report1.open_tab == "table"
446+
447+
# Test explicitly set to 'stats'
448+
report2 = TableReport(df, open_tab="stats")
449+
assert report2.open_tab == "stats"
450+
451+
# Test set to 'distributions'
452+
report3 = TableReport(df, open_tab="distributions")
453+
assert report3.open_tab == "distributions"
454+
455+
# Test set to 'associations'
456+
report4 = TableReport(df, open_tab="associations")
457+
assert report4.open_tab == "associations"
458+
459+
# Test HTML generation includes correct attributes
460+
html_snippet = report2.html_snippet()
461+
assert 'data-target-panel-id="summary-statistics-panel"' in html_snippet
462+
assert "data-is-selected" in html_snippet
463+
464+
465+
@skip_polars_installed_without_pyarrow
466+
def test_open_tab_wrong_names(df_module):
467+
df = df_module.make_dataframe(
468+
{
469+
"A": [1, 2, 3, 4, 5],
470+
"B": ["a", "b", "c", "d", "e"],
471+
}
472+
)
473+
474+
# Test invalid tab name (should raise error)
475+
with pytest.raises(ValueError, match="'open_tab' must be one of"):
476+
TableReport(df, open_tab="invalid")
477+
478+
with pytest.raises(ValueError, match="'open_tab' must be one of"):
479+
TableReport(df, open_tab="invalid").html()
480+
481+
482+
@skip_polars_installed_without_pyarrow
483+
def test_open_tab_minimal_mode(df_module):
484+
"""Test that default_tab falls back to 'table' in minimal mode when needed"""
485+
df = df_module.make_dataframe(
486+
{
487+
"A": [1, 2, 3, 4, 5],
488+
"B": ["a", "b", "c", "d", "e"],
489+
}
490+
)
491+
492+
# Test minimal mode with open_tab set to 'distributions'
493+
report1 = TableReport(df, open_tab="distributions")
494+
report1._set_minimal_mode()
495+
assert report1.open_tab == "table"
496+
497+
# Test minimal mode with open_tab set to 'associations'
498+
report2 = TableReport(df, open_tab="associations")
499+
report2._set_minimal_mode()
500+
assert report2.open_tab == "table"

0 commit comments

Comments
 (0)