diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 114b4688fffaf..1058a270a76ba 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -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)" @@ -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." ] }, { @@ -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" ] diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 4d0384abbf0c6..30fac493ef5f3 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -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: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0cb9aa3bea6ab..6eac9ba87c73d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -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 @@ -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, @@ -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 @@ -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 ------- @@ -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 @@ -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. @@ -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 @@ -1188,7 +1204,7 @@ 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: @@ -1196,12 +1212,20 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler: 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) @@ -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), @@ -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): @@ -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): """ @@ -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 diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 0e732ddc0a27b..d2c5b5b9d0b2c 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -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: @@ -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 @@ -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) @@ -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: