Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
218b7c2
feat: Enhance Series and DataFrame display with anywidget
shuoweil Dec 19, 2025
9e3163f
test: add more npm tests
shuoweil Dec 19, 2025
3227b23
test: add this file for faster and reliable npm tests
shuoweil Dec 19, 2025
c1a8f83
docs: notebook update
shuoweil Dec 19, 2025
c39293a
test: update old testcase due to new feature implementation
shuoweil Dec 19, 2025
9dff0f4
Revert "test: update old testcase due to new feature implementation"
shuoweil Dec 19, 2025
f67e30f
feat: only display row count when series is large than the number can…
shuoweil Dec 19, 2025
81d1dbe
refactor: Handle special float values and None consistently in sqlglo…
chelsea-lin Dec 19, 2025
057f54d
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 19, 2025
f70d5c1
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 19, 2025
58e357a
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 19, 2025
fd04e6a
refactor: code refactor
shuoweil Dec 19, 2025
4825aeb
fix: fix mypy
shuoweil Dec 19, 2025
8845464
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 22, 2025
d36fc0f
refactor: move code to plaintext file and add checks
shuoweil Dec 22, 2025
593f9ae
refactor: move code to plaintext file and add checks
shuoweil Dec 22, 2025
400ea07
Revert "refactor: move code to plaintext file and add checks"
shuoweil Dec 22, 2025
945616c
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 23, 2025
a474606
refactor: move create_text_representation to plaintext.py
shuoweil Dec 23, 2025
bd56992
refactor: move display logic to display/plaintext.py and display/html.py
shuoweil Dec 23, 2025
971ee33
refactor: restore original order of max_results in __repr__
shuoweil Dec 23, 2025
1a73628
docs: add todo back
shuoweil Dec 23, 2025
1b7952b
refactor: split repr_mimebundle logic, handle deferred mode in html, …
shuoweil Dec 23, 2025
15b2ac6
refactor: rename repr_mimebundle helpers and improve fallback comments
shuoweil Dec 23, 2025
f8914c8
style: fix repr_mimebundle docstring formatting
shuoweil Dec 23, 2025
9fea10e
docs: update anywidget demo notebook with series display showcase
shuoweil Dec 23, 2025
a20a5ee
docs: update notebook
shuoweil Dec 23, 2025
9e92c2a
Merge branch 'main' into shuowei-anywidget-series-display
shuoweil Dec 23, 2025
c0f4b4e
refactor: decouple plaintext representation from core objects
shuoweil Dec 23, 2025
38899b7
refactor: consolidate object metadata extraction for display
shuoweil Dec 23, 2025
64230d1
fix: refactor html display to address review comments
shuoweil Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 106 additions & 13 deletions bigframes/series.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this code is nearly identical to the logic in https://github.com/googleapis/python-bigquery-dataframes/blob/main/bigframes/dataframe.py with the following differences:

  1. Call self.to_frame() before rendering the TableWidget.
  2. Use the Series object when rendering the text/html and text/plain.

