Skip to content

Commit c3fe9c3

Browse files
committed
code update
1 parent c06d8db commit c3fe9c3

File tree

6 files changed

+262
-26
lines changed

6 files changed

+262
-26
lines changed

bigframes/dataframe.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -777,20 +777,16 @@ def _repr_html_(self) -> str:
777777
return formatter.repr_query_job(self._compute_dry_run())
778778

779779
if opts.repr_mode == "anywidget":
780-
import anywidget # type: ignore
781-
782-
# create an iterator for the data batches
783-
batches = self.to_pandas_batches()
784-
785-
# get the first page result
786780
try:
787-
first_page = next(iter(batches))
788-
except StopIteration:
789-
first_page = pandas.DataFrame(columns=self.columns)
781+
from bigframes import display
790782

791-
# Instantiate and return the widget. The widget's frontend will
792-
# handle the display of the table and pagination
793-
return anywidget.AnyWidget(dataframe=first_page)
783+
return display.TableWidget(self)
784+
except AttributeError:
785+
# Fallback if anywidget is not available
786+
warnings.warn(
787+
"Anywidget mode is not available, falling back to deferred mode."
788+
)
789+
return formatter.repr_query_job(self._compute_dry_run())
794790

795791
self._cached()
796792
df = self.copy()

bigframes/display/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from __future__ import annotations
15+
16+
import warnings
17+
18+
try:
19+
import anywidget # noqa
20+
21+
from .anywidget import TableWidget # noqa
22+
23+
__all__ = ["TableWidget"]
24+
except ImportError:
25+
msg = "Anywidget mode not available as anywidget is not installed."
26+
warnings.warn(msg)
27+
__all__ = []

bigframes/display/anywidget.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import uuid
16+
17+
import anywidget # type: ignore
18+
import pandas as pd
19+
import traitlets
20+
21+
import bigframes
22+
23+
24+
class TableWidget(anywidget.AnyWidget):
25+
"""
26+
An interactive, paginated table widget for BigFrames DataFrames.
27+
"""
28+
29+
_esm = """
30+
function render({ model, el }) {
31+
const container = document.createElement('div');
32+
container.innerHTML = model.get('table_html');
33+
34+
const buttonContainer = document.createElement('div');
35+
const prevPage = document.createElement('button');
36+
const label = document.createElement('span');
37+
const nextPage = document.createElement('button');
38+
prevPage.type = 'button';
39+
nextPage.type = 'button';
40+
prevPage.textContent = 'Prev';
41+
nextPage.textContent = 'Next';
42+
43+
// update button states and label
44+
function updateButtonStates() {
45+
const totalPages = Math.ceil(model.get('row_count') / model.get('page_size'));
46+
const currentPage = model.get('page');
47+
48+
// Update label
49+
label.textContent = `Page ${currentPage + 1} of ${totalPages}`;
50+
51+
// Update button states
52+
prevPage.disabled = currentPage === 0;
53+
nextPage.disabled = currentPage >= totalPages - 1;
54+
}
55+
56+
// Initial button state setup
57+
updateButtonStates();
58+
59+
prevPage.addEventListener('click', () => {
60+
let newPage = model.get('page') - 1;
61+
if (newPage < 0) {
62+
newPage = 0;
63+
}
64+
console.log(`Setting page to ${newPage}`)
65+
model.set('page', newPage);
66+
model.save_changes();
67+
});
68+
69+
nextPage.addEventListener('click', () => {
70+
const newPage = model.get('page') + 1;
71+
console.log(`Setting page to ${newPage}`)
72+
model.set('page', newPage);
73+
model.save_changes();
74+
});
75+
76+
model.on('change:table_html', () => {
77+
container.innerHTML = model.get('table_html');
78+
updateButtonStates(); // Update button states when table changes
79+
});
80+
81+
buttonContainer.appendChild(prevPage);
82+
buttonContainer.appendChild(label);
83+
buttonContainer.appendChild(nextPage);
84+
el.appendChild(container);
85+
el.appendChild(buttonContainer);
86+
}
87+
export default { render };
88+
"""
89+
90+
page = traitlets.Int(0).tag(sync=True)
91+
page_size = traitlets.Int(25).tag(sync=True)
92+
row_count = traitlets.Int(0).tag(sync=True)
93+
table_html = traitlets.Unicode().tag(sync=True)
94+
95+
def __init__(self, dataframe):
96+
"""
97+
Initialize the TableWidget.
98+
99+
Args:
100+
dataframe: The Bigframes Dataframe to display
101+
"""
102+
super().__init__()
103+
self._dataframe = dataframe
104+
105+
# respect display options
106+
self.page_size = bigframes.options.display.max_rows
107+
108+
self._batches = dataframe.to_pandas_batches(page_size=self.page_size)
109+
self._cached_data = pd.DataFrame(columns=self._dataframe.columns)
110+
self._table_id = str(uuid.uuid4())
111+
self._all_data_loaded = False
112+
113+
# store the iterator as an instance variable
114+
self._batch_iterator = None
115+
116+
# len(dataframe) is expensive, since it will trigger a
117+
# SELECT COUNT(*) query. It is a must have however.
118+
self.row_count = len(dataframe)
119+
120+
# get the initial page
121+
self._set_table_html()
122+
123+
def _get_next_batch(self):
124+
"""Gets the next batch of data from the batches generator."""
125+
if self._all_data_loaded:
126+
return False
127+
128+
try:
129+
iterator = self._get_batch_iterator()
130+
batch = next(iterator)
131+
self._cached_data = pd.concat([self._cached_data, batch], ignore_index=True)
132+
return True
133+
except StopIteration:
134+
self._all_data_loaded = True
135+
# update row count if we loaded all data
136+
if self.row_count == 0:
137+
self.row_count = len(self._cached_data)
138+
return False
139+
except Exception as e:
140+
raise RuntimeError(f"Error during batch processing: {str(e)}") from e
141+
142+
def _get_batch_iterator(self):
143+
"""Get batch Iterator."""
144+
if self._batch_iterator is None:
145+
self._batch_iterator = iter(self._batches)
146+
return self._batch_iterator
147+
148+
def _set_table_html(self):
149+
"""Sets the current html data based on the current page and page size."""
150+
start = self.page * self.page_size
151+
end = start + self.page_size
152+
153+
# fetch more dat if the requested page is outside our cache
154+
while len(self._cached_data) < end:
155+
prev_len = len(self._cached_data)
156+
self._get_next_batch()
157+
if len(self._cached_data) == prev_len:
158+
break
159+
# Get the data fro the current page
160+
page_data = self._cached_data.iloc[start:end]
161+
162+
# Generate HTML table
163+
self.table_html = page_data.to_html(
164+
index=False,
165+
max_rows=None,
166+
table_id=f"table-{self._table_id}",
167+
classes="table table-striped table-hover",
168+
escape=False,
169+
)
170+
171+
@traitlets.observe("page")
172+
def _page_changed(self, change):
173+
"""Handler for when the page nubmer is changed from the frontend"""
174+
self._set_table_html()

notebooks/dataframes/anywidget_mode.ipynb

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 1,
5+
"execution_count": null,
66
"id": "d10bfca4",
77
"metadata": {},
88
"outputs": [],
@@ -32,7 +32,7 @@
3232
},
3333
{
3434
"cell_type": "code",
35-
"execution_count": 2,
35+
"execution_count": 1,
3636
"id": "ca22f059",
3737
"metadata": {},
3838
"outputs": [],
@@ -50,7 +50,7 @@
5050
},
5151
{
5252
"cell_type": "code",
53-
"execution_count": 3,
53+
"execution_count": 2,
5454
"id": "1bc5aaf3",
5555
"metadata": {},
5656
"outputs": [],
@@ -68,14 +68,14 @@
6868
},
6969
{
7070
"cell_type": "code",
71-
"execution_count": 4,
71+
"execution_count": 3,
7272
"id": "f289d250",
7373
"metadata": {},
7474
"outputs": [
7575
{
7676
"data": {
7777
"text/html": [
78-
"Query job 91997f19-1768-4360-afa7-4a431b3e2d22 is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:91997f19-1768-4360-afa7-4a431b3e2d22&page=queryresults\">Open Job</a>"
78+
"Query job 4d7057f3-6b68-46b2-b1e3-1dc71cb4682b is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:4d7057f3-6b68-46b2-b1e3-1dc71cb4682b&page=queryresults\">Open Job</a>"
7979
],
8080
"text/plain": [
8181
"<IPython.core.display.HTML object>"
@@ -107,21 +107,45 @@
107107
},
108108
{
109109
"cell_type": "code",
110-
"execution_count": 5,
110+
"execution_count": null,
111111
"id": "42bb02ab",
112112
"metadata": {},
113+
"outputs": [],
114+
"source": [
115+
"test_series = df[\"year\"]\n",
116+
"print(test_series)"
117+
]
118+
},
119+
{
120+
"cell_type": "markdown",
121+
"id": "7bcf1bb7",
122+
"metadata": {},
123+
"source": [
124+
"Interactive BigFrames TableWidget"
125+
]
126+
},
127+
{
128+
"cell_type": "code",
129+
"execution_count": 4,
130+
"id": "ce250157",
131+
"metadata": {},
113132
"outputs": [
114133
{
115-
"name": "stdout",
116-
"output_type": "stream",
117-
"text": [
118-
"Computation deferred. Computation will process 171.4 MB\n"
119-
]
134+
"data": {
135+
"text/html": [
136+
"Computation deferred. Computation will process 171.4 MB"
137+
],
138+
"text/plain": [
139+
"Computation deferred. Computation will process 171.4 MB"
140+
]
141+
},
142+
"execution_count": 4,
143+
"metadata": {},
144+
"output_type": "execute_result"
120145
}
121146
],
122147
"source": [
123-
"test_series = df[\"year\"]\n",
124-
"print(test_series)"
148+
"df"
125149
]
126150
}
127151
],

noxfile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ def doctest(session: nox.sessions.Session):
429429
"bigframes/core/compile/polars",
430430
"--ignore",
431431
"bigframes/testing",
432+
"--ignore",
433+
"bigframes/display/anywidget.py",
432434
),
433435
test_folder="bigframes",
434436
check_cov=True,

tests/system/small/test_progress_bar.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,22 @@ def test_repr_anywidget_dataframe(penguins_df_default_index: bf.dataframe.DataFr
174174
assert EXPECTED_DRY_RUN_MESSAGE in actual_repr
175175

176176

177-
def test_repr_anywidget_idex(penguins_df_default_index: bf.dataframe.DataFrame):
177+
def test_repr_anywidget_index(penguins_df_default_index: bf.dataframe.DataFrame):
178178
pytest.importorskip("anywidget")
179179
with bf.option_context("display.repr_mode", "anywidget"):
180180
index = penguins_df_default_index.index
181181
actual_repr = repr(index)
182182
assert EXPECTED_DRY_RUN_MESSAGE in actual_repr
183+
184+
185+
def test_repr_anywidget_pagination_buttons_initial_state(
186+
penguins_df_default_index: bf.dataframe.DataFrame,
187+
):
188+
pytest.importorskip("anywidget")
189+
with bf.option_context("display.repr_mode", "anywidget"):
190+
from bigframes.display import TableWidget
191+
192+
widget = TableWidget(penguins_df_default_index)
193+
assert widget.page == 0
194+
assert widget.page_size == bf.options.display.max_rows
195+
assert widget.row_count > 0

0 commit comments

Comments
 (0)