diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4d594ddfbc..0456b15ac3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -19,11 +19,9 @@ import datetime import inspect import itertools -import json import re import sys import textwrap -import traceback import typing from typing import ( Any, @@ -55,7 +53,6 @@ import pyarrow import tabulate -import bigframes._config.display_options as display_options import bigframes.constants import bigframes.core from bigframes.core import agg_expressions, log_adapter @@ -800,32 +797,15 @@ def __repr__(self) -> str: ) self._set_internal_query_job(query_job) + from bigframes.display import plaintext - column_count = len(pandas_df.columns) - - with display_options.pandas_repr(opts): - 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_dataframe_repr_params() # type: ignore - ) - if not self._has_index: - to_string_kwargs.update({"index": False}) - repr_string = pandas_df.to_string(**to_string_kwargs) - - # Modify the end of the string to reflect count. - lines = repr_string.split("\n") - pattern = re.compile("\\[[0-9]+ rows x [0-9]+ columns\\]") - if pattern.match(lines[-1]): - lines = lines[:-2] - - if row_count > len(lines) - 1: - lines.append("...") - - lines.append("") - lines.append(f"[{row_count} rows x {column_count} columns]") - return "\n".join(lines) + return plaintext.create_text_representation( + pandas_df, + row_count, + is_series=False, + has_index=self._has_index, + column_count=len(self.columns), + ) def _get_display_df_and_blob_cols(self) -> tuple[DataFrame, list[str]]: """Process blob columns for display.""" @@ -844,75 +824,6 @@ def _get_display_df_and_blob_cols(self) -> tuple[DataFrame, list[str]]: df[col] = df[col].blob._get_runtime(mode="R", with_metadata=True) return df, blob_cols - 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. - This function encapsulates the logic for anywidget display. - """ - from bigframes import display - - df, blob_cols = self._get_display_df_and_blob_cols() - - # Create and display the widget - widget = display.TableWidget(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) - - # 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 - ) - - 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 DataFrame.""" - opts = bigframes.options.display - with display_options.pandas_repr(opts): - 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_dataframe_repr_params() # type: ignore - ) - if not self._has_index: - to_string_kwargs.update({"index": False}) - - # We add our own dimensions string, so don't want pandas to. - to_string_kwargs.update({"show_dimensions": False}) - repr_string = pandas_df.to_string(**to_string_kwargs) - - lines = repr_string.split("\n") - - if total_rows is not None and total_rows > len(pandas_df): - lines.append("...") - - lines.append("") - column_count = len(self.columns) - lines.append(f"[{total_rows or '?'} rows x {column_count} columns]") - return "\n".join(lines) - def _repr_mimebundle_(self, include=None, exclude=None): """ Custom display method for IPython/Jupyter environments. @@ -920,98 +831,9 @@ def _repr_mimebundle_(self, include=None, exclude=None): """ # TODO(b/467647693): Anywidget integration has been tested in Jupyter, VS Code, and # BQ Studio, but there is a known compatibility issue with Marimo that needs to be addressed. - 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. - # TODO(shuowei): When Anywidget becomes the default for all repr modes, - # remove this warning. - 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()}" - ) - - # In non-anywidget mode, fetch data once and use it for both HTML - # and plain text representations to avoid multiple queries. - opts = bigframes.options.display - max_results = opts.max_rows - - df, blob_cols = self._get_display_df_and_blob_cols() - - pandas_df, row_count, query_job = df._block.retrieve_repr_request_results( - max_results - ) - self._set_internal_query_job(query_job) - column_count = len(pandas_df.columns) - - html_string = self._create_html_representation( - pandas_df, row_count, column_count, blob_cols - ) - - text_representation = self._create_text_representation(pandas_df, row_count) - - return {"text/html": html_string, "text/plain": text_representation} - - def _create_html_representation( - self, - pandas_df: pandas.DataFrame, - row_count: int, - column_count: int, - blob_cols: list[str], - ) -> str: - """Create an HTML representation of the DataFrame.""" - opts = bigframes.options.display - with display_options.pandas_repr(opts): - # TODO(shuowei, b/464053870): Escaping HTML would be useful, but - # `escape=False` is needed to show images. We may need to implement - # a full-fledged repr module to better support types not in pandas. - if bigframes.options.display.blob_display and blob_cols: - - def obj_ref_rt_to_html(obj_ref_rt) -> str: - obj_ref_rt_json = json.loads(obj_ref_rt) - obj_ref_details = obj_ref_rt_json["objectref"]["details"] - if "gcs_metadata" in obj_ref_details: - gcs_metadata = obj_ref_details["gcs_metadata"] - content_type = typing.cast( - str, gcs_metadata.get("content_type", "") - ) - if content_type.startswith("image"): - size_str = "" - if bigframes.options.display.blob_display_width: - size_str = f' width="{bigframes.options.display.blob_display_width}"' - if bigframes.options.display.blob_display_height: - size_str = ( - size_str - + f' height="{bigframes.options.display.blob_display_height}"' - ) - url = obj_ref_rt_json["access_urls"]["read_url"] - return f'' - - return f'uri: {obj_ref_rt_json["objectref"]["uri"]}, authorizer: {obj_ref_rt_json["objectref"]["authorizer"]}' - - formatters = {blob_col: obj_ref_rt_to_html for blob_col in blob_cols} - - # set max_colwidth so not to truncate the image url - with pandas.option_context("display.max_colwidth", None): - html_string = pandas_df.to_html( - escape=False, - notebook=True, - max_rows=pandas.get_option("display.max_rows"), - max_cols=pandas.get_option("display.max_columns"), - show_dimensions=pandas.get_option("display.show_dimensions"), - formatters=formatters, # type: ignore - ) - else: - # _repr_html_ stub is missing so mypy thinks it's a Series. Ignore mypy. - html_string = pandas_df._repr_html_() # type:ignore + from bigframes.display import html - html_string += f"[{row_count} rows x {column_count} columns in total]" - return html_string + return html.repr_mimebundle(self, include=include, exclude=exclude) def __delitem__(self, key: str): df = self.drop(columns=[key]) diff --git a/bigframes/display/html.py b/bigframes/display/html.py index 101bd296f1..3f1667eb9c 100644 --- a/bigframes/display/html.py +++ b/bigframes/display/html.py @@ -17,12 +17,23 @@ from __future__ import annotations import html -from typing import Any +import json +import traceback +import typing +from typing import Any, Union +import warnings import pandas as pd import pandas.api.types -from bigframes._config import options +import bigframes +from bigframes._config import display_options, options +from bigframes.display import plaintext +import bigframes.formatting_helpers as formatter + +if typing.TYPE_CHECKING: + import bigframes.dataframe + import bigframes.series def _is_dtype_numeric(dtype: Any) -> bool: @@ -91,3 +102,214 @@ def render_html( table_html.append("") return "\n".join(table_html) + + +def _obj_ref_rt_to_html(obj_ref_rt: str) -> str: + obj_ref_rt_json = json.loads(obj_ref_rt) + obj_ref_details = obj_ref_rt_json["objectref"]["details"] + if "gcs_metadata" in obj_ref_details: + gcs_metadata = obj_ref_details["gcs_metadata"] + content_type = typing.cast(str, gcs_metadata.get("content_type", "")) + if content_type.startswith("image"): + size_str = "" + if options.display.blob_display_width: + size_str = f' width="{options.display.blob_display_width}"' + if options.display.blob_display_height: + size_str = size_str + f' height="{options.display.blob_display_height}"' + url = obj_ref_rt_json["access_urls"]["read_url"] + return f'' + + return f'uri: {obj_ref_rt_json["objectref"]["uri"]}, authorizer: {obj_ref_rt_json["objectref"]["authorizer"]}' + + +def create_html_representation( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + pandas_df: pd.DataFrame, + total_rows: int, + total_columns: int, + blob_cols: list[str], +) -> str: + """Create an HTML representation of the DataFrame or Series.""" + from bigframes.series import Series + + opts = options.display + with display_options.pandas_repr(opts): + if isinstance(obj, Series): + # Some pandas objects may not have a _repr_html_ method, or it might + # fail in certain environments. We fall back to a pre-formatted + # string representation to ensure something is always displayed. + pd_series = pandas_df.iloc[:, 0] + try: + # TODO(b/464053870): Support rich display for blob Series. + html_string = pd_series._repr_html_() + except AttributeError: + html_string = f"
{pd_series.to_string()}
" + + is_truncated = total_rows is not None and total_rows > len(pandas_df) + if is_truncated: + html_string += f"

