team-10/env/Lib/site-packages/streamlit/elements/lib/pandas_styler_utils.py
2025-08-02 07:34:44 +02:00

278 lines
8.3 KiB
Python

# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# 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.
from __future__ import annotations
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar
from streamlit import dataframe_util
from streamlit.errors import StreamlitAPIException
if TYPE_CHECKING:
from pandas import DataFrame
from pandas.io.formats.style import Styler
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
from enum import Enum
def marshall_styler(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
"""Marshall pandas.Styler into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
default_uuid : str
If pandas.Styler uuid is not provided, this value will be used.
"""
import pandas as pd
styler_data_df: pd.DataFrame = styler.data
if styler_data_df.size > int(pd.options.styler.render.max_elements):
raise StreamlitAPIException(
f"The dataframe has `{styler_data_df.size}` cells, but the maximum number "
"of cells allowed to be rendered by Pandas Styler is configured to "
f"`{pd.options.styler.render.max_elements}`. To allow more cells to be "
'styled, you can change the `"styler.render.max_elements"` config. For example: '
f'`pd.set_option("styler.render.max_elements", {styler_data_df.size})`'
)
# pandas.Styler uuid should be set before _compute is called.
_marshall_uuid(proto, styler, default_uuid)
# We're using protected members of pandas.Styler to get styles,
# which is not ideal and could break if the interface changes.
styler._compute()
pandas_styles = styler._translate(False, False)
_marshall_caption(proto, styler)
_marshall_styles(proto, styler, pandas_styles)
_marshall_display_values(proto, styler_data_df, pandas_styles)
def _marshall_uuid(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
"""Marshall pandas.Styler uuid into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
default_uuid : str
If pandas.Styler uuid is not provided, this value will be used.
"""
if styler.uuid is None:
styler.set_uuid(default_uuid)
proto.styler.uuid = str(styler.uuid)
def _marshall_caption(proto: ArrowProto, styler: Styler) -> None:
"""Marshall pandas.Styler caption into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
"""
if styler.caption is not None:
proto.styler.caption = styler.caption
def _marshall_styles(
proto: ArrowProto, styler: Styler, styles: Mapping[str, Any]
) -> None:
"""Marshall pandas.Styler styles into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
styler : pandas.Styler
Helps style a DataFrame or Series according to the data with HTML and CSS.
styles : dict
pandas.Styler translated styles.
"""
css_rules = []
if "table_styles" in styles:
table_styles = styles["table_styles"]
table_styles = _trim_pandas_styles(table_styles)
for style in table_styles:
# styles in "table_styles" have a space
# between the uuid and selector.
rule = _pandas_style_to_css(
"table_styles", style, styler.uuid, separator=" "
)
css_rules.append(rule)
if "cellstyle" in styles:
cellstyle = styles["cellstyle"]
cellstyle = _trim_pandas_styles(cellstyle)
for style in cellstyle:
rule = _pandas_style_to_css("cell_style", style, styler.uuid, separator="_")
css_rules.append(rule)
if len(css_rules) > 0:
proto.styler.styles = "\n".join(css_rules)
M = TypeVar("M", bound=Mapping[str, Any])
def _trim_pandas_styles(styles: list[M]) -> list[M]:
"""Filter out empty styles.
Every cell will have a class, but the list of props
may just be [['', '']].
Parameters
----------
styles : list
pandas.Styler translated styles.
"""
return [x for x in styles if any(any(y) for y in x["props"])]
def _pandas_style_to_css(
style_type: str,
style: Mapping[str, Any],
uuid: str,
separator: str = "_",
) -> str:
"""Convert pandas.Styler translated style to CSS.
Parameters
----------
style_type : str
Either "table_styles" or "cell_style".
style : dict
pandas.Styler translated style.
uuid : str
pandas.Styler uuid.
separator : str
A string separator used between table and cell selectors.
"""
declarations = []
for css_property, css_value in style["props"]:
declaration = str(css_property).strip() + ": " + str(css_value).strip()
declarations.append(declaration)
table_selector = f"#T_{uuid}"
# In pandas >= 1.1.0
# translated_style["cellstyle"] has the following shape:
# > [
# > {
# > "props": [("color", " black"), ("background-color", "orange"), ("", "")],
# > "selectors": ["row0_col0"]
# > }
# > ...
# > ]
cell_selectors = (
[style["selector"]] if style_type == "table_styles" else style["selectors"]
)
selectors = [
table_selector + separator + cell_selector for cell_selector in cell_selectors
]
selector = ", ".join(selectors)
declaration_block = "; ".join(declarations)
return selector + " { " + declaration_block + " }"
def _marshall_display_values(
proto: ArrowProto, df: DataFrame, styles: Mapping[str, Any]
) -> None:
"""Marshall pandas.Styler display values into an Arrow proto.
Parameters
----------
proto : proto.Arrow
Output. The protobuf for Streamlit Arrow proto.
df : pandas.DataFrame
A dataframe with original values.
styles : dict
pandas.Styler translated styles.
"""
new_df = _use_display_values(df, styles)
proto.styler.display_values = dataframe_util.convert_pandas_df_to_arrow_bytes(
new_df
)
def _use_display_values(df: DataFrame, styles: Mapping[str, Any]) -> DataFrame:
"""Create a new pandas.DataFrame where display values are used instead of original ones.
Parameters
----------
df : pandas.DataFrame
A dataframe with original values.
styles : dict
pandas.Styler translated styles.
"""
import re
# If values in a column are not of the same type, Arrow
# serialization would fail. Thus, we need to cast all values
# of the dataframe to strings before assigning them display values.
new_df = df.astype(str)
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
if "body" in styles:
rows = styles["body"]
for row in rows:
for cell in row:
if "id" in cell and (match := cell_selector_regex.match(cell["id"])):
r, c = map(int, match.groups())
# Check if the display value is an Enum type. Enum values need to be
# converted to their `.value` attribute to ensure proper serialization
# and display logic.
if isinstance(cell["display_value"], Enum):
new_df.iloc[r, c] = str(cell["display_value"].value)
else:
new_df.iloc[r, c] = str(cell["display_value"])
return new_df