Skip to content

Commit 15e6175

Browse files
authored
feat: add bigframes.pandas.options.display.precision option (#1979)
* feat: add `bigframes.pandas.options.display.precision` option feat: when using `repr_mode = "anywidget"`, numeric values align right * include traceback in anywidet warning * add tests for more complex data types * add padding * add dataframe class to table * fix unit test * fix doctest
1 parent f25d7bd commit 15e6175

File tree

11 files changed

+423
-49
lines changed

11 files changed

+423
-49
lines changed

bigframes/_config/display_options.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626
class DisplayOptions:
2727
__doc__ = vendored_pandas_config.display_options_doc
2828

29+
# Options borrowed from pandas.
2930
max_columns: int = 20
30-
max_rows: int = 25
31+
max_rows: int = 10
32+
precision: int = 6
33+
34+
# Options unique to BigQuery DataFrames.
3135
progress_bar: Optional[str] = "auto"
3236
repr_mode: Literal["head", "deferred", "anywidget"] = "head"
3337

@@ -52,6 +56,8 @@ def pandas_repr(display_options: DisplayOptions):
5256
display_options.max_columns,
5357
"display.max_rows",
5458
display_options.max_rows,
59+
"display.precision",
60+
display_options.precision,
5561
"display.show_dimensions",
5662
True,
5763
) as pandas_context:

