Skip to content

Commit a31771a

Browse files
committed
update error handling and introduce three stages for sort
1 parent 75174e3 commit a31771a

File tree

5 files changed

+356
-47
lines changed

5 files changed

+356
-47
lines changed

bigframes/display/anywidget.py

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

1717
from importlib import resources
1818
import functools
19-
import logging
2019
import math
2120
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type
2221
import uuid
@@ -27,6 +26,7 @@
2726
from bigframes.core import blocks
2827
import bigframes.dataframe
2928
import bigframes.display.html
29+
import bigframes.dtypes as dtypes
3030

3131
# anywidget and traitlets are optional dependencies. We don't want the import of
3232
# this module to fail if they aren't installed, though. Instead, we try to
@@ -60,6 +60,7 @@ class TableWidget(WIDGET_BASE):
6060
table_html = traitlets.Unicode().tag(sync=True)
6161
sort_column = traitlets.Unicode("").tag(sync=True)
6262
sort_ascending = traitlets.Bool(True).tag(sync=True)
63+
orderable_columns = traitlets.List(traitlets.Unicode(), []).tag(sync=True)
6364
_initial_load_complete = traitlets.Bool(False).tag(sync=True)
6465
_batches: Optional[blocks.PandasBatches] = None
6566
_error_message = traitlets.Unicode(allow_none=True, default_value=None).tag(
@@ -93,9 +94,13 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
9394

9495
# set traitlets properties that trigger observers
9596
self.page_size = initial_page_size
97+
self.orderable_columns = [
98+
col
99+
for col in dataframe.columns
100+
if dtypes.is_orderable(dataframe.dtypes[col])
101+
]
96102

97-
# len(dataframe) is expensive, since it will trigger a
98-
# SELECT COUNT(*) query. It is a must have however.
103+
# obtain the row counts
99104
# TODO(b/428238610): Start iterating over the result of `to_pandas_batches()`
100105
# before we get here so that the count might already be cached.
101106
# TODO(b/452747934): Allow row_count to be None and check to see if
@@ -108,6 +113,7 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
108113
self.row_count = self._batches.total_rows
109114

110115
# get the initial page
116+
self._get_next_batch()
111117
self._set_table_html()
112118

113119
# Signals to the frontend that the initial data load is complete.
@@ -227,9 +233,9 @@ def _set_table_html(self) -> None:
227233
by=self.sort_column, ascending=self.sort_ascending
228234
)
229235
except KeyError:
230-
logging.warning(
231-
f"Attempted to sort by unknown column: {self.sort_column}"
232-
)
236+
self._error_message = f"Column '{self.sort_column}' not found. Please select a valid column to sort by."
237+
# Revert to unsorted state if sorting fails
238+
self.sort_column = ""
233239

234240
# Reset batches when sorting changes
235241
if self._last_sort_state != (self.sort_column, self.sort_ascending):
@@ -258,6 +264,7 @@ def _set_table_html(self) -> None:
258264
self.table_html = bigframes.display.html.render_html(
259265
dataframe=page_data,
260266
table_id=f"table-{self._table_id}",
267+
orderable_columns=self.orderable_columns,
261268
)
262269

263270
@traitlets.observe("sort_column", "sort_ascending")

bigframes/display/html.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
from __future__ import annotations
1818

1919
import html
20+
from typing import Any
2021

2122
import pandas as pd
2223
import pandas.api.types
2324

2425
from bigframes._config import options
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
return pandas.api.types.is_numeric_dtype(dtype)
3031

@@ -33,18 +34,24 @@ 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_columns or []
4144

4245
# Render table head
4346
table_html.append(" <thead>")
4447
table_html.append(' <tr style="text-align: left;">')
4548
for col in dataframe.columns:
49+
th_classes = []
50+
if col in orderable_columns:
51+
th_classes.append("sortable")
52+
class_str = f'class="{" ".join(th_classes)}"' if th_classes else ""
4653
table_html.append(
47-
f' <th style="text-align: left;"><div style="resize: horizontal; overflow: auto; box-sizing: border-box; width: 100%; height: 100%; padding: 0.5em;">{html.escape(str(col))}</div></th>'
54+
f' <th style="text-align: left;" {class_str}><div style="resize: horizontal; overflow: auto; box-sizing: border-box; width: 100%; height: 100%; padding: 0.5em;">{html.escape(str(col))}</div></th>'
4855
)
4956
table_html.append(" </tr>")
5057
table_html.append(" </thead>")

bigframes/display/table_widget.css

Lines changed: 7 additions & 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-
cursor: pointer;
62+
cursor: default; /* Default cursor for non-sortable columns */
6363
position: sticky;
6464
top: 0;
6565
z-index: 1;
@@ -78,3 +78,9 @@
7878
opacity: 0.65;
7979
pointer-events: none;
8080
}
81+
82+
.bigframes-widget .error-message {
83+
font-family:
84+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
85+
font-size: 14px;
86+
}

