278 lines
8.3 KiB
Python
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
|