Skip to content

Commit 218b7c2

Browse files
committed
feat: Enhance Series and DataFrame display with anywidget
1 parent 58b2ac5 commit 218b7c2

File tree

5 files changed

+761
-126
lines changed

5 files changed

+761
-126
lines changed

bigframes/series.py

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import itertools
2323
import numbers
2424
import textwrap
25+
import traceback
2526
import typing
2627
from typing import (
2728
Any,
@@ -48,6 +49,7 @@
4849
import pyarrow as pa
4950
import typing_extensions
5051

52+
import bigframes._config.display_options as display_options
5153
import bigframes.core
5254
from bigframes.core import agg_expressions, groupby, log_adapter
5355
import bigframes.core.block_transforms as block_ops
@@ -568,6 +570,106 @@ def reset_index(
568570
block = block.assign_label(self._value_column, name)
569571
return bigframes.dataframe.DataFrame(block)
570572

573+
def _get_anywidget_bundle(
574+
self, include=None, exclude=None
575+
) -> tuple[dict[str, Any], dict[str, Any]]:
576+
"""
577+
Helper method to create and return the anywidget mimebundle for Series.
578+
"""
579+
from bigframes import display
580+
581+
# Convert Series to DataFrame for TableWidget
582+
series_df = self.to_frame()
583+
584+
# Create and display the widget
585+
widget = display.TableWidget(series_df)
586+
widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude)
587+
588+
# Handle both tuple (data, metadata) and dict returns
589+
if isinstance(widget_repr_result, tuple):
590+
widget_repr, widget_metadata = widget_repr_result
591+
else:
592+
widget_repr = widget_repr_result
593+
widget_metadata = {}
594+
595+
widget_repr = dict(widget_repr)
596+
597+
# Add text representation
598+
widget_repr["text/plain"] = self._create_text_representation(
599+
widget._cached_data, widget.row_count
600+
)
601+
602+
return widget_repr, widget_metadata
603+
604+
def _create_text_representation(
605+
self, pandas_df: pandas.DataFrame, total_rows: typing.Optional[int]
606+
) -> str:
607+
"""Create a text representation of the Series."""
608+
opts = bigframes.options.display
609+
with display_options.pandas_repr(opts):
610+
import pandas.io.formats
611+
612+
# safe to mutate this, this dict is owned by this code, and does not affect global config
613+
to_string_kwargs = (
614+
pandas.io.formats.format.get_series_repr_params() # type: ignore
615+
)
616+
if len(self._block.index_columns) == 0:
617+
to_string_kwargs.update({"index": False})
618+
# Get the first column since Series DataFrame has only one column
619+
pd_series = pandas_df.iloc[:, 0]
620+
repr_string = pd_series.to_string(**to_string_kwargs)
621+
622+
lines = repr_string.split("\n")
623+
624+
if total_rows is not None and total_rows > len(pd_series):
625+
lines.append("...")
626+
627+
lines.append("")
628+
lines.append(f"[{total_rows} rows]")
629+
return "\n".join(lines)
630+
631+
def _repr_mimebundle_(self, include=None, exclude=None):
632+
"""
633+
Custom display method for IPython/Jupyter environments.
634+
This is called by IPython's display system when the object is displayed.
635+
"""
636+
opts = bigframes.options.display
637+
638+
# Only handle widget display in anywidget mode
639+
if opts.repr_mode == "anywidget":
640+
try:
641+
return self._get_anywidget_bundle(include=include, exclude=exclude)
642+
643+
except ImportError:
644+
# Anywidget is an optional dependency, so warn rather than fail.
645+
warnings.warn(
646+
"Anywidget mode is not available. "
647+
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
648+
f"Falling back to static HTML. Error: {traceback.format_exc()}"
649+
)
650+
# Fall back to regular HTML representation
651+
pass
652+
653+
# Continue with regular HTML rendering for non-anywidget modes
654+
self._cached()
655+
pandas_df, row_count, query_job = self._block.retrieve_repr_request_results(
656+
opts.max_rows
657+
)
658+
self._set_internal_query_job(query_job)
659+
660+
pd_series = pandas_df.iloc[:, 0]
661+
662+
# Use pandas Series _repr_html_ if available, otherwise create basic HTML
663+
try:
664+
html_string = pd_series._repr_html_()
665+
except AttributeError:
666+
# Fallback for pandas versions without _repr_html_
667+
html_string = f"<pre>{pd_series.to_string()}</pre>"
668+
669+
text_representation = self._create_text_representation(pandas_df, row_count)
670+
671+
return {"text/html": html_string, "text/plain": text_representation}
672+
571673
def __repr__(self) -> str:
572674
# Protect against errors with uninitialized Series. See:
573675
# https://github.com/googleapis/python-bigquery-dataframes/issues/728
@@ -582,24 +684,16 @@ def __repr__(self) -> str:
582684
max_results = opts.max_rows
583685
# anywdiget mode uses the same display logic as the "deferred" mode
584686
# for faster execution
585-
if opts.repr_mode in ("deferred", "anywidget"):
687+
if opts.repr_mode == "deferred":
586688
return formatter.repr_query_job(self._compute_dry_run())
587689

588690
self._cached()
589-
pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results)
691+
pandas_df, row_count, query_job = self._block.retrieve_repr_request_results(
692+
max_results
693+
)
590694
self._set_internal_query_job(query_job)
591695

592-
pd_series = pandas_df.iloc[:, 0]
593-
594-
import pandas.io.formats
595-
596-
# safe to mutate this, this dict is owned by this code, and does not affect global config
597-
to_string_kwargs = pandas.io.formats.format.get_series_repr_params() # type: ignore
598-
if len(self._block.index_columns) == 0:
599-
to_string_kwargs.update({"index": False})
600-
repr_string = pd_series.to_string(**to_string_kwargs)
601-
602-
return repr_string
696+
return self._create_text_representation(pandas_df, row_count)
603697

604698
def astype(
605699
self,

0 commit comments

Comments
 (0)