Skip to content

Commit 36d39a9

Browse files
authored
Merge branch 'main' into sycai_type_usage
2 parents 1255173 + 34b5975 commit 36d39a9

File tree

7 files changed

+332
-12
lines changed

7 files changed

+332
-12
lines changed

bigframes/display/anywidget.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class TableWidget(_WIDGET_BASE):
6666

6767
page = traitlets.Int(0).tag(sync=True)
6868
page_size = traitlets.Int(0).tag(sync=True)
69+
max_columns = traitlets.Int(allow_none=True, default_value=None).tag(sync=True)
6970
row_count = traitlets.Int(allow_none=True, default_value=None).tag(sync=True)
7071
table_html = traitlets.Unicode("").tag(sync=True)
7172
sort_context = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True)
@@ -103,10 +104,13 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
103104

104105
# respect display options for initial page size
105106
initial_page_size = bigframes.options.display.max_rows
107+
initial_max_columns = bigframes.options.display.max_columns
106108

107109
# set traitlets properties that trigger observers
108110
# TODO(b/462525985): Investigate and improve TableWidget UX for DataFrames with a large number of columns.
109111
self.page_size = initial_page_size
112+
self.max_columns = initial_max_columns
113+
110114
# TODO(b/469861913): Nested columns from structs (e.g., 'struct_col.name') are not currently sortable.
111115
# TODO(b/463754889): Support non-string column labels for sorting.
112116
if all(isinstance(col, str) for col in dataframe.columns):
@@ -218,6 +222,14 @@ def _validate_page_size(self, proposal: dict[str, Any]) -> int:
218222
max_page_size = 1000
219223
return min(value, max_page_size)
220224