[{total_rows} rows]

" + return html_string + else: + # It's a DataFrame + # TODO(shuowei, b/464053870): Escaping HTML would be useful, but + # `escape=False` is needed to show images. We may need to implement + # a full-fledged repr module to better support types not in pandas. + if options.display.blob_display and blob_cols: + formatters = {blob_col: _obj_ref_rt_to_html for blob_col in blob_cols} + + # set max_colwidth so not to truncate the image url + with pandas.option_context("display.max_colwidth", None): + html_string = pandas_df.to_html( + escape=False, + notebook=True, + max_rows=pandas.get_option("display.max_rows"), + max_cols=pandas.get_option("display.max_columns"), + show_dimensions=pandas.get_option("display.show_dimensions"), + formatters=formatters, # type: ignore + ) + else: + # _repr_html_ stub is missing so mypy thinks it's a Series. Ignore mypy. + html_string = pandas_df._repr_html_() # type:ignore + + html_string += f"[{total_rows} rows x {total_columns} columns in total]" + return html_string + + +def _get_obj_metadata( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], +) -> tuple[bool, bool]: + from bigframes.series import Series + + is_series = isinstance(obj, Series) + if is_series: + has_index = len(obj._block.index_columns) > 0 + else: + has_index = obj._has_index + return is_series, has_index + + +def get_anywidget_bundle( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + include=None, + exclude=None, +) -> tuple[dict[str, Any], dict[str, Any]]: + """ + Helper method to create and return the anywidget mimebundle. + This function encapsulates the logic for anywidget display. + """ + from bigframes import display + from bigframes.series import Series + + if isinstance(obj, Series): + df = obj.to_frame() + else: + df, blob_cols = obj._get_display_df_and_blob_cols() + + widget = display.TableWidget(df) + widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude) + + 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) + + # Use cached data from widget to render HTML and plain text versions. + cached_pd = widget._cached_data + total_rows = widget.row_count + total_columns = len(df.columns) + + widget_repr["text/html"] = create_html_representation( + obj, + cached_pd, + total_rows, + total_columns, + blob_cols if "blob_cols" in locals() else [], + ) + is_series, has_index = _get_obj_metadata(obj) + widget_repr["text/plain"] = plaintext.create_text_representation( + cached_pd, + total_rows, + is_series=is_series, + has_index=has_index, + column_count=len(df.columns) if not is_series else 0, + ) + + return widget_repr, widget_metadata + + +def repr_mimebundle_deferred( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], +) -> dict[str, str]: + return { + "text/plain": formatter.repr_query_job(obj._compute_dry_run()), + "text/html": formatter.repr_query_job_html(obj._compute_dry_run()), + } + + +def repr_mimebundle_head( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], +) -> dict[str, str]: + from bigframes.series import Series + + opts = options.display + blob_cols: list[str] + if isinstance(obj, Series): + pandas_df, row_count, query_job = obj._block.retrieve_repr_request_results( + opts.max_rows + ) + blob_cols = [] + else: + df, blob_cols = obj._get_display_df_and_blob_cols() + pandas_df, row_count, query_job = df._block.retrieve_repr_request_results( + opts.max_rows + ) + + obj._set_internal_query_job(query_job) + column_count = len(pandas_df.columns) + + html_string = create_html_representation( + obj, pandas_df, row_count, column_count, blob_cols + ) + + is_series, has_index = _get_obj_metadata(obj) + text_representation = plaintext.create_text_representation( + pandas_df, + row_count, + is_series=is_series, + has_index=has_index, + column_count=len(pandas_df.columns) if not is_series else 0, + ) + + return {"text/html": html_string, "text/plain": text_representation} + + +def repr_mimebundle( + obj: Union[bigframes.dataframe.DataFrame, bigframes.series.Series], + include=None, + exclude=None, +): + """Custom display method for IPython/Jupyter environments.""" + # TODO(b/467647693): Anywidget integration has been tested in Jupyter, VS Code, and + # BQ Studio, but there is a known compatibility issue with Marimo that needs to be addressed. + + opts = options.display + if opts.repr_mode == "deferred": + return repr_mimebundle_deferred(obj) + + if opts.repr_mode == "anywidget": + try: + return get_anywidget_bundle(obj, include=include, exclude=exclude) + except ImportError: + # Anywidget is an optional dependency, so warn rather than fail. + # TODO(shuowei): When Anywidget becomes the default for all repr modes, + # remove this warning. + 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()}" + ) + + return repr_mimebundle_head(obj) diff --git a/bigframes/display/plaintext.py b/bigframes/display/plaintext.py new file mode 100644 index 0000000000..2f7bc1df07 --- /dev/null +++ b/bigframes/display/plaintext.py @@ -0,0 +1,102 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Plaintext display representations.""" + +from __future__ import annotations + +import typing + +import pandas +import pandas.io.formats + +from bigframes._config import display_options, options + +if typing.TYPE_CHECKING: + import pandas as pd + + +def create_text_representation( + pandas_df: pd.DataFrame, + total_rows: typing.Optional[int], + is_series: bool, + has_index: bool = True, + column_count: int = 0, +) -> str: + """Create a text representation of the DataFrame or Series. + + Args: + pandas_df: + The pandas DataFrame containing the data to represent. + total_rows: + The total number of rows in the original BigFrames object. + is_series: + Whether the object being represented is a Series. + has_index: + Whether the object has an index to display. + column_count: + The total number of columns in the original BigFrames object. + Only used for DataFrames. + + Returns: + A plaintext string representation. + """ + opts = options.display + + if is_series: + with display_options.pandas_repr(opts): + pd_series = pandas_df.iloc[:, 0] + if not has_index: + 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) + + lines = repr_string.split("\n") + is_truncated = total_rows is not None and total_rows > len(pandas_df) + + if is_truncated: + lines.append("...") + lines.append("") # Add empty line for spacing only if truncated + lines.append(f"[{total_rows} rows]") + + return "\n".join(lines) + + else: + # DataFrame + with display_options.pandas_repr(opts): + # 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_dataframe_repr_params() # type: ignore + ) + if not has_index: + to_string_kwargs.update({"index": False}) + + # We add our own dimensions string, so don't want pandas to. + to_string_kwargs.update({"show_dimensions": False}) + repr_string = pandas_df.to_string(**to_string_kwargs) + + lines = repr_string.split("\n") + is_truncated = total_rows is not None and total_rows > len(pandas_df) + + if is_truncated: + lines.append("...") + lines.append("") # Add empty line for spacing only if truncated + lines.append(f"[{total_rows or '?'} rows x {column_count} columns]") + else: + # For non-truncated DataFrames, we still need to add dimensions if show_dimensions was False + lines.append("") + lines.append(f"[{total_rows or '?'} rows x {column_count} columns]") + return "\n".join(lines) diff --git a/bigframes/formatting_helpers.py b/bigframes/formatting_helpers.py index 55731069a3..3c37a3470d 100644 --- a/bigframes/formatting_helpers.py +++ b/bigframes/formatting_helpers.py @@ -68,7 +68,7 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]): query_job: The job representing the execution of the query on the server. Returns: - Pywidget html table. + Formatted string. """ if query_job is None: return "No job information available" @@ -94,6 +94,46 @@ def repr_query_job(query_job: Optional[bigquery.QueryJob]): return res +def repr_query_job_html(query_job: Optional[bigquery.QueryJob]): + """Return query job as a formatted html string. + Args: + query_job: + The job representing the execution of the query on the server. + Returns: + Html string. + """ + if query_job is None: + return "No job information available" + if query_job.dry_run: + return f"Computation deferred. Computation will process {get_formatted_bytes(query_job.total_bytes_processed)}" + + # We can reuse the plaintext repr for now or make a nicer table. + # For deferred mode consistency, let's just wrap the text in a pre block or similar, + # but the request implies we want a distinct HTML representation if possible. + # However, existing repr_query_job returns a simple string. + # Let's format it as a simple table or list. + + res = "

