Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd1ecc61

Browse files
shuoweiltswast
andauthored
feat: Implement single-column sorting for interactive table widget (#2255)
This PR introduces single-column sorting functionality to theinteractive table widget.1) **Three-State Sorting UI**1.1) The sort indicator dot (●) is now hidden by default and onlyappears when the user hovers the mouse over a column header1.2) Implemented a sorting cycle: unsorted (●) → ascending (▲) →descending (▼) → unsorted (●).1.3) Visual indicators (●, ▲, ▼) are displayed in column headers toreflect the current sort state.1.4) Sorting controls are now only enabled for columns with orderabledata types.2) **Tests**2.1) Updated `paginated_pandas_df` fixture for better sorting testcoverage2.2) Added new system tests to verify ascending, descending, andmulti-column sorting.**3. Frontend Unit Tests**JavaScript-level unit tests have been added to validate the widget'sfrontend logic, specifically the new sorting functionality and UIinteractions.**How to Run Frontend Unit Tests**:To execute these tests from the project root directory:```bashcd tests/jsnpm install # Only needed if dependencies haven't been installed or have changednpm test```Docs has been updated to document the new features. The main descriptionnow mentions column sorting and adjustable widths, and a new section hasbeen added to explain how to use the column resizing feature. Thesorting section was also updated to mention that the indicators are onlyvisible on hover.Fixes #<459835971> 🦕---------Co-authored-by: Tim Sweña (Swast) <swast@google.com>
1 parent32e5313 commitd1ecc61

File tree

14 files changed

+7329
-84
lines changed

14 files changed

+7329
-84
lines changed

‎.github/workflows/js-tests.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name:js-tests
2+
on:
3+
pull_request:
4+
branches:
5+
-main
6+
push:
7+
branches:
8+
-main
9+
jobs:
10+
build:
11+
runs-on:ubuntu-latest
12+
steps:
13+
-name:Checkout
14+
uses:actions/checkout@v4
15+
-name:Install modules
16+
working-directory:./tests/js
17+
run:npm install
18+
-name:Run tests
19+
working-directory:./tests/js
20+
run:npm test

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ coverage.xml
5858

5959
# System test environment variables.
6060
system_tests/local_test_setup
61+
tests/js/node_modules/
6162

6263
# Make sure a generated file isn't accidentally committed.
6364
pylintrc

‎bigframes/display/anywidget.py‎

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__importannotations
1818

19+
importdataclasses
1920
fromimportlibimportresources
2021
importfunctools
2122
importmath
@@ -28,6 +29,7 @@
2829
frombigframes.coreimportblocks
2930
importbigframes.dataframe
3031
importbigframes.display.html
32+
importbigframes.dtypesasdtypes
3133

3234
# anywidget and traitlets are optional dependencies. We don't want the import of
3335
# this module to fail if they aren't installed, though. Instead, we try to
@@ -48,6 +50,12 @@
4850
WIDGET_BASE=object
4951

5052

53+
@dataclasses.dataclass(frozen=True)
54+
class_SortState:
55+
column:str
56+
ascending:bool
57+
58+
5159
classTableWidget(WIDGET_BASE):
5260
"""An interactive, paginated table widget for BigFrames DataFrames.
5361
@@ -63,6 +71,9 @@ class TableWidget(WIDGET_BASE):
6371
allow_none=True,
6472
).tag(sync=True)
6573
table_html=traitlets.Unicode().tag(sync=True)
74+
sort_column=traitlets.Unicode("").tag(sync=True)
75+
sort_ascending=traitlets.Bool(True).tag(sync=True)
76+
orderable_columns=traitlets.List(traitlets.Unicode(), []).tag(sync=True)
6677
_initial_load_complete=traitlets.Bool(False).tag(sync=True)
6778
_batches:Optional[blocks.PandasBatches]=None
6879
_error_message=traitlets.Unicode(allow_none=True,default_value=None).tag(
@@ -89,15 +100,25 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
89100
self._all_data_loaded=False
90101
self._batch_iter:Optional[Iterator[pd.DataFrame]]=None
91102
self._cached_batches:List[pd.DataFrame]= []
103+
self._last_sort_state:Optional[_SortState]=None
92104

93105
# respect display options for initial page size
94106
initial_page_size=bigframes.options.display.max_rows
95107

96108
# set traitlets properties that trigger observers
109+
# TODO(b/462525985): Investigate and improve TableWidget UX for DataFrames with a large number of columns.
97110
self.page_size=initial_page_size
111+
# TODO(b/463754889): Support non-string column labels for sorting.
112+
ifall(isinstance(col,str)forcolindataframe.columns):
113+
self.orderable_columns= [
114+
str(col_name)
115+
forcol_name,dtypeindataframe.dtypes.items()
116+
ifdtypes.is_orderable(dtype)
117+
]
118+
else:
119+
self.orderable_columns= []
98120

99-
# len(dataframe) is expensive, since it will trigger a
100-
# SELECT COUNT(*) query. It is a must have however.
121+
# obtain the row counts
101122
# TODO(b/428238610): Start iterating over the result of `to_pandas_batches()`
102123
# before we get here so that the count might already be cached.
103124
self._reset_batches_for_new_page_size()
@@ -121,6 +142,11 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
121142
# Also used as a guard to prevent observers from firing during initialization.
122143
self._initial_load_complete=True
123144

145+
@traitlets.observe("_initial_load_complete")
146+
def_on_initial_load_complete(self,change:Dict[str,Any]):
147+
ifchange["new"]:
148+
self._set_table_html()
149+
124150
@functools.cached_property
125151
def_esm(self):
126152
"""Load JavaScript code from external file."""
@@ -221,13 +247,17 @@ def _cached_data(self) -> pd.DataFrame:
221247
returnpd.DataFrame(columns=self._dataframe.columns)
222248
returnpd.concat(self._cached_batches,ignore_index=True)
223249

250+
def_reset_batch_cache(self)->None:
251+
"""Resets batch caching attributes."""
252+
self._cached_batches= []
253+
self._batch_iter=None
254+
self._all_data_loaded=False
255+
224256
def_reset_batches_for_new_page_size(self)->None:
225257
"""Reset the batch iterator when page size changes."""
226258
self._batches=self._dataframe._to_pandas_batches(page_size=self.page_size)
227259

228-
self._cached_batches= []
229-
self._batch_iter=None
230-
self._all_data_loaded=False
260+
self._reset_batch_cache()
231261

232262
def_set_table_html(self)->None:
233263
"""Sets the current html data based on the current page and page size."""
@@ -237,6 +267,21 @@ def _set_table_html(self) -> None:
237267
)
238268
return
239269

270+
# Apply sorting if a column is selected
271+
df_to_display=self._dataframe
272+
ifself.sort_column:
273+
# TODO(b/463715504): Support sorting by index columns.
274+
df_to_display=df_to_display.sort_values(
275+
by=self.sort_column,ascending=self.sort_ascending
276+
)
277+
278+
# Reset batches when sorting changes
279+
ifself._last_sort_state!=_SortState(self.sort_column,self.sort_ascending):
280+
self._batches=df_to_display._to_pandas_batches(page_size=self.page_size)
281+
self._reset_batch_cache()
282+
self._last_sort_state=_SortState(self.sort_column,self.sort_ascending)
283+
self.page=0# Reset to first page
284+
240285
start=self.page*self.page_size
241286
end=start+self.page_size
242287

@@ -272,8 +317,14 @@ def _set_table_html(self) -> None:
272317
self.table_html=bigframes.display.html.render_html(
273318
dataframe=page_data,
274319
table_id=f"table-{self._table_id}",
320+
orderable_columns=self.orderable_columns,
275321
)
276322

323+
@traitlets.observe("sort_column","sort_ascending")
324+
def_sort_changed(self,_change:Dict[str,Any]):
325+
"""Handler for when sorting parameters change from the frontend."""
326+
self._set_table_html()
327+
277328
@traitlets.observe("page")
278329
def_page_changed(self,_change:Dict[str,Any])->None:
279330
"""Handler for when the page number is changed from the frontend."""
@@ -288,6 +339,9 @@ def _page_size_changed(self, _change: Dict[str, Any]) -> None:
288339
return
289340
# Reset the page to 0 when page size changes to avoid invalid page states
290341
self.page=0
342+
# Reset the sort state to default (no sort)
343+
self.sort_column=""
344+
self.sort_ascending=True
291345

292346
# Reset batches to use new page size for future data fetching
293347
self._reset_batches_for_new_page_size()

‎bigframes/display/html.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
from __future__importannotations
1818

1919
importhtml
20+
fromtypingimportAny
2021

2122
importpandasaspd
2223
importpandas.api.types
2324

2425
frombigframes._configimportoptions
2526

2627

27-
def_is_dtype_numeric(dtype)->bool:
28+
def_is_dtype_numeric(dtype:Any)->bool:
2829
"""Check if a dtype is numeric for alignment purposes."""
2930
returnpandas.api.types.is_numeric_dtype(dtype)
3031

@@ -33,18 +34,31 @@ def render_html(
3334
*,
3435
dataframe:pd.DataFrame,
3536
table_id:str,
37+
orderable_columns:list[str]|None=None,
3638
)->str:
3739
"""Render a pandas DataFrame to HTML with specific styling."""
3840
classes="dataframe table table-striped table-hover"
3941
table_html= [f'<table border="1" class="{classes}" id="{table_id}">']
4042
precision=options.display.precision
43+
orderable_columns=orderable_columnsor []
4144

4245
# Render table head
4346
table_html.append(" <thead>")
4447
table_html.append(' <tr style="text-align: left;">')
4548
forcolindataframe.columns:
49+
th_classes= []
50+
ifcolinorderable_columns:
51+
th_classes.append("sortable")
52+
class_str=f'class="{" ".join(th_classes)}"'ifth_classeselse""
53+
header_div= (
54+
'<div style="resize: horizontal; overflow: auto; '
55+
"box-sizing: border-box; width: 100%; height: 100%; "
56+
'padding: 0.5em;">'
57+
f"{html.escape(str(col))}"
58+
"</div>"
59+
)
4660
table_html.append(
47-
f' <th><div>{html.escape(str(col))}</div></th>'
61+
f' <th{class_str}>{header_div}</th>'
4862
)
4963
table_html.append(" </tr>")
5064
table_html.append(" </thead>")

‎bigframes/display/table_widget.css‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
align-items: center;
2929
display: flex;
3030
font-size:0.8rem;
31-
padding-top:8px;
31+
justify-content: space-between;
32+
padding:8px;
33+
font-family:
34+
-apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, sans-serif;
3235
}
3336

3437
.bigframes-widget .footer>* {
@@ -44,6 +47,14 @@
4447
padding:4px;
4548
}
4649

50+
.bigframes-widget .page-indicator {
51+
margin:08px;
52+
}
53+
54+
.bigframes-widget .row-count {
55+
margin:08px;
56+
}
57+
4758
.bigframes-widget .page-size {
4859
align-items: center;
4960
display: flex;
@@ -52,19 +63,31 @@
5263
justify-content: end;
5364
}
5465

66+
.bigframes-widget .page-sizelabel {
67+
margin-right:8px;
68+
}
69+
5570
.bigframes-widgettable {
5671
border-collapse: collapse;
5772
text-align: left;
5873
}
5974

6075
.bigframes-widgetth {
6176
background-color:var(--colab-primary-surface-color,var(--jp-layout-color0));
62-
/* Uncomment once we support sorting: cursor: pointer; */
6377
position: sticky;
6478
top:0;
6579
z-index:1;
6680
}
6781

82+
.bigframes-widgetth .sort-indicator {
83+
padding-left:4px;
84+
visibility: hidden;
85+
}
86+
87+
.bigframes-widgetth:hover .sort-indicator {
88+
visibility: visible;
89+
}
90+
6891
.bigframes-widgetbutton {
6992
cursor: pointer;
7093
display: inline-block;
@@ -78,3 +101,14 @@
78101
opacity:0.65;
79102
pointer-events: none;
80103
}
104+
105+
.bigframes-widget .error-message {
106+
font-family:
107+
-apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, sans-serif;
108+
font-size:14px;
109+
padding:8px;
110+
margin-bottom:8px;
111+
border:1px solid red;
112+
border-radius:4px;
113+
background-color:#ffebee;
114+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp