Skip to content

Commit 27aace4

Browse files
committed
add more testcases and demo notebook
1 parent c7d92fd commit 27aace4

File tree

4 files changed

+126
-47
lines changed

4 files changed

+126
-47
lines changed

bigframes/display/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
1415
from __future__ import annotations
1516

1617
import warnings

bigframes/display/anywidget.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Iterator
1516
import uuid
1617

1718
import anywidget # type: ignore
@@ -26,6 +27,8 @@ class TableWidget(anywidget.AnyWidget):
2627
An interactive, paginated table widget for BigFrames DataFrames.
2728
"""
2829

30+
# The _esm variable contains the JavaScript source code for the frontend
31+
# component of the widget.
2932
_esm = """
3033
function render({ model, el }) {
3134
const container = document.createElement('div');
@@ -97,20 +100,19 @@ def __init__(self, dataframe):
97100
Initialize the TableWidget.
98101
99102
Args:
100-
dataframe: The Bigframes Dataframe to display
103+
dataframe: The Bigframes Dataframe to display.
101104
"""
102105
super().__init__()
103106
self._dataframe = dataframe
104107

105108
# respect display options
106109
self.page_size = bigframes.options.display.max_rows
107110

111+
# Initialize data fetching attributes.
108112
self._batches = dataframe.to_pandas_batches(page_size=self.page_size)
109113
self._cached_data = pd.DataFrame(columns=self._dataframe.columns)
110114
self._table_id = str(uuid.uuid4())
111115
self._all_data_loaded = False
112-
113-
# store the iterator as an instance variable
114116
self._batch_iterator = None
115117

116118
# len(dataframe) is expensive, since it will trigger a
@@ -120,8 +122,13 @@ def __init__(self, dataframe):
120122
# get the initial page
121123
self._set_table_html()
122124

123-
def _get_next_batch(self):
124-
"""Gets the next batch of data from the batches generator."""
125+
def _get_next_batch(self) -> bool:
126+
"""
127+
Gets the next batch of data from the generator and appends to cache.
128+
129+
Returns:
130+
bool: True if a batch was successfully loaded, False otherwise.
131+
"""
125132
if self._all_data_loaded:
126133
return False
127134

@@ -139,8 +146,8 @@ def _get_next_batch(self):
139146
except Exception as e:
140147
raise RuntimeError(f"Error during batch processing: {str(e)}") from e
141148

142-
def _get_batch_iterator(self):
143-
"""Get batch Iterator."""
149+
def _get_batch_iterator(self) -> Iterator[pd.DataFrame]:
150+
"""Lazily initializes and returns the batch iterator."""
144151
if self._batch_iterator is None:
145152
self._batch_iterator = iter(self._batches)
146153
return self._batch_iterator
@@ -150,12 +157,10 @@ def _set_table_html(self):
150157
start = self.page * self.page_size
151158
end = start + self.page_size
152159

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)
160+
# fetch more data if the requested page is outside our cache
161+
while len(self._cached_data) < end and not self._all_data_loaded:
156162
self._get_next_batch()
157-
if len(self._cached_data) == prev_len:
158-
break
163+
159164
# Get the data fro the current page
160165
page_data = self._cached_data.iloc[start:end]
161166

@@ -170,5 +175,5 @@ def _set_table_html(self):
170175

171176
@traitlets.observe("page")
172177
def _page_changed(self, change):
173-
"""Handler for when the page nubmer is changed from the frontend"""
178+
"""Handler for when the page nubmer is changed from the frontend."""
174179
self._set_table_html()

notebooks/dataframes/anywidget_mode.ipynb

Lines changed: 107 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": null,
5+
"execution_count": 1,
66
"id": "d10bfca4",
77
"metadata": {},
88
"outputs": [],
@@ -32,7 +32,7 @@
3232
},
3333
{
3434
"cell_type": "code",
35-
"execution_count": 1,
35+
"execution_count": 2,
3636
"id": "ca22f059",
3737
"metadata": {},
3838
"outputs": [],
@@ -50,7 +50,7 @@
5050
},
5151
{
5252
"cell_type": "code",
53-
"execution_count": 2,
53+
"execution_count": 3,
5454
"id": "1bc5aaf3",
5555
"metadata": {},
5656
"outputs": [],
@@ -63,33 +63,26 @@
6363
"id": "0a354c69",
6464
"metadata": {},
6565
"source": [
66-
"Display the dataframe in anywidget mode"
66+
"Load Sample Data"
6767
]
6868
},
6969
{
7070
"cell_type": "code",
71-
"execution_count": 3,
71+
"execution_count": null,
7272
"id": "f289d250",
7373
"metadata": {},
7474
"outputs": [
7575
{
7676
"data": {
7777
"text/html": [
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>"
78+
"Query job 6fddde2a-5b5f-4920-a0b1-cb38e636bab3 is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:6fddde2a-5b5f-4920-a0b1-cb38e636bab3&page=queryresults\">Open Job</a>"
7979
],
8080
"text/plain": [
8181
"<IPython.core.display.HTML object>"
8282
]
8383
},
8484
"metadata": {},
8585
"output_type": "display_data"
86-
},
87-
{
88-
"name": "stdout",
89-
"output_type": "stream",
90-
"text": [
91-
"Computation deferred. Computation will process 171.4 MB\n"
92-
]
9386
}
9487
],
9588
"source": [
@@ -110,7 +103,18 @@
110103
"execution_count": null,
111104
"id": "42bb02ab",
112105
"metadata": {},
113-
"outputs": [],
106+
"outputs": [
107+
{
108+
"data": {
109+
"text/plain": [
110+
"Computation deferred. Computation will process 171.4 MB"
111+
]
112+
},
113+
"execution_count": 5,
114+
"metadata": {},
115+
"output_type": "execute_result"
116+
}
117+
],
114118
"source": [
115119
"test_series = df[\"year\"]\n",
116120
"print(test_series)"
@@ -121,31 +125,113 @@
121125
"id": "7bcf1bb7",
122126
"metadata": {},
123127
"source": [
124-
"Interactive BigFrames TableWidget"
128+
"Display with Pagination"
125129
]
126130
},
127131
{
128132
"cell_type": "code",
129-
"execution_count": 4,
133+
"execution_count": 6,
130134
"id": "ce250157",
131135
"metadata": {},
132136
"outputs": [
137+
{
138+
"name": "stderr",
139+
"output_type": "stream",
140+
"text": [
141+
"/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/display/anywidget.py:138: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
142+
" self._cached_data = pd.concat([self._cached_data, batch], ignore_index=True)\n",
143+
"/usr/local/google/home/shuowei/src/python-bigquery-dataframes/venv/lib/python3.10/site-packages/IPython/core/formatters.py:429: FormatterWarning: text/html formatter returned invalid type <class 'bigframes.display.anywidget.TableWidget'> (expected <class 'str'>) for object: Computation deferred. Computation will process 171.4 MB\n",
144+
" warnings.warn(\n"
145+
]
146+
},
133147
{
134148
"data": {
135-
"text/html": [
136-
"Computation deferred. Computation will process 171.4 MB"
137-
],
138149
"text/plain": [
139150
"Computation deferred. Computation will process 171.4 MB"
140151
]
141152
},
142-
"execution_count": 4,
153+
"execution_count": 6,
143154
"metadata": {},
144155
"output_type": "execute_result"
145156
}
146157
],
147158
"source": [
148-
"df"
159+
"df.head(50)"
160+
]
161+
},
162+
{
163+
"cell_type": "markdown",
164+
"id": "bb15bab6",
165+
"metadata": {},
166+
"source": [
167+
"Progarmmatic Navigation Demo"
168+
]
169+
},
170+
{
171+
"cell_type": "code",
172+
"execution_count": null,
173+
"id": "6920d49b",
174+
"metadata": {},
175+
"outputs": [],
176+
"source": [
177+
"from bigframes.display.anywidget import TableWidget\n",
178+
"import math\n",
179+
" \n",
180+
"# Create widget programmatically \n",
181+
"widget = TableWidget(df) \n",
182+
"print(f\"Total pages: {math.ceil(widget.row_count / widget.page_size)}\") \n",
183+
" \n",
184+
"# Display the widget \n",
185+
"widget"
186+
]
187+
},
188+
{
189+
"cell_type": "markdown",
190+
"id": "02cbd1be",
191+
"metadata": {},
192+
"source": [
193+
"Test Navigation Programmatically"
194+
]
195+
},
196+
{
197+
"cell_type": "code",
198+
"execution_count": null,
199+
"id": "12b68f15",
200+
"metadata": {},
201+
"outputs": [],
202+
"source": [
203+
"# Simulate button clicks programmatically \n",
204+
"print(\"Current page:\", widget.page) \n",
205+
" \n",
206+
"# Go to next page \n",
207+
"widget.page = 1 \n",
208+
"print(\"After next:\", widget.page) \n",
209+
" \n",
210+
"# Go to previous page \n",
211+
"widget.page = 0 \n",
212+
"print(\"After prev:\", widget.page)"
213+
]
214+
},
215+
{
216+
"cell_type": "markdown",
217+
"id": "9d310138",
218+
"metadata": {},
219+
"source": [
220+
"Edge Case Demonstration"
221+
]
222+
},
223+
{
224+
"cell_type": "code",
225+
"execution_count": null,
226+
"id": "a9d5d13a",
227+
"metadata": {},
228+
"outputs": [],
229+
"source": [
230+
"# Test with very small dataset \n",
231+
"small_df = df.head(5) \n",
232+
"small_widget = TableWidget(small_df) \n",
233+
"print(f\"Small dataset pages: {math.ceil(small_widget.row_count / small_widget.page_size)}\") \n",
234+
"small_widget"
149235
]
150236
}
151237
],

tests/system/small/test_progress_bar.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,3 @@ def test_repr_anywidget_index(penguins_df_default_index: bf.dataframe.DataFrame)
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)