Skip to content

Commit 08f391b

Browse files
committed
Beautify buttons and tables
1 parent 92a2377 commit 08f391b

File tree

4 files changed

+123
-19
lines changed

4 files changed

+123
-19
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ repos:
4646
rev: v2.0.2
4747
hooks:
4848
- id: biome-check
49-
files: '\.js$'
49+
files: '\.(js|css)$'

bigframes/display/anywidget.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
6262
super().__init__()
6363
self._dataframe = dataframe
6464

65-
# respect display options
66-
self.page_size = bigframes.options.display.max_rows
65+
# respect display options for initial page size
66+
initial_page_size = bigframes.options.display.max_rows
67+
self.page_size = initial_page_size
6768

6869
# Initialize data fetching attributes.
6970
self._batches = dataframe.to_pandas_batches(page_size=self.page_size)
@@ -91,6 +92,11 @@ def _esm(self):
9192
"""Load JavaScript code from external file."""
9293
return resources.read_text(bigframes.display, "table_widget.js")
9394

95+
@functools.cached_property
96+
def _css(self):
97+
"""Load CSS code from external file."""
98+
return resources.read_text(bigframes.display, "table_widget.css")
99+
94100
page = traitlets.Int(0).tag(sync=True)
95101
page_size = traitlets.Int(25).tag(sync=True)
96102
row_count = traitlets.Int(0).tag(sync=True)
@@ -115,6 +121,23 @@ def _validate_page(self, proposal: Dict[str, Any]):
115121
# Clamp the proposed value to the valid range [0, max_page].
116122
return max(0, min(value, max_page))
117123

