Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ New features
- :meth:`DataOp.skb.full_report` now accepts a new parameter, title, that is displayed
in the html report.
:pr:`1654` by :user:`Marie Sacksick <MarieSacksick>`.
- :class:`TableReport` now includes the ``open_tab`` parameter, which lets the
user select which tab should be opened when the ``TableReport`` is
rendered. :pr:`1737` by :user:`Riccardo Cappuzzo<rcap107>`.


Changes
-------
Expand Down
8 changes: 7 additions & 1 deletion skrub/_reporting/_data/templates/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ if (customElements.get('skrub-table-report') === undefined) {
constructor(elem, exchange) {
super(elem, exchange);
this.tabs = new Map();
let preSelectedTab = null;
this.elem.querySelectorAll("button[data-role='tab']").forEach(
tab => {
const panel = tab.getRootNode().getElementById(tab.dataset
Expand All @@ -499,6 +500,10 @@ if (customElements.get('skrub-table-report') === undefined) {
this.firstTab = tab;
}
this.lastTab = tab;
// Check for pre-selected tab
if (tab.hasAttribute('data-is-selected')) {
preSelectedTab = tab;
}
tab.addEventListener("click", () => this.selectTab(tab));
// See forwardKeyboardEvent for details about captureKeys
tab.dataset.captureKeys = "ArrowRight ArrowLeft";
Expand All @@ -508,7 +513,8 @@ if (customElements.get('skrub-table-report') === undefined) {
.onKeyDown(
unwrapSkrubKeyDown(event)));
});
this.selectTab(this.firstTab, false);
// Select pre-selected tab if exists, otherwise select first tab
this.selectTab(preSelectedTab || this.firstTab, false);
}

selectTab(tabToSelect, focus = true) {
Expand Down
17 changes: 9 additions & 8 deletions skrub/_reporting/_data/templates/tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@
<div class="tab-list tab-list-scroller" data-manager="TabList" >
<div>
<button type="button" data-target-panel-id="dataframe-sample-panel"
data-role="tab" data-is-selected data-test="sample-tab" class="tab"
data-role="tab" {% if open_panel_id == "dataframe-sample-panel" %}data-is-selected{% endif %} data-test="sample-tab" class="tab"
title="{{ table_title }}">Table</button>
</div>
<div>
<button type="button" data-target-panel-id="summary-statistics-panel"
data-role="tab" data-test="summary-statistics-tab" class="tab"
data-role="tab" {% if open_panel_id == "summary-statistics-panel" %}data-is-selected{% endif %} data-test="summary-statistics-tab" class="tab"
title="{{ stats_title }}">Stats</button>
</div>
{% if not minimal_report_mode %}
<div>
<button type="button" data-target-panel-id="column-summaries-panel"
data-role="tab" data-test="summaries-tab" class="tab"
data-role="tab" {% if open_panel_id == "column-summaries-panel" %}data-is-selected{% endif %} data-test="summaries-tab" class="tab"
title="{{ column_summaries_title }}">Distributions</button>
</div>
<div>
<button type="button" data-target-panel-id="column-associations-panel"
data-role="tab" class="tab" {% if associations_warning %}
data-role="tab" {% if open_panel_id == "column-associations-panel" %}data-is-selected{% endif %} class="tab" {% if associations_warning %}
data-has-warning {% endif %} data-test="associations-tab"
title="{{ associations_title }}">
<div class="warning-sign">
Expand All @@ -45,19 +45,20 @@
</div>
</div>

<div class="tab-panel" id="dataframe-sample-panel" data-test="sample-panel">
<div class="tab-panel" id="dataframe-sample-panel" {% if open_panel_id != "dataframe-sample-panel" %}data-hidden{% endif %}
data-test="sample-panel">
{% include "dataframe-sample.html" %}
</div>
<div class="tab-panel" id="summary-statistics-panel" data-hidden
<div class="tab-panel" id="summary-statistics-panel" {% if open_panel_id != "summary-statistics-panel" %}data-hidden{% endif %}
data-test="summary-statistics-panel">
{% include "summary-statistics.html" %}
</div>
{% if not minimal_report_mode %}
<div class="tab-panel" id="column-summaries-panel" data-hidden
<div class="tab-panel" id="column-summaries-panel" {% if open_panel_id != "column-summaries-panel" %}data-hidden{% endif %}
data-test="summaries-panel">
{% include "column-summaries.html" %}
</div>
<div class="tab-panel" id="column-associations-panel" data-hidden
<div class="tab-panel" id="column-associations-panel" {% if open_panel_id != "column-associations-panel" %}data-hidden{% endif %}
data-test="associations-panel">
{% include "column-associations.html" %}
</div>
Expand Down
22 changes: 21 additions & 1 deletion skrub/_reporting/_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@

_HIGH_ASSOCIATION_THRESHOLD = 0.9

_TAB_NAME_TO_ID = {
"table": "dataframe-sample-panel",
"stats": "summary-statistics-panel",
"distributions": "column-summaries-panel",
"associations": "column-associations-panel",
}

_FILTER_NAMES = {
"first_10": "First 10",
"high_association": "High similarity",
Expand Down Expand Up @@ -108,7 +115,13 @@ def _get_column_filters(summary):
return filters


def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=False):
def to_html(
summary,
standalone=True,
column_filters=None,
minimal_report_mode=False,
open_tab="table",
):
"""Given a dataframe summary, generate the HTML string.

Parameters
Expand All @@ -128,13 +141,19 @@ def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=F
minimal_report_mode : bool
Whether to turn on the minimal mode, which hides the 'distributions'
and 'associations' tabs.
open_tab : str, default="table"
The tab that will be displayed by default when the report is opened.
Must be one of "table", "stats", "distributions", or "associations".

Returns
-------
str
The report as a string (containing HTML).
"""
column_filters = column_filters if column_filters is not None else {}

open_panel_id = _TAB_NAME_TO_ID[open_tab]

jinja_env = _get_jinja_env()
if standalone:
template = jinja_env.get_template("standalone-report.html")
Expand All @@ -153,6 +172,7 @@ def to_html(summary, standalone=True, column_filters=None, minimal_report_mode=F
"base64_column_filters": _b64_encode(column_filters),
"report_id": f"report_{secrets.token_hex()[:8]}",
"minimal_report_mode": minimal_report_mode,
"open_panel_id": open_panel_id,
"config": _config.get_config(),
}
)
Expand Down
24 changes: 24 additions & 0 deletions skrub/_reporting/_table_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ class TableReport:

export SKB_MAX_ASSOCIATION_COLUMNS=30

open_tab : str, default="table"
The tab that will be displayed by default when the report is opened.
Must be one of "table", "stats", "distributions", or "associations".

* "table": Shows a sample of the dataframe rows
* "stats": Shows summary statistics for all columns
* "distributions": Shows plots of column distributions
* "associations": Shows column associations and similarities

See Also
--------
patch_display :
Expand Down Expand Up @@ -188,6 +197,7 @@ def __init__(
verbose=1,
max_plot_columns=None,
max_association_columns=None,
open_tab="table",
):
if isinstance(dataframe, np.ndarray):
if dataframe.ndim == 1:
Expand All @@ -205,6 +215,15 @@ def __init__(
)

n_rows = max(1, n_rows)

# Validate open_tab parameter
valid_tabs = ["table", "stats", "distributions", "associations"]
if open_tab not in valid_tabs:
raise ValueError(
f"'open_tab' must be one of {valid_tabs}, got {open_tab!r}."
)
self.open_tab = open_tab

self._summary_kwargs = {
"order_by": order_by,
"max_top_slice_size": -(n_rows // -2),
Expand Down Expand Up @@ -243,6 +262,9 @@ def _set_minimal_mode(self):
self._to_html_kwargs["minimal_report_mode"] = True
self.max_association_columns = 0
self.max_plot_columns = 0
# In minimal mode, fall back to 'table' if user selected unavailable tabs
if self.open_tab in ["distributions", "associations"]:
self.open_tab = "table"

def _display_subsample_hint(self):
self._summary["is_subsampled"] = True
Expand Down Expand Up @@ -280,6 +302,7 @@ def html(self):
self._summary,
standalone=True,
column_filters=self.column_filters,
open_tab=self.open_tab,
**self._to_html_kwargs,
)

Expand All @@ -295,6 +318,7 @@ def html_snippet(self):
self._summary,
standalone=False,
column_filters=self.column_filters,
open_tab=self.open_tab,
**self._to_html_kwargs,
)

Expand Down
35 changes: 35 additions & 0 deletions skrub/_reporting/js_tests/cypress/e2e/open-tab.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
describe('test open_tab parameter', () => {
// Map each open_tab value to its corresponding tab/panel selectors and test file
const tabs = {
'sample': { tab: 'sample-tab', panel: 'sample-panel', file: 'employee_salaries.html' },
'stats': { tab: 'summary-statistics-tab', panel: 'summary-statistics-panel', file: 'open_tab_stats.html' },
'distributions': { tab: 'summaries-tab', panel: 'summaries-panel', file: 'open_tab_distributions.html' },
'associations': { tab: 'associations-tab', panel: 'associations-panel', file: 'open_tab_associations.html' },
};

// Extract all tab and panel selectors for validation
const allTabs = Object.values(tabs).map(t => t.tab);
const allPanels = Object.values(tabs).map(t => t.panel);

// Generate a test case for each open_tab value
Object.entries(tabs).forEach(([name, { tab, panel, file }]) => {
it(`opens on the ${name} tab when open_tab="${name}"`, () => {
cy.visit(`_reports/${file}`);
cy.get('skrub-table-report').shadow().as('report');

// Verify the correct tab is selected and its panel is visible
cy.get('@report').find(`[data-test="${tab}"]`).should('have.data', 'isSelected');
cy.get('@report').find(`[data-test="${panel}"]`).should('be.visible');

// Verify all other tabs are not selected
allTabs.filter(t => t !== tab).forEach(t => {
cy.get('@report').find(`[data-test="${t}"]`).should('not.have.data', 'isSelected');
});

// Verify all other panels are not visible
allPanels.filter(p => p !== panel).forEach(p => {
cy.get('@report').find(`[data-test="${p}"]`).should('not.be.visible');
});
});
});
});
19 changes: 19 additions & 0 deletions skrub/_reporting/js_tests/make-reports
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,22 @@ if __name__ == "__main__":

html = TableReport(df).html()
(reports_dir / "multi_index.html").write_text(html, encoding="UTF-8")

#
# Reports with different open_tab settings for testing
# -----------------------------------------------------
#

df = datasets.fetch_employee_salaries().X

# Report opening on stats tab
html = TableReport(df, open_tab="stats").html()
(reports_dir / "open_tab_stats.html").write_text(html, encoding="UTF-8")

# Report opening on distributions tab
html = TableReport(df, open_tab="distributions").html()
(reports_dir / "open_tab_distributions.html").write_text(html, encoding="UTF-8")

# Report opening on associations tab
html = TableReport(df, open_tab="associations").html()
(reports_dir / "open_tab_associations.html").write_text(html, encoding="UTF-8")
70 changes: 70 additions & 0 deletions skrub/_reporting/tests/test_table_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,73 @@ def test_polars_df_no_pyarrow():
"when PyArrow is not installed"
in html_snippet
)


@skip_polars_installed_without_pyarrow
def test_open_tab_parameter(df_module):
"""Test the open_tab parameter functionality"""
df = df_module.make_dataframe(
{
"A": [1, 2, 3, 4, 5],
"B": ["a", "b", "c", "d", "e"],
}
)

# Test open behavior (should be 'table')
report1 = TableReport(df)
assert report1.open_tab == "table"

# Test explicitly set to 'stats'
report2 = TableReport(df, open_tab="stats")
assert report2.open_tab == "stats"

# Test set to 'distributions'
report3 = TableReport(df, open_tab="distributions")
assert report3.open_tab == "distributions"

# Test set to 'associations'
report4 = TableReport(df, open_tab="associations")
assert report4.open_tab == "associations"

# Test HTML generation includes correct attributes
html_snippet = report2.html_snippet()
assert 'data-target-panel-id="summary-statistics-panel"' in html_snippet
assert "data-is-selected" in html_snippet


@skip_polars_installed_without_pyarrow
def test_open_tab_wrong_names(df_module):
df = df_module.make_dataframe(
{
"A": [1, 2, 3, 4, 5],
"B": ["a", "b", "c", "d", "e"],
}
)

# Test invalid tab name (should raise error)
with pytest.raises(ValueError, match="'open_tab' must be one of"):
TableReport(df, open_tab="invalid")

with pytest.raises(ValueError, match="'open_tab' must be one of"):
TableReport(df, open_tab="invalid").html()


@skip_polars_installed_without_pyarrow
def test_open_tab_minimal_mode(df_module):
"""Test that default_tab falls back to 'table' in minimal mode when needed"""
df = df_module.make_dataframe(
{
"A": [1, 2, 3, 4, 5],
"B": ["a", "b", "c", "d", "e"],
}
)

# Test minimal mode with open_tab set to 'distributions'
report1 = TableReport(df, open_tab="distributions")
report1._set_minimal_mode()
assert report1.open_tab == "table"

# Test minimal mode with open_tab set to 'associations'
report2 = TableReport(df, open_tab="associations")
report2._set_minimal_mode()
assert report2.open_tab == "table"