225+
@traitlets.validate("max_columns")
226+
def _validate_max_columns(self, proposal: dict[str, Any]) -> int:
227+
"""Validate max columns to ensure it's positive or 0 (for all)."""
228+
value = proposal["value"]
229+
if value is None:
230+
return 0 # Normalize None to 0 for traitlet
231+
return max(0, value)
232+
221233
def _get_next_batch(self) -> bool:
222234
"""
223235
Gets the next batch of data from the generator and appends to cache.
@@ -348,6 +360,7 @@ def _set_table_html(self) -> None:
348360
dataframe=page_data,
349361
table_id=f"table-{self._table_id}",
350362
orderable_columns=self.orderable_columns,
363+
max_columns=self.max_columns,
351364
)
352365

353366
if new_page is not None:
@@ -382,3 +395,10 @@ def _page_size_changed(self, _change: dict[str, Any]) -> None:
382395

383396
# Update the table display
384397
self._set_table_html()
398+
399+
@traitlets.observe("max_columns")
400+
def _max_columns_changed(self, _change: dict[str, Any]) -> None:
401+
"""Handler for when max columns is changed from the frontend."""
402+
if not self._initial_load_complete:
403+
return
404+
self._set_table_html()

bigframes/display/html.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,51 @@ def render_html(
4646
dataframe: pd.DataFrame,
4747
table_id: str,
4848
orderable_columns: list[str] | None = None,
49+
max_columns: int | None = None,
4950
) -> str:
5051
"""Render a pandas DataFrame to HTML with specific styling."""
5152
orderable_columns = orderable_columns or []
5253
classes = "dataframe table table-striped table-hover"
5354
table_html_parts = [f'<table border="1" class="{classes}" id="{table_id}">']
54-
table_html_parts.append(_render_table_header(dataframe, orderable_columns))
55-
table_html_parts.append(_render_table_body(dataframe))
55+
56+
# Handle column truncation
57+
columns = list(dataframe.columns)
58+
if max_columns is not None and max_columns > 0 and len(columns) > max_columns:
59+
half = max_columns // 2
60+
left_columns = columns[:half]
61+
# Ensure we don't take more than available if half is 0 or calculation is weird,
62+
# but typical case is safe.
63+
right_count = max_columns - half
64+
right_columns = columns[-right_count:] if right_count > 0 else []
65+
show_ellipsis = True
66+
else:
67+
left_columns = columns
68+
right_columns = []
69+
show_ellipsis = False
70+
71+
table_html_parts.append(
72+
_render_table_header(
73+
dataframe, orderable_columns, left_columns, right_columns, show_ellipsis
74+
)
75+
)
76+
table_html_parts.append(
77+
_render_table_body(dataframe, left_columns, right_columns, show_ellipsis)
78+
)
5679
table_html_parts.append("</table>")
5780
return "".join(table_html_parts)
5881

5982

60-
def _render_table_header(dataframe: pd.DataFrame, orderable_columns: list[str]) -> str:
83+
def _render_table_header(
84+
dataframe: pd.DataFrame,
85+
orderable_columns: list[str],
86+
left_columns: list[Any],
87+
right_columns: list[Any],
88+
show_ellipsis: bool,
89+
) -> str:
6190
"""Render the header of the HTML table."""
6291
header_parts = [" <thead>", " <tr>"]
63-
for col in dataframe.columns:
92+
93+
def render_col_header(col):
6494
th_classes = []
6595
if col in orderable_columns:
6696
th_classes.append("sortable")
@@ -69,19 +99,38 @@ def _render_table_header(dataframe: pd.DataFrame, orderable_columns: list[str])
6999
f' <th {class_str}><div class="bf-header-content">'
70100
f"{html.escape(str(col))}</div></th>"
71101
)
102+
103+
for col in left_columns:
104+
render_col_header(col)
105+
106+
if show_ellipsis:
107+
header_parts.append(
108+
' <th><div class="bf-header-content" style="cursor: default;">...</div></th>'
109+
)
110+
111+
for col in right_columns:
112+
render_col_header(col)
113+
72114
header_parts.extend([" </tr>", " </thead>"])
73115
return "\n".join(header_parts)
74116

75117

76-
def _render_table_body(dataframe: pd.DataFrame) -> str:
118+
def _render_table_body(
119+
dataframe: pd.DataFrame,
120+
left_columns: list[Any],
121+
right_columns: list[Any],
122+
show_ellipsis: bool,
123+
) -> str:
77124
"""Render the body of the HTML table."""
78125
body_parts = [" <tbody>"]
79126
precision = options.display.precision
80127

81128
for i in range(len(dataframe)):
82129
body_parts.append(" <tr>")
83130
row = dataframe.iloc[i]
84-
for col_name, value in row.items():
131+
132+
def render_col_cell(col_name):
133+
value = row[col_name]
85134
dtype = dataframe.dtypes.loc[col_name] # type: ignore
86135
align = "right" if _is_dtype_numeric(dtype) else "left"
87136

@@ -101,6 +150,17 @@ def _render_table_body(dataframe: pd.DataFrame) -> str:
101150
f' <td class="cell-align-{align}">'
102151
f"{html.escape(cell_content)}</td>"
103152
)
153+
154+
for col in left_columns:
155+
render_col_cell(col)
156+
157+
if show_ellipsis:
158+
# Ellipsis cell
159+
body_parts.append(' <td class="cell-align-left">...</td>')
160+
161+
for col in right_columns:
162+
render_col_cell(col)
163+
104164
body_parts.append(" </tr>")
105165
body_parts.append(" </tbody>")
106166
return "\n".join(body_parts)

bigframes/display/table_widget.css

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,24 @@ body[data-theme='dark'] .bigframes-widget.bigframes-widget {
117117
margin: 0 8px;
118118
}
119119

120-
.bigframes-widget .page-size {
120+
.bigframes-widget .settings {
121121
align-items: center;
122122
display: flex;
123123
flex-direction: row;
124-
gap: 4px;
124+
gap: 16px;
125125
justify-content: end;
126126
}
127127

128-
.bigframes-widget .page-size label {
128+
.bigframes-widget .page-size,
129+
.bigframes-widget .max-columns {
130+
align-items: center;
131+
display: flex;
132+
flex-direction: row;
133+
gap: 4px;
134+
}
135+
136+
.bigframes-widget .page-size label,
137+
.bigframes-widget .max-columns label {
129138
margin-right: 8px;
130139
}
131140

bigframes/display/table_widget.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const ModelProperty = {
2222
ROW_COUNT: 'row_count',
2323
SORT_CONTEXT: 'sort_context',
2424
TABLE_HTML: 'table_html',
25+
MAX_COLUMNS: 'max_columns',
2526
};
2627

2728
const Event = {
@@ -71,6 +72,10 @@ function render({ model, el }) {
7172
attributeFilter: ['class', 'data-theme', 'data-vscode-theme-kind'],
7273
});
7374

75+
// Settings controls container
76+
const settingsContainer = document.createElement('div');
77+
settingsContainer.classList.add('settings');
78+
7479
// Pagination controls
7580
const paginationContainer = document.createElement('div');
7681
paginationContainer.classList.add('pagination');
@@ -102,6 +107,32 @@ function render({ model, el }) {
102107
pageSizeInput.appendChild(option);
103108
}
104109

110+
// Max columns controls
111+
const maxColumnsContainer = document.createElement('div');
112+
maxColumnsContainer.classList.add('max-columns');
113+
const maxColumnsLabel = document.createElement('label');
114+
const maxColumnsInput = document.createElement('select');
115+
116+
maxColumnsLabel.textContent = 'Max columns:';
117+
118+
// 0 represents "All" (all columns)
119+
const maxColumnOptions = [5, 10, 15, 20, 0];
120+
for (const cols of maxColumnOptions) {
121+
const option = document.createElement('option');
122+
option.value = cols;
123+
option.textContent = cols === 0 ? 'All' : cols;
124+
125+
const currentMax = model.get(ModelProperty.MAX_COLUMNS);
126+
// Handle None/null from python as 0/All
127+
const currentMaxVal =
128+
currentMax === null || currentMax === undefined ? 0 : currentMax;
129+
130+
if (cols === currentMaxVal) {
131+
option.selected = true;
132+
}
133+
maxColumnsInput.appendChild(option);
134+
}
135+
105136
function updateButtonStates() {
106137
const currentPage = model.get(ModelProperty.PAGE);
107138
const pageSize = model.get(ModelProperty.PAGE_SIZE);
@@ -259,6 +290,12 @@ function render({ model, el }) {
259290
}
260291
});
261292

293+
maxColumnsInput.addEventListener(Event.CHANGE, (e) => {
294+
const newVal = Number(e.target.value);
295+
model.set(ModelProperty.MAX_COLUMNS, newVal);
296+
model.save_changes();
297+
});
298+
262299
model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange);
263300
model.on(`change:${ModelProperty.ROW_COUNT}`, updateButtonStates);
264301
model.on(`change:${ModelProperty.ERROR_MESSAGE}`, handleErrorMessageChange);
@@ -270,11 +307,19 @@ function render({ model, el }) {
270307
paginationContainer.appendChild(prevPage);
271308
paginationContainer.appendChild(pageIndicator);
272309
paginationContainer.appendChild(nextPage);
310+
273311
pageSizeContainer.appendChild(pageSizeLabel);
274312
pageSizeContainer.appendChild(pageSizeInput);
313+
314+
maxColumnsContainer.appendChild(maxColumnsLabel);
315+
maxColumnsContainer.appendChild(maxColumnsInput);
316+
317+
settingsContainer.appendChild(maxColumnsContainer);
318+
settingsContainer.appendChild(pageSizeContainer);
319+
275320
footer.appendChild(rowCountLabel);
276321
footer.appendChild(paginationContainer);
277-
footer.appendChild(pageSizeContainer);
322+
footer.appendChild(settingsContainer);
278323

279324
el.appendChild(errorContainer);
280325
el.appendChild(tableContainer);

notebooks/dataframes/anywidget_mode.ipynb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"- **Rich DataFrames & Series:** Both DataFrames and Series are displayed as interactive widgets.\n",
5252
"- **Pagination:** Navigate through large datasets page by page without overwhelming the output.\n",
5353
"- **Column Sorting:** Click column headers to toggle between ascending, descending, and unsorted views. Use **Shift + Click** to sort by multiple columns.\n",
54-
"- **Column Resizing:** Drag the dividers between column headers to adjust their width."
54+
"- **Column Resizing:** Drag the dividers between column headers to adjust their width.\n",
55+
"- **Max Columns Control:** Limit the number of displayed columns to improve performance and readability for wide datasets."
5556
]
5657
},
5758
{
@@ -511,7 +512,10 @@
511512
"metadata": {},
512513
"source": [
513514
"### Adjustable Column Widths\n",
514-
"You can easily adjust the width of any column in the table. Simply hover your mouse over the vertical dividers between column headers. When the cursor changes to a resize icon, click and drag to expand or shrink the column to your desired width. This allows for better readability and customization of your table view."
515+
"You can easily adjust the width of any column in the table. Simply hover your mouse over the vertical dividers between column headers. When the cursor changes to a resize icon, click and drag to expand or shrink the column to your desired width. This allows for better readability and customization of your table view.\n",
516+
"\n",
517+
"### Control Maximum Columns\n",
518+
"You can control the number of columns displayed in the widget using the **Max columns** dropdown in the footer. This is useful for wide DataFrames where you want to focus on a subset of columns or improve rendering performance. Options include 3, 5, 7, 10, 20, or All."
515519
]
516520
},
517521
{

0 commit comments

Comments
 (0)