2323import pandas as pd
2424
2525import bigframes
26+ from bigframes .core import blocks
2627import bigframes .dataframe
2728import bigframes .display .html
2829
29- # anywidget and traitlets are optional dependencies. We don't want the import of this
30- # module to fail if they aren't installed, though. Instead, we try to limit the surface that
31- # these packages could affect. This makes unit testing easier and ensures we don't
32- # accidentally make these required packages.
30+ # anywidget and traitlets are optional dependencies. We don't want the import of
31+ # this module to fail if they aren't installed, though. Instead, we try to
32+ # limit the surface that these packages could affect. This makes unit testing
33+ # easier and ensures we don't accidentally make these required packages.
3334try :
3435 import anywidget
3536 import traitlets
4647
4748
4849class TableWidget (WIDGET_BASE ):
50+ """An interactive, paginated table widget for BigFrames DataFrames.
51+
52+ This widget provides a user-friendly way to display and navigate through
53+ large BigQuery DataFrames within a Jupyter environment.
4954 """
50- An interactive, paginated table widget for BigFrames DataFrames.
51- """
55+
56+ page = traitlets .Int (0 ).tag (sync = True )
57+ page_size = traitlets .Int (0 ).tag (sync = True )
58+ row_count = traitlets .Int (0 ).tag (sync = True )
59+ table_html = traitlets .Unicode ().tag (sync = True )
60+ _initial_load_complete = traitlets .Bool (False ).tag (sync = True )
61+ _batches : Optional [blocks .PandasBatches ] = None
62+ _error_message = traitlets .Unicode (allow_none = True , default_value = None ).tag (
63+ sync = True
64+ )
5265
5366 def __init__ (self , dataframe : bigframes .dataframe .DataFrame ):
5467 """Initialize the TableWidget.
@@ -61,10 +74,11 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
6174 "Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use TableWidget."
6275 )
6376
64- super ().__init__ ()
6577 self ._dataframe = dataframe
6678
67- # Initialize attributes that might be needed by observers FIRST
79+ super ().__init__ ()
80+
81+ # Initialize attributes that might be needed by observers first
6882 self ._table_id = str (uuid .uuid4 ())
6983 self ._all_data_loaded = False
7084 self ._batch_iter : Optional [Iterator [pd .DataFrame ]] = None
@@ -73,9 +87,6 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
7387 # respect display options for initial page size
7488 initial_page_size = bigframes .options .display .max_rows
7589
76- # Initialize data fetching attributes.
77- self ._batches = dataframe ._to_pandas_batches (page_size = initial_page_size )
78-
7990 # set traitlets properties that trigger observers
8091 self .page_size = initial_page_size
8192
@@ -84,12 +95,21 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
8495 # TODO(b/428238610): Start iterating over the result of `to_pandas_batches()`
8596 # before we get here so that the count might already be cached.
8697 # TODO(b/452747934): Allow row_count to be None and check to see if
87- # there are multiple pages and show "page 1 of many" in this case.
88- self .row_count = self ._batches .total_rows or 0
98+ # there are multiple pages and show "page 1 of many" in this case
99+ self ._reset_batches_for_new_page_size ()
100+ if self ._batches is None or self ._batches .total_rows is None :
101+ self ._error_message = "Could not determine total row count. Data might be unavailable or an error occurred."
102+ self .row_count = 0
103+ else :
104+ self .row_count = self ._batches .total_rows
89105
90106 # get the initial page
91107 self ._set_table_html ()
92108
109+ # Signals to the frontend that the initial data load is complete.
110+ # Also used as a guard to prevent observers from firing during initialization.
111+ self ._initial_load_complete = True
112+
93113 @functools .cached_property
94114 def _esm (self ):
95115 """Load JavaScript code from external file."""
@@ -100,11 +120,6 @@ def _css(self):
100120 """Load CSS code from external file."""
101121 return resources .read_text (bigframes .display , "table_widget.css" )
102122
103- page = traitlets .Int (0 ).tag (sync = True )
104- page_size = traitlets .Int (25 ).tag (sync = True )
105- row_count = traitlets .Int (0 ).tag (sync = True )
106- table_html = traitlets .Unicode ().tag (sync = True )
107-
108123 @traitlets .validate ("page" )
109124 def _validate_page (self , proposal : Dict [str , Any ]) -> int :
110125 """Validate and clamp the page number to a valid range.
@@ -171,7 +186,10 @@ def _get_next_batch(self) -> bool:
171186 def _batch_iterator (self ) -> Iterator [pd .DataFrame ]:
172187 """Lazily initializes and returns the batch iterator."""
173188 if self ._batch_iter is None :
174- self ._batch_iter = iter (self ._batches )
189+ if self ._batches is None :
190+ self ._batch_iter = iter ([])
191+ else :
192+ self ._batch_iter = iter (self ._batches )
175193 return self ._batch_iter
176194
177195 @property
@@ -181,15 +199,22 @@ def _cached_data(self) -> pd.DataFrame:
181199 return pd .DataFrame (columns = self ._dataframe .columns )
182200 return pd .concat (self ._cached_batches , ignore_index = True )
183201
184- def _reset_batches_for_new_page_size (self ):
202+ def _reset_batches_for_new_page_size (self ) -> None :
185203 """Reset the batch iterator when page size changes."""
186204 self ._batches = self ._dataframe ._to_pandas_batches (page_size = self .page_size )
205+
187206 self ._cached_batches = []
188207 self ._batch_iter = None
189208 self ._all_data_loaded = False
190209
191- def _set_table_html (self ):
210+ def _set_table_html (self ) -> None :
192211 """Sets the current html data based on the current page and page size."""
212+ if self ._error_message :
213+ self .table_html = (
214+ f"<div class='bigframes-error-message'>{ self ._error_message } </div>"
215+ )
216+ return
217+
193218 start = self .page * self .page_size
194219 end = start + self .page_size
195220
@@ -211,13 +236,17 @@ def _set_table_html(self):
211236 )
212237
213238 @traitlets .observe ("page" )
214- def _page_changed (self , _change : Dict [str , Any ]):
239+ def _page_changed (self , _change : Dict [str , Any ]) -> None :
215240 """Handler for when the page number is changed from the frontend."""
241+ if not self ._initial_load_complete :
242+ return
216243 self ._set_table_html ()
217244
218245 @traitlets .observe ("page_size" )
219- def _page_size_changed (self , _change : Dict [str , Any ]):
246+ def _page_size_changed (self , _change : Dict [str , Any ]) -> None :
220247 """Handler for when the page size is changed from the frontend."""
248+ if not self ._initial_load_complete :
249+ return
221250 # Reset the page to 0 when page size changes to avoid invalid page states
222251 self .page = 0
223252
0 commit comments