Could you please refactor dataframe.py and this file to use shared helper functions to reduce duplication? https://github.com/googleapis/python-bigquery-dataframes/blob/main/bigframes/display/html.py seems like it'd be and appropriate place for such helpers.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import itertools
import numbers
import textwrap
import traceback
import typing
from typing import (
Any,
Expand All @@ -48,6 +49,7 @@
import pyarrow as pa
import typing_extensions

import bigframes._config.display_options as display_options
import bigframes.core
from bigframes.core import agg_expressions, groupby, log_adapter
import bigframes.core.block_transforms as block_ops
Expand Down Expand Up @@ -568,6 +570,105 @@ def reset_index(
block = block.assign_label(self._value_column, name)
return bigframes.dataframe.DataFrame(block)

def _get_anywidget_bundle(
self, include=None, exclude=None
) -> tuple[dict[str, Any], dict[str, Any]]:
"""
Helper method to create and return the anywidget mimebundle for Series.
"""
from bigframes import display

# Convert Series to DataFrame for TableWidget
series_df = self.to_frame()

# Create and display the widget
widget = display.TableWidget(series_df)
widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude)

# Handle both tuple (data, metadata) and dict returns
if isinstance(widget_repr_result, tuple):
widget_repr, widget_metadata = widget_repr_result
else:
widget_repr = widget_repr_result
widget_metadata = {}

widget_repr = dict(widget_repr)

# Add text representation
widget_repr["text/plain"] = self._create_text_representation(
widget._cached_data, widget.row_count
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like with dataframe, we should have the html rendering here too. See

# At this point, we have already executed the query as part of the
# widget construction. Let's use the information available to render
# the HTML and plain text versions.
widget_repr["text/html"] = self._create_html_representation(
widget._cached_data,
widget.row_count,
len(self.columns),
blob_cols,
)
widget_repr["text/plain"] = self._create_text_representation(
widget._cached_data, widget.row_count
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd refactor so that the code is shared. One way to do this would be to call the respective Series._create_html_representation or DataFrame._create_html_representation from the helper function.


return widget_repr, widget_metadata

def _create_text_representation(
self, pandas_df: pandas.DataFrame, total_rows: typing.Optional[int]
) -> str:
"""Create a text representation of the Series."""
opts = bigframes.options.display
with display_options.pandas_repr(opts):
# Get the first column since Series DataFrame has only one column
pd_series = pandas_df.iloc[:, 0]
if len(self._block.index_columns) == 0:
repr_string = pd_series.to_string(
length=False, index=False, name=True, dtype=True
)
else:
repr_string = pd_series.to_string(length=False, name=True, dtype=True)

is_truncated = total_rows is not None and total_rows > len(pd_series)

if is_truncated:
lines = repr_string.split("\n")
lines.append("...")
lines.append("")
lines.append(f"[{total_rows} rows]")
return "\n".join(lines)
else:
return repr_string

def _repr_mimebundle_(self, include=None, exclude=None):
"""
Custom display method for IPython/Jupyter environments.
This is called by IPython's display system when the object is displayed.
"""
opts = bigframes.options.display

# Only handle widget display in anywidget mode
if opts.repr_mode == "anywidget":
try:
return self._get_anywidget_bundle(include=include, exclude=exclude)

except ImportError:
# Anywidget is an optional dependency, so warn rather than fail.
warnings.warn(
"Anywidget mode is not available. "
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
f"Falling back to static HTML. Error: {traceback.format_exc()}"
)
# Fall back to regular HTML representation
pass

# Continue with regular HTML rendering for non-anywidget modes
self._cached()
pandas_df, row_count, query_job = self._block.retrieve_repr_request_results(
opts.max_rows
)
self._set_internal_query_job(query_job)

pd_series = pandas_df.iloc[:, 0]

# Use pandas Series _repr_html_ if available, otherwise create basic HTML
try:
html_string = pd_series._repr_html_()
except AttributeError:
# Fallback for pandas versions without _repr_html_
html_string = f"<pre>{pd_series.to_string()}</pre>"

text_representation = self._create_text_representation(pandas_df, row_count)

return {"text/html": html_string, "text/plain": text_representation}

def __repr__(self) -> str:
# Protect against errors with uninitialized Series. See:
# https://github.com/googleapis/python-bigquery-dataframes/issues/728
Expand All @@ -582,24 +683,16 @@ def __repr__(self) -> str:
max_results = opts.max_rows
# anywdiget mode uses the same display logic as the "deferred" mode
# for faster execution
if opts.repr_mode in ("deferred", "anywidget"):
if opts.repr_mode == "deferred":
return formatter.repr_query_job(self._compute_dry_run())

self._cached()
pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results)
pandas_df, row_count, query_job = self._block.retrieve_repr_request_results(
max_results
)
self._set_internal_query_job(query_job)

pd_series = pandas_df.iloc[:, 0]

import pandas.io.formats

# safe to mutate this, this dict is owned by this code, and does not affect global config
to_string_kwargs = pandas.io.formats.format.get_series_repr_params() # type: ignore
if len(self._block.index_columns) == 0:
to_string_kwargs.update({"index": False})
repr_string = pd_series.to_string(**to_string_kwargs)

return repr_string
return self._create_text_representation(pandas_df, row_count)

def astype(
self,
Expand Down
Loading