Skip to content

Commit 65d90c2

Browse files
committed
feat: Implement column sorting for interactive table widget
1 parent 64995d6 commit 65d90c2

File tree

5 files changed

+192
-257
lines changed

5 files changed

+192
-257
lines changed

bigframes/display/anywidget.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
from importlib import resources
1818
import functools
19+
import logging
1920
import math
20-
from typing import Any, Dict, Iterator, List, Optional, Type
21+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type
2122
import uuid
2223

2324
import pandas as pd
@@ -57,6 +58,8 @@ class TableWidget(WIDGET_BASE):
5758
page_size = traitlets.Int(0).tag(sync=True)
5859
row_count = traitlets.Int(0).tag(sync=True)
5960
table_html = traitlets.Unicode().tag(sync=True)
61+
sort_column = traitlets.Unicode("").tag(sync=True)
62+
sort_ascending = traitlets.Bool(True).tag(sync=True)
6063
_initial_load_complete = traitlets.Bool(False).tag(sync=True)
6164
_batches: Optional[blocks.PandasBatches] = None
6265
_error_message = traitlets.Unicode(allow_none=True, default_value=None).tag(
@@ -83,6 +86,7 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
8386
self._all_data_loaded = False
8487
self._batch_iter: Optional[Iterator[pd.DataFrame]] = None
8588
self._cached_batches: List[pd.DataFrame] = []
89+
self._last_sort_state: Optional[Tuple[str, bool]] = None
8690

8791
# respect display options for initial page size
8892
initial_page_size = bigframes.options.display.max_rows
@@ -215,6 +219,27 @@ def _set_table_html(self) -> None:
215219
)
216220
return
217221

222+
# Apply sorting if a column is selected
223+
df_to_display = self._dataframe
224+
if self.sort_column:
225+
try:
226+
df_to_display = df_to_display.sort_values(
227+
by=self.sort_column, ascending=self.sort_ascending
228+
)
229+
except KeyError:
230+
logging.warning(
231+
f"Attempted to sort by unknown column: {self.sort_column}"
232+
)
233+
234+
# Reset batches when sorting changes
235+
if self._last_sort_state != (self.sort_column, self.sort_ascending):
236+
self._batches = df_to_display._to_pandas_batches(page_size=self.page_size)
237+
self._cached_batches = []
238+
self._batch_iter = None
239+
self._all_data_loaded = False
240+
self._last_sort_state = (self.sort_column, self.sort_ascending)
241+
self.page = 0 # Reset to first page
242+
218243
start = self.page * self.page_size
219244
end = start + self.page_size
220245

@@ -235,6 +260,11 @@ def _set_table_html(self) -> None:
235260
table_id=f"table-{self._table_id}",
236261
)
237262

263+
@traitlets.observe("sort_column", "sort_ascending")
264+
def _sort_changed(self, _change: Dict[str, Any]):
265+
"""Handler for when sorting parameters change from the frontend."""
266+
self._set_table_html()
267+
238268
@traitlets.observe("page")
239269
def _page_changed(self, _change: Dict[str, Any]) -> None:
240270
"""Handler for when the page number is changed from the frontend."""

bigframes/display/table_widget.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959

6060
.bigframes-widget th {
6161
background-color: var(--colab-primary-surface-color, var(--jp-layout-color0));
62-
/* Uncomment once we support sorting: cursor: pointer; */
62+
cursor: pointer;
6363
position: sticky;
6464
top: 0;
6565
z-index: 1;

bigframes/display/table_widget.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const ModelProperty = {
1919
PAGE_SIZE: "page_size",
2020
ROW_COUNT: "row_count",
2121
TABLE_HTML: "table_html",
22+
SORT_COLUMN: "sort_column",
23+
SORT_ASCENDING: "sort_ascending",
2224
};
2325

2426
const Event = {
@@ -124,6 +126,36 @@ function render({ model, el }) {
124126
// Note: Using innerHTML is safe here because the content is generated
125127
// by a trusted backend (DataFrame.to_html).
126128
tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML);
129+
130+
// Add click handlers to column headers for sorting
131+
const headers = tableContainer.querySelectorAll("th");
132+
headers.forEach((header) => {
133+
const columnName = header.textContent.trim();
134+
if (columnName) {
135+
header.style.cursor = "pointer";
136+
header.addEventListener(Event.CLICK, () => {
137+
const currentSortColumn = model.get(ModelProperty.SORT_COLUMN);
138+
const currentSortAscending = model.get(ModelProperty.SORT_ASCENDING);
139+
140+
if (currentSortColumn === columnName) {
141+
// Toggle sort direction
142+
model.set(ModelProperty.SORT_ASCENDING, !currentSortAscending);
143+
} else {
144+
// New column, default to ascending
145+
model.set(ModelProperty.SORT_COLUMN, columnName);
146+
model.set(ModelProperty.SORT_ASCENDING, true);
147+
}
148+
model.save_changes();
149+
});
150+
151+
// Add visual indicator for sorted column
152+
if (model.get(ModelProperty.SORT_COLUMN) === columnName) {
153+
const arrow = model.get(ModelProperty.SORT_ASCENDING) ? " ▲" : " ▼";
154+
header.textContent = columnName + arrow;
155+
}
156+
}
157+
});
158+
127159
updateButtonStates();
128160
}
129161

0 commit comments

Comments
 (0)