Skip to content

Commit f02d4cc

Browse files
authored
ENH: Styler css Str optional input arguments as well as List[Tuple] (#39564)
1 parent b2671cc commit f02d4cc

File tree

4 files changed

+108
-22
lines changed

4 files changed

+108
-22
lines changed

doc/source/user_guide/style.ipynb

+5-6
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@
180180
"\n",
181181
"styles = [\n",
182182
" hover(),\n",
183-
" {'selector': \"th\", 'props': [(\"font-size\", \"150%\"),\n",
184-
" (\"text-align\", \"center\")]}\n",
183+
" {'selector': \"th\", 'props': [(\"font-size\", \"150%\"), (\"text-align\", \"center\")]}\n",
185184
"]\n",
186185
"\n",
187186
"df.style.set_table_styles(styles)"
@@ -224,7 +223,7 @@
224223
"cell_type": "markdown",
225224
"metadata": {},
226225
"source": [
227-
"We can also chain all of the above by setting the `overwrite` argument to `False` so that it preserves previous settings."
226+
"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."
228227
]
229228
},
230229
{
@@ -238,13 +237,13 @@
238237
" set_table_styles(styles).\\\n",
239238
" set_table_styles({\n",
240239
" 'A': [{'selector': '',\n",
241-
" 'props': [('color', 'red')]}],\n",
240+
" 'props': 'color:red;'}],\n",
242241
" 'B': [{'selector': 'td',\n",
243-
" 'props': [('color', 'blue')]}]\n",
242+
" 'props': 'color:blue;'}]\n",
244243
" }, axis=0, overwrite=False).\\\n",
245244
" set_table_styles({\n",
246245
" 3: [{'selector': 'td',\n",
247-
" 'props': [('color', 'green')]}]\n",
246+
" 'props': 'color:green;font-weight:bold;'}]\n",
248247
" }, axis=1, overwrite=False)\n",
249248
"s"
250249
]

doc/source/whatsnew/v1.3.0.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ Other enhancements
5353
- :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`)
5454
- :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`)
5555
- :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`)
56-
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes.
56+
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`)
57+
- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`)
5758
- :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`)
5859
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
5960

61+
6062
.. ---------------------------------------------------------------------------
6163
6264
.. _whatsnew_130.notable_bug_fixes:

pandas/io/formats/style.py

+64-13
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice
4343

4444
jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
45-
45+
CSSSequence = Sequence[Tuple[str, Union[str, int, float]]]
46+
CSSProperties = Union[str, CSSSequence]
47+
CSSStyles = List[Dict[str, CSSProperties]]
4648

4749
try:
4850
from matplotlib import colors
@@ -147,7 +149,7 @@ def __init__(
147149
self,
148150
data: FrameOrSeriesUnion,
149151
precision: Optional[int] = None,
150-
table_styles: Optional[List[Dict[str, List[Tuple[str, str]]]]] = None,
152+
table_styles: Optional[CSSStyles] = None,
151153
uuid: Optional[str] = None,
152154
caption: Optional[str] = None,
153155
table_attributes: Optional[str] = None,
@@ -267,7 +269,7 @@ def set_tooltips(self, ttips: DataFrame) -> Styler:
267269
def set_tooltips_class(
268270
self,
269271
name: Optional[str] = None,
270-
properties: Optional[Sequence[Tuple[str, Union[str, int, float]]]] = None,
272+
properties: Optional[CSSProperties] = None,
271273
) -> Styler:
272274
"""
273275
Manually configure the name and/or properties of the class for
@@ -279,8 +281,8 @@ def set_tooltips_class(
279281
----------
280282
name : str, default None
281283
Name of the tooltip class used in CSS, should conform to HTML standards.
282-
properties : list-like, default None
283-
List of (attr, value) tuples; see example.
284+
properties : list-like or str, default None
285+
List of (attr, value) tuples or a valid CSS string; see example.
284286
285287
Returns
286288
-------
@@ -311,6 +313,8 @@ def set_tooltips_class(
311313
... ('visibility', 'hidden'),
312314
... ('position', 'absolute'),
313315
... ('z-index', 1)])
316+
>>> df.style.set_tooltips_class(name='tt-add',
317+
... properties='visibility:hidden; position:absolute; z-index:1;')
314318
"""
315319
self._init_tooltips()
316320
assert self.tooltips is not None # mypy requirement
@@ -1118,7 +1122,12 @@ def set_caption(self, caption: str) -> Styler:
11181122
self.caption = caption
11191123
return self
11201124

1121-
def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
1125+
def set_table_styles(
1126+
self,
1127+
table_styles: Union[Dict[Any, CSSStyles], CSSStyles],
1128+
axis: int = 0,
1129+
overwrite: bool = True,
1130+
) -> Styler:
11221131
"""
11231132
Set the table styles on a Styler.
11241133
@@ -1172,13 +1181,20 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
11721181
... 'props': [('background-color', 'yellow')]}]
11731182
... )
11741183
1184+
Or with CSS strings
1185+
1186+
>>> df.style.set_table_styles(
1187+
... [{'selector': 'tr:hover',
1188+
... 'props': 'background-color: yellow; font-size: 1em;']}]
1189+
... )
1190+
11751191
Adding column styling by name
11761192
11771193
>>> df.style.set_table_styles({
11781194
... 'A': [{'selector': '',
11791195
... 'props': [('color', 'red')]}],
11801196
... 'B': [{'selector': 'td',
1181-
... 'props': [('color', 'blue')]}]
1197+
... 'props': 'color: blue;']}]
11821198
... }, overwrite=False)
11831199
11841200
Adding row styling
@@ -1188,20 +1204,28 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler:
11881204
... 'props': [('font-size', '25px')]}]
11891205
... }, axis=1, overwrite=False)
11901206
"""
1191-
if is_dict_like(table_styles):
1207+
if isinstance(table_styles, dict):
11921208
if axis in [0, "index"]:
11931209
obj, idf = self.data.columns, ".col"
11941210
else:
11951211
obj, idf = self.data.index, ".row"
11961212

11971213
table_styles = [
11981214
{
1199-
"selector": s["selector"] + idf + str(obj.get_loc(key)),
1200-
"props": s["props"],
1215+
"selector": str(s["selector"]) + idf + str(obj.get_loc(key)),
1216+
"props": _maybe_convert_css_to_tuples(s["props"]),
12011217
}
12021218
for key, styles in table_styles.items()
12031219
for s in styles
12041220
]
1221+
else:
1222+
table_styles = [
1223+
{
1224+
"selector": s["selector"],
1225+
"props": _maybe_convert_css_to_tuples(s["props"]),
1226+
}
1227+
for s in table_styles
1228+
]
12051229

12061230
if not overwrite and self.table_styles is not None:
12071231
self.table_styles.extend(table_styles)
@@ -1816,7 +1840,7 @@ class _Tooltips:
18161840

18171841
def __init__(
18181842
self,
1819-
css_props: Sequence[Tuple[str, Union[str, int, float]]] = [
1843+
css_props: CSSProperties = [
18201844
("visibility", "hidden"),
18211845
("position", "absolute"),
18221846
("z-index", 1),
@@ -1830,7 +1854,7 @@ def __init__(
18301854
self.class_name = css_name
18311855
self.class_properties = css_props
18321856
self.tt_data = tooltips
1833-
self.table_styles: List[Dict[str, Union[str, List[Tuple[str, str]]]]] = []
1857+
self.table_styles: CSSStyles = []
18341858

18351859
@property
18361860
def _class_styles(self):
@@ -1843,7 +1867,12 @@ def _class_styles(self):
18431867
-------
18441868
styles : List
18451869
"""
1846-
return [{"selector": f".{self.class_name}", "props": self.class_properties}]
1870+
return [
1871+
{
1872+
"selector": f".{self.class_name}",
1873+
"props": _maybe_convert_css_to_tuples(self.class_properties),
1874+
}
1875+
]
18471876

18481877
def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str):
18491878
"""
@@ -2025,3 +2054,25 @@ def _maybe_wrap_formatter(
20252054
else:
20262055
msg = f"Expected a string, got {na_rep} instead"
20272056
raise TypeError(msg)
2057+
2058+
2059+
def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence:
2060+
"""
2061+
Convert css-string to sequence of tuples format if needed.
2062+
'color:red; border:1px solid black;' -> [('color', 'red'),
2063+
('border','1px solid red')]
2064+
"""
2065+
if isinstance(style, str):
2066+
s = style.split(";")
2067+
try:
2068+
return [
2069+
(x.split(":")[0].strip(), x.split(":")[1].strip())
2070+
for x in s
2071+
if x.strip() != ""
2072+
]
2073+
except IndexError:
2074+
raise ValueError(
2075+
"Styles supplied as string must follow CSS rule formats, "
2076+
f"for example 'attr: val;'. {style} was given."
2077+
)
2078+
return style

pandas/tests/io/formats/test_style.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
import pandas._testing as tm
1313

1414
jinja2 = pytest.importorskip("jinja2")
15-
from pandas.io.formats.style import Styler, _get_level_lengths # isort:skip
15+
from pandas.io.formats.style import ( # isort:skip
16+
Styler,
17+
_get_level_lengths,
18+
_maybe_convert_css_to_tuples,
19+
)
1620

1721

1822
class TestStyler:
@@ -1167,7 +1171,7 @@ def test_unique_id(self):
11671171
assert np.unique(ids).size == len(ids)
11681172

11691173
def test_table_styles(self):
1170-
style = [{"selector": "th", "props": [("foo", "bar")]}]
1174+
style = [{"selector": "th", "props": [("foo", "bar")]}] # default format
11711175
styler = Styler(self.df, table_styles=style)
11721176
result = " ".join(styler.render().split())
11731177
assert "th { foo: bar; }" in result
@@ -1177,6 +1181,24 @@ def test_table_styles(self):
11771181
assert styler is result
11781182
assert styler.table_styles == style
11791183

1184+
# GH 39563
1185+
style = [{"selector": "th", "props": "foo:bar;"}] # css string format
1186+
styler = self.df.style.set_table_styles(style)
1187+
result = " ".join(styler.render().split())
1188+
assert "th { foo: bar; }" in result
1189+
1190+
def test_maybe_convert_css_to_tuples(self):
1191+
expected = [("a", "b"), ("c", "d e")]
1192+
assert _maybe_convert_css_to_tuples("a:b;c:d e;") == expected
1193+
assert _maybe_convert_css_to_tuples("a: b ;c: d e ") == expected
1194+
expected = []
1195+
assert _maybe_convert_css_to_tuples("") == expected
1196+
1197+
def test_maybe_convert_css_to_tuples_err(self):
1198+
msg = "Styles supplied as string must follow CSS rule formats"
1199+
with pytest.raises(ValueError, match=msg):
1200+
_maybe_convert_css_to_tuples("err")
1201+
11801202
def test_table_attributes(self):
11811203
attributes = 'class="foo" data-bar'
11821204
styler = Styler(self.df, table_attributes=attributes)
@@ -1897,6 +1919,18 @@ def test_tooltip_class(self):
18971919
in s
18981920
)
18991921

1922+
# GH 39563
1923+
s = (
1924+
Styler(df, uuid_len=0)
1925+
.set_tooltips(DataFrame([["tooltip"]]))
1926+
.set_tooltips_class(name="other-class", properties="color:green;color:red;")
1927+
.render()
1928+
)
1929+
assert (
1930+
"#T__ .other-class {\n color: green;\n color: red;\n "
1931+
in s
1932+
)
1933+
19001934

19011935
@td.skip_if_no_mpl
19021936
class TestStylerMatplotlibDep:

0 commit comments

Comments
 (0)