Skip to content

Commit 853240d

Browse files
authored
feat: Disable progress bars in Anywidget mode to reduce notebook clutter (#2437)
This PR improves the user experience of the interactive Anywidget display mode by automatically disabling progress bars and job logging during widget operations. Changes: - Wrapped the get_anywidget_bundle call in repr_mimebundle with option_context("display.progress_bar", None) to silence the initial widget load. - Wrapped TableWidget._initial_load and TableWidget._set_table_html methods to silence subsequent interactions like pagination and sorting. Motivation: When interacting with the TableWidget (paging, sorting), the repeated appearance of progress bars creates visual noise and clutter in the notebook output cell, distracting from the interactive data exploration experience. This change ensures a clean and seamless interface. Verified at: - vs code notebook: https://screencast.googleplex.com/cast/NTY2NDkzOTc4Mjk2MzIwMHwwYjU0Njc1MS03MA - colab notebook: https://screencast.googleplex.com/cast/NjIzMjM0NTEyNzQxOTkwNHxiMDM0YzM2Ni1iZQ Fixes #<482120359> 🦕
1 parent 2d973b5 commit 853240d

8 files changed

Lines changed: 195 additions & 253 deletions

File tree

‎bigframes/core/compile/polars/compiler.py‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
importdataclasses
1717
importfunctools
1818
importitertools
19+
importjson
1920
fromtypingimportcast, Literal, Optional, Sequence, Tuple, Type, TYPE_CHECKING
2021

2122
importpandasaspd
@@ -429,7 +430,68 @@ def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
429430
@compile_op.register(json_ops.JSONDecode)
430431
def_(self, op: ops.ScalarOp, input: pl.Expr) ->pl.Expr:
431432
assertisinstance(op, json_ops.JSONDecode)
432-
returninput.str.json_decode(_DTYPE_MAPPING[op.to_type])
433+
target_dtype=_bigframes_dtype_to_polars_dtype(op.to_type)
434+
ifop.safe:
435+
# Polars does not support safe JSON decoding (returning null on failure).
436+
# We use map_elements to provide safe JSON decoding.
437+
defsafe_decode(val):
438+
ifvalisNone:
439+
returnNone
440+
try:
441+
decoded=json.loads(val)
442+
exceptException:
443+
returnNone
444+
445+
ifdecodedisNone:
446+
returnNone
447+
448+
ifop.to_type==bigframes.dtypes.INT_DTYPE:
449+
iftype(decoded) isbool:
450+
returnNone
451+
ifisinstance(decoded, int):
452+
returndecoded
453+
ifisinstance(decoded, float):
454+
ifdecoded.is_integer():
455+
returnint(decoded)
456+
ifisinstance(decoded, str):
457+
try:
458+
returnint(decoded)
459+
exceptException:
460+
pass
461+
returnNone
462+
463+
ifop.to_type==bigframes.dtypes.FLOAT_DTYPE:
464+
iftype(decoded) isbool:
465+
returnNone
466+
ifisinstance(decoded, (int, float)):
467+
returnfloat(decoded)
468+
ifisinstance(decoded, str):
469+
try:
470+
returnfloat(decoded)
471+
exceptException:
472+
pass
473+
returnNone
474+
475+
ifop.to_type==bigframes.dtypes.BOOL_DTYPE:
476+
ifisinstance(decoded, bool):
477+
returndecoded
478+
ifisinstance(decoded, str):
479+
ifdecoded.lower() =="true":
480+
returnTrue
481+
ifdecoded.lower() =="false":
482+
returnFalse
483+
returnNone
484+
485+
ifop.to_type==bigframes.dtypes.STRING_DTYPE:
486+
ifisinstance(decoded, str):
487+
returndecoded
488+
returnNone
489+
490+
returndecoded
491+
492+
returninput.map_elements(safe_decode, return_dtype=target_dtype)
493+
494+
returninput.str.json_decode(target_dtype)
433495

434496
@compile_op.register(arr_ops.ToArrayOp)
435497
def_(self, op: ops.ToArrayOp, *inputs: pl.Expr) ->pl.Expr:

‎bigframes/core/compile/polars/lowering.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def _lower_cast(cast_op: ops.AsTypeOp, arg: expression.Expression):
391391
returnarg
392392

393393
ifarg.output_type==dtypes.JSON_DTYPE:
394-
returnjson_ops.JSONDecode(cast_op.to_type).as_expr(arg)
394+
returnjson_ops.JSONDecode(cast_op.to_type, safe=cast_op.safe).as_expr(arg)
395395
if (
396396
arg.output_type==dtypes.STRING_DTYPE
397397
andcast_op.to_type==dtypes.DATETIME_DTYPE

‎bigframes/display/anywidget.py‎

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -133,25 +133,26 @@ def _initial_load(self) -> None:
133133
# obtain the row counts
134134
# TODO(b/428238610): Start iterating over the result of `to_pandas_batches()`
135135
# before we get here so that the count might already be cached.
136-
self._reset_batches_for_new_page_size()
136+
withbigframes.option_context("display.progress_bar", None):
137+
self._reset_batches_for_new_page_size()
137138

138-
ifself._batchesisNone:
139-
self._error_message= (
140-
"Could not retrieve data batches. Data might be unavailable or "
141-
"an error occurred."
142-
)
143-
self.row_count=None
144-
elifself._batches.total_rowsisNone:
145-
# Total rows is unknown, this is an expected state.
146-
# TODO(b/461536343): Cheaply discover if we have exactly 1 page.
147-
# There are cases where total rows is not set, but there are no additional
148-
# pages. We could disable the "next" button in these cases.
149-
self.row_count=None
150-
else:
151-
self.row_count=self._batches.total_rows
139+
ifself._batchesisNone:
140+
self._error_message= (
141+
"Could not retrieve data batches. Data might be unavailable or "
142+
"an error occurred."
143+
)
144+
self.row_count=None
145+
elifself._batches.total_rowsisNone:
146+
# Total rows is unknown, this is an expected state.
147+
# TODO(b/461536343): Cheaply discover if we have exactly 1 page.
148+
# There are cases where total rows is not set, but there are no additional
149+
# pages. We could disable the "next" button in these cases.
150+
self.row_count=None
151+
else:
152+
self.row_count=self._batches.total_rows
152153

153-
# get the initial page
154-
self._set_table_html()
154+
# get the initial page
155+
self._set_table_html()
155156

156157
@traitlets.observe("_initial_load_complete")
157158
def_on_initial_load_complete(self, change: dict[str, Any]):
@@ -281,7 +282,9 @@ def _reset_batches_for_new_page_size(self) -> None:
281282
def_set_table_html(self) ->None:
282283
"""Sets the current html data based on the current page and page size."""
283284
new_page=None
284-
withself._setting_html_lock:
285+
withself._setting_html_lock, bigframes.option_context(
286+
"display.progress_bar", None
287+
):
285288
ifself._error_message:
286289
self.table_html= (
287290
f"<div class='bigframes-error-message'>"

‎bigframes/display/html.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ def repr_mimebundle(
363363

364364
ifopts.repr_mode=="anywidget":
365365
try:
366-
returnget_anywidget_bundle(obj, include=include, exclude=exclude)
366+
withbigframes.option_context("display.progress_bar", None):
367+
returnget_anywidget_bundle(obj, include=include, exclude=exclude)
367368
exceptImportError:
368369
# Anywidget is an optional dependency, so warn rather than fail.
369370
# TODO(shuowei): When Anywidget becomes the default for all repr modes,

‎bigframes/operations/json_ops.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ def output_type(self, *input_types):
220220
classJSONDecode(base_ops.UnaryOp):
221221
name: typing.ClassVar[str] ="json_decode"
222222
to_type: dtypes.Dtype
223+
safe: bool=False
223224

224225
defoutput_type(self, *input_types):
225226
input_type=input_types[0]

‎bigframes/session/polars_executor.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
numeric_ops,
3535
string_ops,
3636
)
37+
importbigframes.operations.json_opsasjson_ops
3738
frombigframes.sessionimportexecutor, semi_executor
3839

3940
ifTYPE_CHECKING:
@@ -94,6 +95,7 @@
9495
string_ops.EndsWithOp,
9596
string_ops.StrContainsOp,
9697
string_ops.StrContainsRegexOp,
98+
json_ops.JSONDecode,
9799
)
98100
_COMPATIBLE_AGG_OPS= (
99101
agg_ops.SizeOp,

0 commit comments

Comments
 (0)