1818
1919import dataclasses
2020import functools
21+ import logging
22+
23+ logger = logging .getLogger (__name__ )
2124import math
2225import threading
2326import uuid
@@ -58,6 +61,19 @@ class _SortState:
5861 ascending : tuple [bool , ...]
5962
6063
64+ @dataclasses .dataclass
65+ class _ExecutionResult :
66+ df_to_set : Optional [bigframes .dataframe .DataFrame ] = None
67+ orderable_cols : Optional [list [str ]] = None
68+ batches : Optional [blocks .PandasBatches ] = None
69+ batch_iter : Optional [Iterator [pd .DataFrame ]] = None
70+ cached_batches : Optional [list [pd .DataFrame ]] = None
71+ all_data_loaded : bool = False
72+ total_rows : Optional [int ] = None
73+ initial_html : Optional [str ] = None
74+ error_message : Optional [str ] = None
75+
76+
6177class TableWidget (_WIDGET_BASE ):
6278 """An interactive, paginated table widget for BigFrames DataFrames.
6379
@@ -77,8 +93,19 @@ class TableWidget(_WIDGET_BASE):
7793 _error_message = traitlets .Unicode (allow_none = True , default_value = None ).tag (
7894 sync = True
7995 )
80-
81- def __init__ (self , dataframe : bigframes .dataframe .DataFrame ):
96+ start_execution = traitlets .Bool (False ).tag (sync = True )
97+ is_deferred_mode = traitlets .Bool (False ).tag (sync = True )
98+ dry_run_info = traitlets .Unicode ("" ).tag (sync = True )
99+ ping = traitlets .Int (0 ).tag (sync = True )
100+
101+ def __init__ (
102+ self ,
103+ dataframe : (
104+ bigframes .dataframe .DataFrame
105+ | bigframes .session .deferred .DeferredBigQueryDataFrame
106+ ),
107+ dry_run_info : Optional [str ] = None ,
108+ ):
82109 """Initialize the TableWidget.
83110
84111 Args:
@@ -90,40 +117,219 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
90117 "`pip install 'bigframes[anywidget]'` to use TableWidget."
91118 )
92119
93- self ._dataframe = dataframe
120+ # Enable third-party widgets manager in Google Colab environment.
121+ try :
122+ import sys
123+
124+ if "google.colab" in sys .modules :
125+ from google .colab import output
126+
127+ output .enable_custom_widget_manager ()
128+ except Exception :
129+ pass
130+
131+ from bigframes .session import deferred
132+
133+ is_deferred = False
134+ deferred_df = None
135+ df = None
136+
137+ if isinstance (dataframe , deferred .DeferredBigQueryDataFrame ):
138+ is_deferred = True
139+ deferred_df = dataframe
140+ elif bigframes .options .display .repr_mode == "deferred" :
141+ is_deferred = True
142+ df = dataframe
143+ else :
144+ df = dataframe
94145
95146 from bigframes .core .utils import get_ipython_execution_count
96147
97148 self ._cell_execution_count = get_ipython_execution_count ()
98149
99150 super ().__init__ ()
100151
152+ self .is_deferred_mode = is_deferred
153+ self ._deferred_dataframe = deferred_df
154+ self ._dataframe = df
155+
156+ if dry_run_info :
157+ self .dry_run_info = dry_run_info
158+
101159 # Initialize attributes that might be needed by observers first
102160 self ._table_id = str (uuid .uuid4 ())
103161 self ._all_data_loaded = False
104162 self ._batch_iter : Optional [Iterator [pd .DataFrame ]] = None
105163 self ._cached_batches : list [pd .DataFrame ] = []
106164 self ._last_sort_state : Optional [_SortState ] = None
165+ self ._execution_result : Optional [_ExecutionResult ] = None
107166 # Lock to ensure only one thread at a time is updating the table HTML.
108167 self ._setting_html_lock = threading .Lock ()
109168
110169 # respect display options for initial page size
111170 initial_page_size = bigframes .options .display .max_rows
112171 initial_max_columns = bigframes .options .display .max_columns
113172
114- # set traitlets properties that trigger observers
115- # TODO(b/462525985): Investigate and improve TableWidget UX for DataFrames with a large number of columns.
116173 self .page_size = initial_page_size
117174 self .max_columns = initial_max_columns
118175
119- self .orderable_columns = self ._get_orderable_columns (dataframe )
120-
121- self ._initial_load ()
176+ if not self .is_deferred_mode :
177+ self ._initialize_from_dataframe ()
122178
123179 # Signals to the frontend that the initial data load is complete.
124180 # Also used as a guard to prevent observers from firing during initialization.
125181 self ._initial_load_complete = True
126182
183+ @traitlets .observe ("start_execution" )
184+ def _on_start_execution (self , change : dict [str , Any ]):
185+ if change ["new" ]:
186+ import asyncio
187+
188+ try :
189+ loop = asyncio .get_running_loop ()
190+ except RuntimeError :
191+ try :
192+ import tornado .ioloop # type: ignore[import-not-found]
193+
194+ loop = tornado .ioloop .IOLoop .current ().asyncio_loop # type: ignore[attr-defined]
195+ except Exception :
196+ loop = None
197+
198+ def run_execution ():
199+ try :
200+ self ._error_message = None
201+ df = None
202+ if self .is_deferred_mode :
203+ if self ._deferred_dataframe is not None :
204+ result = self ._deferred_dataframe .execute ()
205+ if isinstance (result , bigframes .series .Series ):
206+ df = result .to_frame ()
207+ elif isinstance (result , bigframes .dataframe .DataFrame ):
208+ df = result
209+ else :
210+ raise TypeError (
211+ f"Unexpected result type: { type (result )} "
212+ )
213+ elif self ._dataframe is not None :
214+ df = self ._dataframe
215+ else :
216+ df = self ._dataframe
217+
218+ if df is None :
219+ raise ValueError ("No DataFrame to execute." )
220+
221+ df_to_set = df ._prepare_display_df ()
222+ orderable_cols = self ._get_orderable_columns (df_to_set )
223+
224+ with bigframes .option_context ("display.progress_bar" , None ):
225+ batches = df_to_set .to_pandas_batches (
226+ page_size = self .page_size ,
227+ cell_execution_count = self ._cell_execution_count ,
228+ )
229+
230+ total_rows = getattr (batches , "total_rows" , None )
231+
232+ # Fetch the first batch
233+ batch_iter = iter (batches )
234+ try :
235+ initial_batch = next (batch_iter )
236+ cached_batches = [initial_batch ]
237+ all_data_loaded = False
238+ except StopIteration :
239+ initial_batch = pd .DataFrame (columns = df_to_set .columns )
240+ cached_batches = []
241+ all_data_loaded = True
242+
243+ # Render the HTML
244+ page_data = initial_batch .copy ()
245+ start = 0
246+ if df_to_set ._block .has_index :
247+ is_unnamed_single_index = (
248+ page_data .index .name is None
249+ and not isinstance (page_data .index , pd .MultiIndex )
250+ )
251+ page_data = page_data .reset_index ()
252+ if is_unnamed_single_index and "index" in page_data .columns :
253+ page_data .rename (columns = {"index" : "" }, inplace = True )
254+ else :
255+ page_data .insert (
256+ 0 , "Row" , range (start + 1 , start + len (page_data ) + 1 )
257+ )
258+
259+ initial_html = bigframes .display .html .render_html (
260+ dataframe = page_data ,
261+ table_id = f"table-{ self ._table_id } " ,
262+ orderable_columns = orderable_cols ,
263+ max_columns = self .max_columns ,
264+ )
265+
266+ self ._execution_result = _ExecutionResult (
267+ df_to_set = df_to_set ,
268+ orderable_cols = orderable_cols ,
269+ batches = batches ,
270+ batch_iter = batch_iter ,
271+ cached_batches = cached_batches ,
272+ all_data_loaded = all_data_loaded ,
273+ total_rows = total_rows ,
274+ initial_html = initial_html ,
275+ )
276+ except Exception as e :
277+ logger .warning (f"Error in background execution: { e } " )
278+ self ._execution_result = _ExecutionResult (error_message = str (e ))
279+
280+ import sys
281+
282+ is_colab = "google.colab" in sys .modules
283+
284+ if loop is not None and loop .is_running () and not is_colab :
285+ loop .call_soon_threadsafe (self ._apply_execution_result )
286+ elif is_colab :
287+ # In Google Colab, background thread updates to traitlets are not automatically
288+ # synchronized to the frontend. We rely on the frontend's active pinging
289+ # (which triggers `_on_ping` on the main kernel thread) to apply the result.
290+ pass
291+ else :
292+ self ._apply_execution_result ()
293+
294+ self ._execution_thread = threading .Thread (target = run_execution , daemon = True )
295+ self ._execution_thread .start ()
296+
297+ def _apply_execution_result (self ) -> None :
298+ if self ._execution_result is None :
299+ return
300+
301+ result = self ._execution_result
302+ self ._execution_result = None
303+
304+ with self .hold_sync ():
305+ if result .error_message is not None :
306+ self ._error_message = result .error_message
307+ self .start_execution = False
308+ else :
309+ self ._dataframe = result .df_to_set
310+ self .orderable_columns = result .orderable_cols or []
311+ self ._batches = result .batches
312+ self ._batch_iter = result .batch_iter
313+ self ._cached_batches = result .cached_batches or []
314+ self ._all_data_loaded = result .all_data_loaded
315+ self ._last_sort_state = _SortState ((), ())
316+ self .row_count = result .total_rows
317+ self .table_html = result .initial_html or ""
318+ self .is_deferred_mode = False
319+ self .start_execution = False
320+
321+ @traitlets .observe ("ping" )
322+ def _on_ping (self , _change : dict [str , Any ]):
323+ self ._apply_execution_result ()
324+
325+ def _initialize_from_dataframe (self ):
326+ if self ._dataframe is None :
327+ return
328+
329+ self .orderable_columns = self ._get_orderable_columns (self ._dataframe )
330+
331+ self ._initial_load ()
332+
127333 def _get_orderable_columns (
128334 self , dataframe : bigframes .dataframe .DataFrame
129335 ) -> list [str ]:
@@ -278,7 +484,9 @@ def _batch_iterator(self) -> Iterator[pd.DataFrame]:
278484 def _cached_data (self ) -> pd .DataFrame :
279485 """Combine all cached batches into a single DataFrame."""
280486 if not self ._cached_batches :
281- return pd .DataFrame (columns = self ._dataframe .columns )
487+ if self ._dataframe is not None :
488+ return pd .DataFrame (columns = self ._dataframe .columns )
489+ return pd .DataFrame ()
282490 return pd .concat (self ._cached_batches )
283491
284492 def _reset_batch_cache (self ) -> None :
@@ -289,6 +497,8 @@ def _reset_batch_cache(self) -> None:
289497
290498 def _reset_batches_for_new_page_size (self ) -> None :
291499 """Reset the batch iterator when page size changes."""
500+ if self ._dataframe is None :
501+ return
292502 with bigframes .option_context ("display.progress_bar" , None ):
293503 self ._batches = self ._dataframe .to_pandas_batches (
294504 page_size = self .page_size ,
@@ -299,6 +509,9 @@ def _reset_batches_for_new_page_size(self) -> None:
299509
300510 def _set_table_html (self ) -> None :
301511 """Sets the current html data based on the current page and page size."""
512+ if self .is_deferred_mode :
513+ return
514+
302515 new_page = None
303516 with (
304517 self ._setting_html_lock ,
@@ -310,6 +523,10 @@ def _set_table_html(self) -> None:
310523 )
311524 return
312525
526+ if self ._dataframe is None :
527+ self .table_html = "<div class='bigframes-error-message'>Internal Error: DataFrame is missing.</div>"
528+ return
529+
313530 # Apply sorting if a column is selected
314531 df_to_display = self ._dataframe
315532 sort_columns = [item ["column" ] for item in self .sort_context ]
0 commit comments