bigframes/display/table_widget.js

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const ModelProperty = {
2121
TABLE_HTML: "table_html",
2222
SORT_COLUMN: "sort_column",
2323
SORT_ASCENDING: "sort_ascending",
24+
ERROR_MESSAGE: "error_message",
25+
ORDERABLE_COLUMNS: "orderable_columns",
2426
};
2527

2628
const Event = {
@@ -40,7 +42,17 @@ function render({ model, el }) {
4042
// Main container with a unique class for CSS scoping
4143
el.classList.add("bigframes-widget");
4244

43-
// Structure
45+
// Add error message container at the top
46+
const errorContainer = document.createElement("div");
47+
errorContainer.classList.add("error-message");
48+
errorContainer.style.display = "none";
49+
errorContainer.style.color = "red";
50+
errorContainer.style.padding = "8px";
51+
errorContainer.style.marginBottom = "8px";
52+
errorContainer.style.border = "1px solid red";
53+
errorContainer.style.borderRadius = "4px";
54+
errorContainer.style.backgroundColor = "#ffebee";
55+
4456
const tableContainer = document.createElement("div");
4557
const footer = document.createElement("div");
4658

@@ -121,44 +133,65 @@ function render({ model, el }) {
121133
}
122134
}
123135

124-
/** Updates the HTML in the table container and refreshes button states. */
125136
function handleTableHTMLChange() {
126-
// Note: Using innerHTML is safe here because the content is generated
127-
// by a trusted backend (DataFrame.to_html).
128137
tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML);
129138

139+
// Get sortable columns from backend
140+
const sortableColumns = model.get(ModelProperty.ORDERABLE_COLUMNS);
141+
const currentSortColumn = model.get(ModelProperty.SORT_COLUMN);
142+
const currentSortAscending = model.get(ModelProperty.SORT_ASCENDING);
143+
130144
// Add click handlers to column headers for sorting
131145
const headers = tableContainer.querySelectorAll("th");
132146
headers.forEach((header) => {
133-
const columnName = header.textContent.trim();
134-
if (columnName) {
147+
const columnName = header.querySelector("div").textContent.trim();
148+
149+
// Only add sorting UI for sortable columns
150+
if (columnName && sortableColumns.includes(columnName)) {
135151
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);
139152

153+
// Determine sort indicator
154+
let indicator = " ●"; // Default: unsorted (dot)
155+
if (currentSortColumn === columnName) {
156+
indicator = currentSortAscending ? " ▲" : " ▼";
157+
}
158+
header.textContent = columnName + indicator;
159+
160+
// Add click handler for three-state toggle
161+
header.addEventListener(Event.CLICK, () => {
140162
if (currentSortColumn === columnName) {
141-
// Toggle sort direction
142-
model.set(ModelProperty.SORT_ASCENDING, !currentSortAscending);
163+
if (currentSortAscending) {
164+
// Currently ascending → switch to descending
165+
model.set(ModelProperty.SORT_ASCENDING, false);
166+
} else {
167+
// Currently descending → clear sort (back to unsorted)
168+
model.set(ModelProperty.SORT_COLUMN, "");
169+
model.set(ModelProperty.SORT_ASCENDING, true);
170+
}
143171
} else {
144-
// New column, default to ascending
172+
// Not currently sorted → sort ascending
145173
model.set(ModelProperty.SORT_COLUMN, columnName);
146174
model.set(ModelProperty.SORT_ASCENDING, true);
147175
}
148176
model.save_changes();
149177
});
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-
}
156178
}
157179
});
158180

159181
updateButtonStates();
160182
}
161183

184+
// Add error message handler
185+
function handleErrorMessageChange() {
186+
const errorMsg = model.get(ModelProperty.ERROR_MESSAGE);
187+
if (errorMsg) {
188+
errorContainer.textContent = errorMsg;
189+
errorContainer.style.display = "block";
190+
} else {
191+
errorContainer.style.display = "none";
192+
}
193+
}
194+
162195
// Add event listeners
163196
prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1));
164197
nextPage.addEventListener(Event.CLICK, () => handlePageChange(1));
@@ -170,6 +203,7 @@ function render({ model, el }) {
170203
});
171204
model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange);
172205
model.on(`change:${ModelProperty.ROW_COUNT}`, updateButtonStates);
206+
model.on(`change:${ModelProperty.ERROR_MESSAGE}`, handleErrorMessageChange);
173207
model.on(`change:_initial_load_complete`, (val) => {
174208
if (val) {
175209
updateButtonStates();
@@ -188,11 +222,13 @@ function render({ model, el }) {
188222
footer.appendChild(paginationContainer);
189223
footer.appendChild(pageSizeContainer);
190224

225+
el.appendChild(errorContainer);
191226
el.appendChild(tableContainer);
192227
el.appendChild(footer);
193228

194229
// Initial render
195230
handleTableHTMLChange();
231+
handleErrorMessageChange();
196232
}
197233

198234
export default { render };

0 commit comments

Comments
 (0)