Query Job Info

" + return res + + current_display: Optional[display.HTML] = None current_display_id: Optional[str] = None previous_display_html: str = "" @@ -296,7 +336,7 @@ def get_job_url( """ if project_id is None or location is None or job_id is None: return None - return f"""https://console.cloud.google.com/bigquery?project={project_id}&j=bq:{location}:{job_id}&page=queryresults""" + return f"""https://console.cloud. google.com/bigquery?project={project_id}&j=bq:{location}:{job_id}&page=queryresults""" def render_bqquery_sent_event_html( @@ -508,7 +548,7 @@ def get_base_job_loading_html(job: GenericJob): Returns: Html string. """ - return f"""{job.job_type.capitalize()} job {job.job_id} is {job.state}. str: # Protect against errors with uninitialized Series. See: # https://github.com/googleapis/python-bigquery-dataframes/issues/728 @@ -579,27 +590,22 @@ def __repr__(self) -> str: # TODO(swast): Avoid downloading the whole series by using job # metadata, like we do with DataFrame. opts = bigframes.options.display - 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( + opts.max_rows + ) self._set_internal_query_job(query_job) + from bigframes.display import plaintext - 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 plaintext.create_text_representation( + pandas_df, + row_count, + is_series=True, + has_index=len(self._block.index_columns) > 0, + ) def astype( self, diff --git a/notebooks/dataframes/anywidget_mode.ipynb b/notebooks/dataframes/anywidget_mode.ipynb index 0ce286ce64..facefc6069 100644 --- a/notebooks/dataframes/anywidget_mode.ipynb +++ b/notebooks/dataframes/anywidget_mode.ipynb @@ -45,10 +45,13 @@ "id": "04406a4d", "metadata": {}, "source": [ - "This notebook demonstrates the anywidget display mode, which provides an interactive table experience.\n", - "Key features include:\n", - "- **Column Sorting:** Click on column headers to sort data in ascending, descending, or unsorted states.\n", - "- **Adjustable Column Widths:** Drag the dividers between column headers to resize columns." + "This notebook demonstrates the **anywidget** display mode for BigQuery DataFrames. This mode provides an interactive table experience for exploring your data directly within the notebook.\n", + "\n", + "**Key features:**\n", + "- **Rich DataFrames & Series:** Both DataFrames and Series are displayed as interactive widgets.\n", + "- **Pagination:** Navigate through large datasets page by page without overwhelming the output.\n", + "- **Column Sorting:** Click column headers to toggle between ascending, descending, and unsorted views.\n", + "- **Column Resizing:** Drag the dividers between column headers to adjust their width." ] }, { @@ -70,6 +73,15 @@ "Load Sample Data" ] }, + { + "cell_type": "markdown", + "id": "interactive-df-header", + "metadata": {}, + "source": [ + "## 1. Interactive DataFrame Display\n", + "Loading a dataset from BigQuery automatically renders the interactive widget." + ] + }, { "cell_type": "code", "execution_count": 4, @@ -106,17 +118,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "state gender year name number\n", - " AL F 1910 Lillian 99\n", - " AL F 1910 Ruby 204\n", - " AL F 1910 Helen 76\n", - " AL F 1910 Eunice 41\n", - " AR F 1910 Dora 42\n", - " CA F 1910 Edna 62\n", - " CA F 1910 Helen 239\n", - " CO F 1910 Alice 46\n", - " FL F 1910 Willie 71\n", - " FL F 1910 Thelma 65\n", + "state gender year name number\n", + " AL F 1910 Vera 71\n", + " AR F 1910 Viola 37\n", + " AR F 1910 Alice 57\n", + " AR F 1910 Edna 95\n", + " AR F 1910 Ollie 40\n", + " CA F 1910 Beatrice 37\n", + " CT F 1910 Marion 36\n", + " CT F 1910 Marie 36\n", + " FL F 1910 Alice 53\n", + " GA F 1910 Thelma 133\n", "...\n", "\n", "[5552452 rows x 5 columns]\n" @@ -128,45 +140,10 @@ "print(df)" ] }, - { - "cell_type": "markdown", - "id": "3a73e472", - "metadata": {}, - "source": [ - "Display Series in anywidget mode" - ] - }, { "cell_type": "code", "execution_count": 5, - "id": "42bb02ab", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Computation deferred. Computation will process 44.4 MB\n" - ] - } - ], - "source": [ - "test_series = df[\"year\"]\n", - "print(test_series)" - ] - }, - { - "cell_type": "markdown", - "id": "7bcf1bb7", - "metadata": {}, - "source": [ - "Display with Pagination" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "ce250157", + "id": "220340b0", "metadata": {}, "outputs": [ { @@ -196,7 +173,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "775e84ca212c4867bb889266b830ae68", + "model_id": "424cfa14088641518224b137b5444d58", "version_major": 2, "version_minor": 1 }, @@ -232,80 +209,80 @@ " AL\n", " F\n", " 1910\n", - " Cora\n", - " 61\n", + " Vera\n", + " 71\n", " \n", " \n", " 1\n", - " AL\n", + " AR\n", " F\n", " 1910\n", - " Anna\n", - " 74\n", + " Viola\n", + " 37\n", " \n", " \n", " 2\n", " AR\n", " F\n", " 1910\n", - " Willie\n", - " 132\n", + " Alice\n", + " 57\n", " \n", " \n", " 3\n", - " CO\n", + " AR\n", " F\n", " 1910\n", - " Anna\n", - " 42\n", + " Edna\n", + " 95\n", " \n", " \n", " 4\n", - " FL\n", + " AR\n", " F\n", " 1910\n", - " Louise\n", - " 70\n", + " Ollie\n", + " 40\n", " \n", " \n", " 5\n", - " GA\n", + " CA\n", " F\n", " 1910\n", - " Catherine\n", - " 57\n", + " Beatrice\n", + " 37\n", " \n", " \n", " 6\n", - " IL\n", + " CT\n", " F\n", " 1910\n", - " Jessie\n", - " 43\n", + " Marion\n", + " 36\n", " \n", " \n", " 7\n", - " IN\n", + " CT\n", " F\n", " 1910\n", - " Anna\n", - " 100\n", + " Marie\n", + " 36\n", " \n", " \n", " 8\n", - " IN\n", + " FL\n", " F\n", " 1910\n", - " Pauline\n", - " 77\n", + " Alice\n", + " 53\n", " \n", " \n", " 9\n", - " IN\n", + " GA\n", " F\n", " 1910\n", - " Beulah\n", - " 39\n", + " Thelma\n", + " 133\n", " \n", " \n", "\n", @@ -313,23 +290,23 @@ "[5552452 rows x 5 columns in total]" ], "text/plain": [ - "state gender year name number\n", - " AL F 1910 Cora 61\n", - " AL F 1910 Anna 74\n", - " AR F 1910 Willie 132\n", - " CO F 1910 Anna 42\n", - " FL F 1910 Louise 70\n", - " GA F 1910 Catherine 57\n", - " IL F 1910 Jessie 43\n", - " IN F 1910 Anna 100\n", - " IN F 1910 Pauline 77\n", - " IN F 1910 Beulah 39\n", + "state gender year name number\n", + " AL F 1910 Vera 71\n", + " AR F 1910 Viola 37\n", + " AR F 1910 Alice 57\n", + " AR F 1910 Edna 95\n", + " AR F 1910 Ollie 40\n", + " CA F 1910 Beatrice 37\n", + " CT F 1910 Marion 36\n", + " CT F 1910 Marie 36\n", + " FL F 1910 Alice 53\n", + " GA F 1910 Thelma 133\n", "...\n", "\n", "[5552452 rows x 5 columns]" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -338,6 +315,175 @@ "df" ] }, + { + "cell_type": "markdown", + "id": "3a73e472", + "metadata": {}, + "source": [ + "## 2. Interactive Series Display\n", + "BigQuery DataFrames `Series` objects now also support the full interactive widget experience, including pagination and formatting." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "42bb02ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3904868f71114a0c95c8c133a6c29d0b", + "version_major": 2, + "version_minor": 1 + }, + "text/html": [ + "
0    1910\n",
+       "1    1910\n",
+       "2    1910\n",
+       "3    1910\n",
+       "4    1910\n",
+       "5    1910\n",
+       "6    1910\n",
+       "7    1910\n",
+       "8    1910\n",
+       "9    1910
[5552452 rows]" + ], + "text/plain": [ + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "Name: year, dtype: Int64\n", + "...\n", + "\n", + "[5552452 rows]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_series = df[\"year\"]\n", + "# Displaying the series triggers the interactive widget\n", + "test_series" + ] + }, + { + "cell_type": "markdown", + "id": "7bcf1bb7", + "metadata": {}, + "source": [ + "Display with Pagination" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "da23e0f3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0fd0bd56db2348a68d5755a045652001", + "version_major": 2, + "version_minor": 1 + }, + "text/html": [ + "
0    1910\n",
+       "1    1910\n",
+       "2    1910\n",
+       "3    1910\n",
+       "4    1910\n",
+       "5    1910\n",
+       "6    1910\n",
+       "7    1910\n",
+       "8    1910\n",
+       "9    1910
[5552452 rows]" + ], + "text/plain": [ + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "1910\n", + "Name: year, dtype: Int64\n", + "...\n", + "\n", + "[5552452 rows]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_series" + ] + }, { "cell_type": "markdown", "id": "sorting-intro", @@ -369,9 +515,18 @@ "Programmatic Navigation Demo" ] }, + { + "cell_type": "markdown", + "id": "programmatic-header", + "metadata": {}, + "source": [ + "## 3. Programmatic Widget Control\n", + "You can also instantiate the `TableWidget` directly for more control, such as checking page counts or driving navigation programmatically." + ] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "6920d49b", "metadata": {}, "outputs": [ @@ -409,15 +564,15 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "bf4224f8022042aea6d72507ddb5570b", + "model_id": "13b063f7ea74473eb18de270c48c6417", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -444,7 +599,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "12b68f15", "metadata": {}, "outputs": [ @@ -476,12 +631,13 @@ "id": "9d310138", "metadata": {}, "source": [ - "Edge Case Demonstration" + "## 4. Edge Cases\n", + "The widget handles small datasets gracefully, disabling unnecessary pagination controls." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "a9d5d13a", "metadata": {}, "outputs": [ @@ -523,15 +679,15 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8d9bfeeba3ca4d11a56dccb28aacde23", + "model_id": "0918149d2d734296afb3243f283eb2d3", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -553,9 +709,18 @@ "The `AI.GENERATE` function in BigQuery returns results in a JSON column. While BigQuery's JSON type is not natively supported by the underlying Arrow `to_pandas_batches()` method used in anywidget mode ([Apache Arrow issue #45262](https://github.com/apache/arrow/issues/45262)), BigQuery Dataframes automatically converts JSON columns to strings for display. This allows you to view the results of generative AI functions seamlessly." ] }, + { + "cell_type": "markdown", + "id": "ai-header", + "metadata": {}, + "source": [ + "## 5. Advanced Data Types (JSON/Structs)\n", + "The `AI.GENERATE` function in BigQuery returns results in a JSON column. BigQuery Dataframes automatically handles complex types like JSON strings for display, allowing you to view generative AI results seamlessly." + ] + }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "added-cell-1", "metadata": {}, "outputs": [ @@ -563,7 +728,7 @@ "data": { "text/html": [ "✅ Completed. \n", - " Query processed 85.9 kB in 13 seconds of slot time.\n", + " Query processed 85.9 kB in 24 seconds of slot time.\n", " " ], "text/plain": [ @@ -624,7 +789,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9fce25a077604e4882144d46d0d4ba45", + "model_id": "9543a0ef6eb744f480e49d4876c31b84", "version_major": 2, "version_minor": 1 }, @@ -690,24 +855,6 @@ " EU\n", " DE\n", " 03.10.2018\n", - " H05B 6/12\n", - " <NA>\n", - " 18165514.3\n", - " 03.04.2018\n", - " 30.03.2017\n", - " <NA>\n", - " BSH Hausger√§te GmbH\n", - " Acero Acero, Jesus\n", - " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", - " EP 3 383 141 A2\n", - " \n", - " \n", - " 2\n", - " {'application_number': None, 'class_internatio...\n", - " gs://gcs-public-data--labeled-patents/espacene...\n", - " EU\n", - " DE\n", - " 03.10.2018\n", " H01L 21/20\n", " <NA>\n", " 18166536.5\n", @@ -720,7 +867,7 @@ " EP 3 382 744 A1\n", " \n", " \n", - " 3\n", + " 2\n", " {'application_number': None, 'class_internatio...\n", " gs://gcs-public-data--labeled-patents/espacene...\n", " EU\n", @@ -738,7 +885,7 @@ " EP 3 382 553 A1\n", " \n", " \n", - " 4\n", + " 3\n", " {'application_number': None, 'class_internatio...\n", " gs://gcs-public-data--labeled-patents/espacene...\n", " EU\n", @@ -755,6 +902,24 @@ " MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E...\n", " EP 3 381 276 A1\n", " \n", + " \n", + " 4\n", + " {'application_number': None, 'class_internatio...\n", + " gs://gcs-public-data--labeled-patents/espacene...\n", + " EU\n", + " DE\n", + " 03.10.2018\n", + " H05B 6/12\n", + " <NA>\n", + " 18165514.3\n", + " 03.04.2018\n", + " 30.03.2017\n", + " <NA>\n", + " BSH Hausger√§te GmbH\n", + " Acero Acero, Jesus\n", + " VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG\n", + " EP 3 383 141 A2\n", + " \n", " \n", "\n", "

5 rows × 15 columns

\n", @@ -777,36 +942,36 @@ "\n", " publication_date class_international class_us application_number \\\n", "0 29.08.018 E04H 6/12 18157874.1 \n", - "1 03.10.2018 H05B 6/12 18165514.3 \n", - "2 03.10.2018 H01L 21/20 18166536.5 \n", - "3 03.10.2018 G06F 11/30 18157347.8 \n", - "4 03.10.2018 A01K 31/00 18171005.4 \n", + "1 03.10.2018 H01L 21/20 18166536.5 \n", + "2 03.10.2018 G06F 11/30 18157347.8 \n", + "3 03.10.2018 A01K 31/00 18171005.4 \n", + "4 03.10.2018 H05B 6/12 18165514.3 \n", "\n", " filing_date priority_date_eu representative_line_1_eu \\\n", "0 21.02.2018 22.02.2017 Liedtke & Partner Patentanw√§lte \n", - "1 03.04.2018 30.03.2017 \n", - "2 16.02.2016 Scheider, Sascha et al \n", - "3 19.02.2018 31.03.2017 Hoffmann Eitle \n", - "4 05.02.2015 05.02.2014 Stork Bamberger Patentanw√§lte \n", + "1 16.02.2016 Scheider, Sascha et al \n", + "2 19.02.2018 31.03.2017 Hoffmann Eitle \n", + "3 05.02.2015 05.02.2014 Stork Bamberger Patentanw√§lte \n", + "4 03.04.2018 30.03.2017 \n", "\n", " applicant_line_1 inventor_line_1 \\\n", "0 SHB Hebezeugbau GmbH VOLGER, Alexander \n", - "1 BSH Hausger√§te GmbH Acero Acero, Jesus \n", - "2 EV Group E. Thallner GmbH Kurz, Florian \n", - "3 FUJITSU LIMITED Kukihara, Kensuke \n", - "4 Linco Food Systems A/S Thrane, Uffe \n", + "1 EV Group E. Thallner GmbH Kurz, Florian \n", + "2 FUJITSU LIMITED Kukihara, Kensuke \n", + "3 Linco Food Systems A/S Thrane, Uffe \n", + "4 BSH Hausger√§te GmbH Acero Acero, Jesus \n", "\n", " title_line_1 number \n", "0 STEUERUNGSSYSTEM F√úR AUTOMATISCHE PARKH√ÑUSER EP 3 366 869 A1 \n", - "1 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", - "2 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", - "3 METHOD EXECUTED BY A COMPUTER, INFORMATION PRO... EP 3 382 553 A1 \n", - "4 MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E... EP 3 381 276 A1 \n", + "1 VORRICHTUNG ZUM BONDEN VON SUBSTRATEN EP 3 382 744 A1 \n", + "2 METHOD EXECUTED BY A COMPUTER, INFORMATION PRO... EP 3 382 553 A1 \n", + "3 MASTH√ÑHNCHENCONTAINER ALS BESTANDTEIL EINER E... EP 3 381 276 A1 \n", + "4 VORRICHTUNG ZUR INDUKTIVEN ENERGIE√úBERTRAGUNG EP 3 383 141 A2 \n", "\n", "[5 rows x 15 columns]" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..064bdaf362 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "python-bigquery-dataframes", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/tests/js/package-lock.json b/tests/js/package-lock.json index 8a562a11ea..5526e0581e 100644 --- a/tests/js/package-lock.json +++ b/tests/js/package-lock.json @@ -10,11 +10,19 @@ "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.24.7", + "@testing-library/jest-dom": "^6.4.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jsdom": "^24.1.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -2453,6 +2461,26 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -2706,6 +2734,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3306,6 +3344,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -3428,6 +3473,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -4020,6 +4072,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5321,6 +5383,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5655,6 +5727,20 @@ "dev": true, "license": "MIT" }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -5972,6 +6058,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/tests/js/package.json b/tests/js/package.json index 8de4b4747c..d34c5a065a 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -14,6 +14,7 @@ "@babel/preset-env": "^7.24.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "@testing-library/jest-dom": "^6.4.6", "jsdom": "^24.1.0" } } diff --git a/tests/js/table_widget.test.js b/tests/js/table_widget.test.js index 77ec7bcdd5..6b5dda48d1 100644 --- a/tests/js/table_widget.test.js +++ b/tests/js/table_widget.test.js @@ -206,4 +206,57 @@ describe("TableWidget", () => { expect(indicator2.textContent).toBe("●"); }); }); + + it("should render the series as a table with an index and one value column", () => { + // Mock the initial state + model.get.mockImplementation((property) => { + if (property === "table_html") { + return ` +
+
+ + + + + + + + + + + + + + + + + +
value
0a
1b
+
+
`; + } + if (property === "orderable_columns") { + return []; + } + return null; + }); + + render({ model, el }); + + // Manually trigger the table_html change handler + const tableHtmlChangeHandler = model.on.mock.calls.find( + (call) => call[0] === "change:table_html", + )[1]; + tableHtmlChangeHandler(); + + // Check that the table has two columns + const headers = el.querySelectorAll( + ".paginated-table-container .col-header-name", + ); + expect(headers).toHaveLength(2); + + // Check that the headers are an empty string (for the index) and "value" + expect(headers[0].textContent).toBe(""); + expect(headers[1].textContent).toBe("value"); + }); }); diff --git a/tests/system/small/test_anywidget.py b/tests/system/small/test_anywidget.py index b0eeb4a3c2..854e693fdf 100644 --- a/tests/system/small/test_anywidget.py +++ b/tests/system/small/test_anywidget.py @@ -201,6 +201,7 @@ def _assert_html_matches_pandas_slice( def test_widget_initialization_should_calculate_total_row_count( paginated_bf_df: bf.dataframe.DataFrame, ): + """Test that a TableWidget calculates the total row count on creation.""" """A TableWidget should correctly calculate the total row count on creation.""" from bigframes.display import TableWidget @@ -313,9 +314,7 @@ def test_widget_pagination_should_work_with_custom_page_size( start_row: int, end_row: int, ): - """ - A widget should paginate correctly with a custom page size of 3. - """ + """Test that a widget paginates correctly with a custom page size.""" with bigframes.option_context( "display.repr_mode", "anywidget", "display.max_rows", 3 ): @@ -956,10 +955,11 @@ def test_repr_in_anywidget_mode_should_not_be_deferred( assert "page_1_row_1" in representation -def test_dataframe_repr_mimebundle_anywidget_with_metadata( +def test_dataframe_repr_mimebundle_should_return_widget_with_metadata_in_anywidget_mode( monkeypatch: pytest.MonkeyPatch, session: bigframes.Session, # Add session as a fixture ): + """Test that _repr_mimebundle_ returns a widget view with metadata when anywidget is available.""" with bigframes.option_context("display.repr_mode", "anywidget"): # Create a real DataFrame object (or a mock that behaves like one minimally) # for _repr_mimebundle_ to operate on. @@ -984,7 +984,7 @@ def test_dataframe_repr_mimebundle_anywidget_with_metadata( # Patch the class method directly with mock.patch( - "bigframes.dataframe.DataFrame._get_anywidget_bundle", + "bigframes.display.html.get_anywidget_bundle", return_value=mock_get_anywidget_bundle_return_value, ): result = test_df._repr_mimebundle_() @@ -1135,3 +1135,41 @@ def test_widget_with_custom_index_matches_pandas_output( # TODO(b/438181139): Add tests for custom multiindex # This may not be necessary for the SQL Cell use case but should be # considered for completeness. + + +def test_series_anywidget_integration_with_notebook_display( + paginated_bf_df: bf.dataframe.DataFrame, +): + """Test Series display integration in Jupyter-like environment.""" + pytest.importorskip("anywidget") + + with bf.option_context("display.repr_mode", "anywidget"): + series = paginated_bf_df["value"] + + # Test the full display pipeline + from IPython.display import display as ipython_display + + # This should work without errors + ipython_display(series) + + +def test_series_different_data_types_anywidget(session: bf.Session): + """Test Series with different data types in anywidget mode.""" + pytest.importorskip("anywidget") + + # Create Series with different types + test_data = pd.DataFrame( + { + "string_col": ["a", "b", "c"], + "int_col": [1, 2, 3], + "float_col": [1.1, 2.2, 3.3], + "bool_col": [True, False, True], + } + ) + bf_df = session.read_pandas(test_data) + + with bf.option_context("display.repr_mode", "anywidget"): + for col_name in test_data.columns: + series = bf_df[col_name] + widget = bigframes.display.TableWidget(series.to_frame()) + assert widget.row_count == 3