124+
@traitlets.validate("page_size")
125+
def _validate_page_size(self, proposal: Dict[str, Any]):
126+
"""Validate page size to ensure it's positive and reasonable.
127+
Args:
128+
proposal: A dictionary from the traitlets library containing the
129+
proposed change. The new value is in proposal["value"].
130+
"""
131+
value = proposal["value"]
132+
133+
# Ensure page size is positive and within reasonable bounds
134+
if value <= 0:
135+
return self.page_size # Keep current value
136+
137+
# Cap at reasonable maximum to prevent performance issues
138+
max_page_size = 1000
139+
return min(value, max_page_size)
140+
118141
def _get_next_batch(self) -> bool:
119142
"""
120143
Gets the next batch of data from the generator and appends to cache.
@@ -148,6 +171,13 @@ def _cached_data(self) -> pd.DataFrame:
148171
return pd.DataFrame(columns=self._dataframe.columns)
149172
return pd.concat(self._cached_batches, ignore_index=True)
150173

174+
def _reset_batches_for_new_page_size(self):
175+
"""Reset the batch iterator when page size changes."""
176+
self._batches = self._dataframe.to_pandas_batches(page_size=self.page_size)
177+
self._cached_batches = []
178+
self._batch_iter = None
179+
self._all_data_loaded = False
180+
151181
def _set_table_html(self):
152182
"""Sets the current html data based on the current page and page size."""
153183
start = self.page * self.page_size
@@ -174,6 +204,18 @@ def _set_table_html(self):
174204
)
175205

176206
@traitlets.observe("page")
177-
def _page_changed(self, change):
207+
def _page_changed(self, _change: Dict[str, Any]):
178208
"""Handler for when the page number is changed from the frontend."""
179209
self._set_table_html()
210+
211+
@traitlets.observe("page_size")
212+
def _page_size_changed(self, _change: Dict[str, Any]):
213+
"""Handler for when the page size is changed from the frontend."""
214+
# Reset the page to 0 when page size changes to avoid invalid page states
215+
self.page = 0
216+
217+
# Reset batches to use new page size for future data fetching
218+
self._reset_batches_for_new_page_size()
219+
220+
# Update the table display
221+
self._set_table_html()

bigframes/display/table_widget.js

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const ModelProperty = {
2222
};
2323

2424
const Event = {
25+
CHANGE: "change",
2526
CHANGE_TABLE_HTML: `change:${ModelProperty.TABLE_HTML}`,
2627
CLICK: "click",
2728
};
@@ -34,29 +35,59 @@ const Event = {
3435
* }} options
3536
*/
3637
function render({ model, el }) {
38+
// Structure
3739
const container = document.createElement("div");
38-
container.innerHTML = model.get(ModelProperty.TABLE_HTML);
39-
40-
const buttonContainer = document.createElement("div");
40+
const tableContainer = document.createElement("div");
41+
const footer = document.createElement("div");
42+
// Total rows label
43+
const rowCountLabel = document.createElement("div");
44+
// Pagination controls
45+
const paginationContainer = document.createElement("div");
4146
const prevPage = document.createElement("button");
42-
const label = document.createElement("span");
47+
const paginationLabel = document.createElement("span");
4348
const nextPage = document.createElement("button");
49+
// Page size controls
50+
const pageSizeContainer = document.createElement("div");
51+
const pageSizeLabel = document.createElement("label");
52+
const pageSizeSelect = document.createElement("select");
53+
54+
tableContainer.classList.add("table-container");
55+
footer.classList.add("footer");
56+
paginationContainer.classList.add("pagination");
57+
pageSizeContainer.classList.add("page-size");
4458

4559
prevPage.type = "button";
4660
nextPage.type = "button";
4761
prevPage.textContent = "Prev";
4862
nextPage.textContent = "Next";
4963

64+
pageSizeLabel.textContent = "Page Size";
65+
for (const size of [10, 25, 50, 100]) {
66+
const option = document.createElement("option");
67+
option.value = size;
68+
option.textContent = size;
69+
if (size === model.get(ModelProperty.PAGE_SIZE)) {
70+
option.selected = true;
71+
}
72+
pageSizeSelect.appendChild(option);
73+
}
74+
5075
/** Updates the button states and page label based on the model. */
5176
function updateButtonStates() {
77+
const rowCount = model.get(ModelProperty.ROW_COUNT);
78+
rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`;
79+
5280
const totalPages = Math.ceil(
5381
model.get(ModelProperty.ROW_COUNT) / model.get(ModelProperty.PAGE_SIZE),
5482
);
5583
const currentPage = model.get(ModelProperty.PAGE);
5684

57-
label.textContent = `Page ${currentPage + 1} of ${totalPages}`;
85+
paginationLabel.textContent = `Page ${currentPage + 1} of ${totalPages}`;
5886
prevPage.disabled = currentPage === 0;
5987
nextPage.disabled = currentPage >= totalPages - 1;
88+
89+
// Update page size selector
90+
pageSizeSelect.value = model.get(ModelProperty.PAGE_SIZE);
6091
}
6192

6293
/**
@@ -72,24 +103,48 @@ function render({ model, el }) {
72103
}
73104
}
74105

75-
prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1));
76-
nextPage.addEventListener(Event.CLICK, () => handlePageChange(1));
106+
/** Handles the page_size in the model.
107+
* @param {number} size - new size to set
108+
*/
109+
function handlePageSizeChange(size) {
110+
const currentSize = model.get(ModelProperty.PAGE_SIZE);
111+
if (size !== currentSize) {
112+
model.set(ModelProperty.PAGE_SIZE, size);
113+
model.save_changes();
114+
}
115+
}
77116

78-
model.on(Event.CHANGE_TABLE_HTML, () => {
117+
/** Updates the HTML in the table container **/
118+
function handleTableHTMLChange() {
79119
// Note: Using innerHTML can be a security risk if the content is
80120
// user-generated. Ensure 'table_html' is properly sanitized.
81-
container.innerHTML = model.get(ModelProperty.TABLE_HTML);
121+
tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML);
82122
updateButtonStates();
123+
}
124+
125+
prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1));
126+
nextPage.addEventListener(Event.CLICK, () => handlePageChange(1));
127+
pageSizeSelect.addEventListener(Event.CHANGE, (e) => {
128+
const newSize = Number(e.target.value);
129+
if (newSize) {
130+
handlePageSizeChange(newSize);
131+
}
83132
});
133+
model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange);
84134

85135
// Initial setup
86-
updateButtonStates();
87-
88-
buttonContainer.appendChild(prevPage);
89-
buttonContainer.appendChild(label);
90-
buttonContainer.appendChild(nextPage);
136+
paginationContainer.appendChild(prevPage);
137+
paginationContainer.appendChild(paginationLabel);
138+
paginationContainer.appendChild(nextPage);
139+
pageSizeContainer.appendChild(pageSizeLabel);
140+
pageSizeContainer.appendChild(pageSizeSelect);
141+
footer.appendChild(rowCountLabel);
142+
footer.appendChild(paginationContainer);
143+
footer.appendChild(pageSizeContainer);
144+
container.appendChild(tableContainer);
145+
container.appendChild(footer);
91146
el.appendChild(container);
92-
el.appendChild(buttonContainer);
147+
handleTableHTMLChange();
93148
}
94149

95150
export default { render };

owlbot.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@
107107
"recursive-include bigframes *.json *.proto *.js py.typed",
108108
)
109109

110+
# Include JavaScript and CSS files for display widgets
111+
assert 1 == s.replace( # MANIFEST.in
112+
["MANIFEST.in"],
113+
re.escape("recursive-include bigframes *.json *.proto *.js py.typed"),
114+
"recursive-include bigframes *.json *.proto *.js *.css py.typed",
115+
)
116+
110117
# Fixup the documentation.
111118
assert 1 == s.replace( # docs/conf.py
112119
["docs/conf.py"],

0 commit comments

Comments
 (0)