Skip to content

ENH: Styler css Str optional input arguments as well as List[Tuple] #39564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 5, 2021
11 changes: 5 additions & 6 deletions doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,7 @@
"\n",
"styles = [\n",
" hover(),\n",
" {'selector': \"th\", 'props': [(\"font-size\", \"150%\"),\n",
" (\"text-align\", \"center\")]}\n",
" {'selector': \"th\", 'props': [(\"font-size\", \"150%\"), (\"text-align\", \"center\")]}\n",
"]\n",
"\n",
"df.style.set_table_styles(styles)"
Expand Down Expand Up @@ -224,7 +223,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also chain all of the above by setting the `overwrite` argument to `False` so that it preserves previous settings."
"We can also chain all of the above by setting the `overwrite` argument to `False` so that it preserves previous settings. We also show the CSS string input rather than the list of tuples."
]
},
{
Expand All @@ -238,13 +237,13 @@
" set_table_styles(styles).\\\n",
" set_table_styles({\n",
" 'A': [{'selector': '',\n",
" 'props': [('color', 'red')]}],\n",
" 'props': 'color:red;'}],\n",
" 'B': [{'selector': 'td',\n",
" 'props': [('color', 'blue')]}]\n",
" 'props': 'color:blue;'}]\n",
" }, axis=0, overwrite=False).\\\n",
" set_table_styles({\n",
" 3: [{'selector': 'td',\n",
" 'props': [('color', 'green')]}]\n",
" 'props': 'color:green;font-weight:bold;'}]\n",
" }, axis=1, overwrite=False)\n",
"s"
]
Expand Down
4 changes: 3 additions & 1 deletion doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ Other enhancements
- :meth:`DataFrame.apply` can now accept non-callable DataFrame properties as strings, e.g. ``df.apply("size")``, which was already the case for :meth:`Series.apply` (:issue:`39116`)
- :meth:`Series.apply` can now accept list-like or dictionary-like arguments that aren't lists or dictionaries, e.g. ``ser.apply(np.array(["sum", "mean"]))``, which was already the case for :meth:`DataFrame.apply` (:issue:`39140`)
- :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`)
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes.
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`)
- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`)
- :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`)
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.


.. ---------------------------------------------------------------------------

.. _whatsnew_130.notable_bug_fixes:
Expand Down
77 changes: 64 additions & 13 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice

jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")

CSSSequence = Sequence[Tuple[str, Union[str, int, float]]]
CSSProperties = Union[str, CSSSequence]
CSSStyles = List[Dict[str, CSSProperties]]