bigframes/dataframe.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import re
2424
import sys
2525
import textwrap
26+
import traceback
2627
import typing
2728
from typing import (
2829
Callable,
@@ -814,7 +815,9 @@ def _repr_html_(self) -> str:
814815
except (AttributeError, ValueError, ImportError):
815816
# Fallback if anywidget is not available
816817
warnings.warn(
817-
"Anywidget mode is not available. Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. Falling back to deferred mode."
818+
"Anywidget mode is not available. "
819+
"Please `pip install anywidget traitlets` or `pip install 'bigframes[anywidget]'` to use interactive tables. "
820+
f"Falling back to deferred mode. Error: {traceback.format_exc()}"
818821
)
819822
return formatter.repr_query_job(self._compute_dry_run())
820823

bigframes/display/anywidget.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import pandas as pd
2424

2525
import bigframes
26+
import bigframes.display.html
2627

2728
# anywidget and traitlets are optional dependencies. We don't want the import of this
2829
# module to fail if they aren't installed, though. Instead, we try to limit the surface that
@@ -201,12 +202,9 @@ def _set_table_html(self):
201202
page_data = cached_data.iloc[start:end]
202203

203204
# Generate HTML table
204-
self.table_html = page_data.to_html(
205-
index=False,
206-
max_rows=None,
205+
self.table_html = bigframes.display.html.render_html(
206+
dataframe=page_data,
207207
table_id=f"table-{self._table_id}",
208-
classes="table table-striped table-hover",
209-
escape=False,
210208
)
211209

212210
@traitlets.observe("page")

bigframes/display/html.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""HTML rendering for DataFrames and other objects."""
16+
17+
from __future__ import annotations
18+
19+
import html
20+
21+
import pandas as pd
22+
import pandas.api.types
23+
24+
from bigframes._config import options
25+
26+
27+
def _is_dtype_numeric(dtype) -> bool:
28+
"""Check if a dtype is numeric for alignment purposes."""
29+
return pandas.api.types.is_numeric_dtype(dtype)
30+
31+
32+
def render_html(
33+
*,
34+
dataframe: pd.DataFrame,
35+
table_id: str,
36+
) -> str:
37+
"""Render a pandas DataFrame to HTML with specific styling."""
38+
classes = "dataframe table table-striped table-hover"
39+
table_html = [f'<table border="1" class="{classes}" id="{table_id}">']
40+
precision = options.display.precision
41+
42+
# Render table head
43+
table_html.append(" <thead>")
44+
table_html.append(' <tr style="text-align: left;">')
45+
for col in dataframe.columns:
46+
table_html.append(
47+
f' <th style="text-align: left;"><div style="resize: horizontal; overflow: auto; box-sizing: border-box; width: 100%; height: 100%; padding: 0.5em;">{html.escape(str(col))}</div></th>'
48+
)
49+
table_html.append(" </tr>")
50+
table_html.append(" </thead>")
51+
52+
# Render table body
53+
table_html.append(" <tbody>")
54+
for i in range(len(dataframe)):
55+
table_html.append(" <tr>")
56+
row = dataframe.iloc[i]
57+
for col_name, value in row.items():
58+
dtype = dataframe.dtypes.loc[col_name] # type: ignore
59+
align = "right" if _is_dtype_numeric(dtype) else "left"
60+
table_html.append(
61+
' <td style="text-align: {}; padding: 0.5em;">'.format(align)
62+
)
63+
64+
# TODO(b/438181139): Consider semi-exploding ARRAY/STRUCT columns
65+
# into multiple rows/columns like the BQ UI does.
66+
if pandas.api.types.is_scalar(value) and pd.isna(value):
67+
table_html.append(' <em style="color: gray;">&lt;NA&gt;</em>')
68+
else:
69+
if isinstance(value, float):
70+
formatted_value = f"{value:.{precision}f}"
71+
table_html.append(f" {html.escape(formatted_value)}")
72+
else:
73+
table_html.append(f" {html.escape(str(value))}")
74+
table_html.append(" </td>")
75+
table_html.append(" </tr>")
76+
table_html.append(" </tbody>")
77+
table_html.append("</table>")
78+
79+
return "\n".join(table_html)

notebooks/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.ipynb_checkpoints/

notebooks/dataframes/anywidget_mode.ipynb

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"metadata": {},
5656
"outputs": [],
5757
"source": [
58+
"bpd.options.bigquery.ordering_mode = \"partial\"\n",
5859
"bpd.options.display.repr_mode = \"anywidget\""
5960
]
6061
},
@@ -75,7 +76,7 @@
7576
{
7677
"data": {
7778
"text/html": [
78-
"Query job c5fcfd5e-1617-49c8-afa3-86ca21019de4 is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:c5fcfd5e-1617-49c8-afa3-86ca21019de4&page=queryresults\">Open Job</a>"
79+
"Query job a643d120-4af9-44fc-ba3c-ed461cf1092b is DONE. 0 Bytes processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:a643d120-4af9-44fc-ba3c-ed461cf1092b&page=queryresults\">Open Job</a>"
7980
],
8081
"text/plain": [
8182
"<IPython.core.display.HTML object>"
@@ -115,7 +116,7 @@
115116
"name": "stdout",
116117
"output_type": "stream",
117118
"text": [
118-
"Computation deferred. Computation will process 171.4 MB\n"
119+
"Computation deferred. Computation will process 44.4 MB\n"
119120
]
120121
}
121122
],
@@ -138,27 +139,15 @@
138139
"id": "ce250157",
139140
"metadata": {},
140141
"outputs": [
141-
{
142-
"data": {
143-
"text/html": [
144-
"Query job ab900a53-5011-4e80-85d5-0ef2958598db is DONE. 171.4 MB processed. <a target=\"_blank\" href=\"https://console.cloud.google.com/bigquery?project=bigframes-dev&j=bq:US:ab900a53-5011-4e80-85d5-0ef2958598db&page=queryresults\">Open Job</a>"
145-
],
146-
"text/plain": [
147-
"<IPython.core.display.HTML object>"
148-
]
149-
},
150-
"metadata": {},
151-
"output_type": "display_data"
152-
},
153142
{
154143
"data": {
155144
"application/vnd.jupyter.widget-view+json": {
156-
"model_id": "bda63ba739dc4d5f83a5e18eb27b2686",
145+
"model_id": "d2d4ef22ea9f414b89ea5bd85f0e6635",
157146
"version_major": 2,
158147
"version_minor": 1
159148
},
160149
"text/plain": [
161-
"TableWidget(row_count=5552452, table_html='<table border=\"1\" class=\"dataframe table table-striped table-hover\""
150+
"TableWidget(page_size=10, row_count=5552452, table_html='<table border=\"1\" class=\"table table-striped table-ho"
162151
]
163152
},
164153
"metadata": {},
@@ -185,7 +174,7 @@
185174
"id": "bb15bab6",
186175
"metadata": {},
187176
"source": [
188-
"Progarmmatic Navigation Demo"
177+
"Programmatic Navigation Demo"
189178
]
190179
},
191180
{
@@ -198,18 +187,18 @@
198187
"name": "stdout",
199188
"output_type": "stream",
200189
"text": [
201-
"Total pages: 222099\n"
190+
"Total pages: 555246\n"
202191
]
203192
},
204193
{
205194
"data": {
206195
"application/vnd.jupyter.widget-view+json": {
207-
"model_id": "9bffeb73549e48419c3dd895edfe30e8",
196+
"model_id": "121e3d2f28004036a922e3a11a08d4b7",
208197
"version_major": 2,
209198
"version_minor": 1
210199
},
211200
"text/plain": [
212-
"TableWidget(row_count=5552452, table_html='<table border=\"1\" class=\"dataframe table table-striped table-hover\""
201+
"TableWidget(page_size=10, row_count=5552452, table_html='<table border=\"1\" class=\"table table-striped table-ho"
213202
]
214203
},
215204
"execution_count": 7,
@@ -280,6 +269,14 @@
280269
"id": "a9d5d13a",
281270
"metadata": {},
282271
"outputs": [
272+
{
273+
"name": "stderr",
274+
"output_type": "stream",
275+
"text": [
276+
"/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:230: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n",
277+
" warnings.warn(msg, bfe.AmbiguousWindowWarning)\n"
278+
]
279+
},
283280
{
284281
"name": "stdout",
285282
"output_type": "stream",
@@ -290,12 +287,12 @@
290287
{
291288
"data": {
292289
"application/vnd.jupyter.widget-view+json": {
293-
"model_id": "dfd4fa3a1c6f4b3eb1877cb0e9ba7e94",
290+
"model_id": "5ed335bbbc064e5391ea06a9a218642e",
294291
"version_major": 2,
295292
"version_minor": 1
296293
},
297294
"text/plain": [
298-
"TableWidget(row_count=5, table_html='<table border=\"1\" class=\"dataframe table table-striped table-hover\" id=\"t"
295+
"TableWidget(page_size=10, row_count=5, table_html='<table border=\"1\" class=\"table table-striped table-hover\" i"
299296
]
300297
},
301298
"execution_count": 9,
@@ -305,16 +302,24 @@
305302
],
306303
"source": [
307304
"# Test with very small dataset\n",
308-
"small_df = df.head(5)\n",
305+
"small_df = df.sort_values([\"name\", \"year\", \"state\"]).head(5)\n",
309306
"small_widget = TableWidget(small_df)\n",
310307
"print(f\"Small dataset pages: {math.ceil(small_widget.row_count / small_widget.page_size)}\")\n",
311308
"small_widget"
312309
]
310+
},
311+
{
312+
"cell_type": "code",
313+
"execution_count": null,
314+
"id": "c4e5836b-c872-4a9c-b9ec-14f6f338176d",
315+
"metadata": {},
316+
"outputs": [],
317+
"source": []
313318
}
314319
],
315320
"metadata": {
316321
"kernelspec": {
317-
"display_name": "venv",
322+
"display_name": "Python 3 (ipykernel)",
318323
"language": "python",
319324
"name": "python3"
320325
},
@@ -328,7 +333,7 @@
328333
"name": "python",
329334
"nbconvert_exporter": "python",
330335
"pygments_lexer": "ipython3",
331-
"version": "3.10.15"
336+
"version": "3.10.16"
332337
}
333338
},
334339
"nbformat": 4,

0 commit comments

Comments
 (0)