try:
from matplotlib import colors
Expand Down Expand Up @@ -147,7 +149,7 @@ def __init__(
self,
data: FrameOrSeriesUnion,
precision: Optional[int] = None,
table_styles: Optional[List[Dict[str, List[Tuple[str, str]]]]] = None,
table_styles: Optional[CSSStyles] = None,
uuid: Optional[str] = None,
caption: Optional[str] = None,
table_attributes: Optional[str] = None,
Expand Down Expand Up @@ -267,7 +269,7 @@ def set_tooltips(self, ttips: DataFrame) -> Styler:
def set_tooltips_class(
self,
name: Optional[str] = None,
properties: Optional[Sequence[Tuple[str, Union[str, int, float]]]] = None,
properties: Optional[CSSProperties] = None,
) -> Styler:
"""
Manually configure the name and/or properties of the class for
Expand All @@ -279,8 +281,8 @@ def set_tooltips_class(
----------
name : str, default None
Name of the tooltip class used in CSS, should conform to HTML standards.
properties : list-like, default None
List of (attr, value) tuples; see example.
properties : list-like or str, default None
List of (attr, value) tuples or a valid CSS string; see example.

Returns
-------
Expand Down Expand Up @@ -311,6 +313,8 @@ def set_tooltips_class(
... ('visibility', 'hidden'),
... ('position', 'absolute'),
... ('z-index', 1)])
>>> df.style.set_tooltips_class(name='tt-add',
... properties='visibility:hidden; position:absolute; z-index:1;')
"""
self._init_tooltips()
assert self.tooltips is not None # mypy requirement
Expand Down Expand Up @@ -1118,7 +1122,12 @@ def set_caption(self, caption: str) -> Styler:
self.caption = caption
return self

def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
def set_table_styles(
self,
table_styles: Union[Dict[Any, CSSStyles], CSSStyles],
axis: int = 0,
overwrite: bool = True,
) -> Styler:
"""
Set the table styles on a Styler.

Expand Down Expand Up @@ -1172,13 +1181,20 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
... 'props': [('background-color', 'yellow')]}]
... )

Or with CSS strings

>>> df.style.set_table_styles(
... [{'selector': 'tr:hover',
... 'props': 'background-color: yellow; font-size: 1em;']}]
... )

Adding column styling by name

>>> df.style.set_table_styles({
... 'A': [{'selector': '',
... 'props': [('color', 'red')]}],
... 'B': [{'selector': 'td',
... 'props': [('color', 'blue')]}]
... 'props': 'color: blue;']}]
... }, overwrite=False)

Adding row styling
Expand All @@ -1188,20 +1204,28 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
... 'props': [('font-size', '25px')]}]
... }, axis=1, overwrite=False)
"""
if is_dict_like(table_styles):
if isinstance(table_styles, dict):
if axis in [0, "index"]:
obj, idf = self.data.columns, ".col"
else:
obj, idf = self.data.index, ".row"

table_styles = [
{
"selector": s["selector"] + idf + str(obj.get_loc(key)),
"props": s["props"],
"selector": str(s["selector"]) + idf + str(obj.get_loc(key)),
"props": _maybe_convert_css_to_tuples(s["props"]),
}
for key, styles in table_styles.items()
for s in styles
]
else:
table_styles = [
{
"selector": s["selector"],
"props": _maybe_convert_css_to_tuples(s["props"]),
}
for s in table_styles
]

if not overwrite and self.table_styles is not None:
self.table_styles.extend(table_styles)
Expand Down Expand Up @@ -1816,7 +1840,7 @@ class _Tooltips:

def __init__(
self,
css_props: Sequence[Tuple[str, Union[str, int, float]]] = [
css_props: CSSProperties = [
("visibility", "hidden"),
("position", "absolute"),
("z-index", 1),
Expand All @@ -1830,7 +1854,7 @@ def __init__(
self.class_name = css_name
self.class_properties = css_props
self.tt_data = tooltips
self.table_styles: List[Dict[str, Union[str, List[Tuple[str, str]]]]] = []
self.table_styles: CSSStyles = []

@property
def _class_styles(self):
Expand All @@ -1843,7 +1867,12 @@ def _class_styles(self):
-------
styles : List
"""
return [{"selector": f".{self.class_name}", "props": self.class_properties}]
return [
{
"selector": f".{self.class_name}",
"props": _maybe_convert_css_to_tuples(self.class_properties),
}
]

def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str):
"""
Expand Down Expand Up @@ -2025,3 +2054,25 @@ def _maybe_wrap_formatter(
else:
msg = f"Expected a string, got {na_rep} instead"
raise TypeError(msg)


def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence:
"""
Convert css-string to sequence of tuples format if needed.
'color:red; border:1px solid black;' -> [('color', 'red'),
('border','1px solid red')]
"""
if isinstance(style, str):
s = style.split(";")
try:
return [
(x.split(":")[0].strip(), x.split(":")[1].strip())
for x in s
if x.strip() != ""
]
except IndexError:
raise ValueError(
"Styles supplied as string must follow CSS rule formats, "
f"for example 'attr: val;'. {style} was given."
)
return style
38 changes: 36 additions & 2 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import pandas._testing as tm

jinja2 = pytest.importorskip("jinja2")
from pandas.io.formats.style import Styler, _get_level_lengths # isort:skip
from pandas.io.formats.style import ( # isort:skip
Styler,
_get_level_lengths,
_maybe_convert_css_to_tuples,
)


class TestStyler:
Expand Down Expand Up @@ -1167,7 +1171,7 @@ def test_unique_id(self):
assert np.unique(ids).size == len(ids)

def test_table_styles(self):
style = [{"selector": "th", "props": [("foo", "bar")]}]
style = [{"selector": "th", "props": [("foo", "bar")]}] # default format
styler = Styler(self.df, table_styles=style)
result = " ".join(styler.render().split())
assert "th { foo: bar; }" in result
Expand All @@ -1177,6 +1181,24 @@ def test_table_styles(self):
assert styler is result
assert styler.table_styles == style

# GH 39563
style = [{"selector": "th", "props": "foo:bar;"}] # css string format
styler = self.df.style.set_table_styles(style)
result = " ".join(styler.render().split())
assert "th { foo: bar; }" in result

def test_maybe_convert_css_to_tuples(self):
expected = [("a", "b"), ("c", "d e")]
assert _maybe_convert_css_to_tuples("a:b;c:d e;") == expected
assert _maybe_convert_css_to_tuples("a: b ;c: d e ") == expected
expected = []
assert _maybe_convert_css_to_tuples("") == expected

def test_maybe_convert_css_to_tuples_err(self):
msg = "Styles supplied as string must follow CSS rule formats"
with pytest.raises(ValueError, match=msg):
_maybe_convert_css_to_tuples("err")

def test_table_attributes(self):
attributes = 'class="foo" data-bar'
styler = Styler(self.df, table_attributes=attributes)
Expand Down Expand Up @@ -1897,6 +1919,18 @@ def test_tooltip_class(self):
in s
)

# GH 39563
s = (
Styler(df, uuid_len=0)
.set_tooltips(DataFrame([["tooltip"]]))
.set_tooltips_class(name="other-class", properties="color:green;color:red;")
.render()
)
assert (
"#T__ .other-class {\n color: green;\n color: red;\n "
in s
)


@td.skip_if_no_mpl
class TestStylerMatplotlibDep:
Expand Down