From b32dd6f12f2942d0f213631e2f326b9426522e3a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 20:28:03 +0100 Subject: [PATCH 01/12] ENH: better input arguments --- pandas/io/formats/style.py | 40 ++++++++++++++++++++++++++- pandas/tests/io/formats/test_style.py | 26 +++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0cb9aa3bea6ab..1eb3ed6bc6d0f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1172,13 +1172,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 @@ -1203,6 +1210,14 @@ def set_table_styles(self, table_styles, axis=0, overwrite=True) -> Styler: for s in styles ] + 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) else: @@ -2025,3 +2040,26 @@ def _maybe_wrap_formatter( else: msg = f"Expected a string, got {na_rep} instead" raise TypeError(msg) + + +def _maybe_convert_css_to_tuples(style): + """ + Check if need to convert to list of tuples format: + 'color:red; border:1px solid black;' + -> [('color', 'red'), ('border','1px solid red')] + """ + if isinstance(style, str): + s = style.split(";") + try: + s = [ + (x.split(":")[0].strip(), x.split(":")[1].strip()) + for x in s + if x.strip() != "" + ] + return s + 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 6556075272308..d54c4fa98030b 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: @@ -1170,7 +1174,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 @@ -1180,6 +1184,24 @@ def test_table_styles(self): assert styler is result assert styler.table_styles == style + # GH XXXXX + 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) From faf587e3ad3c52b594183b3916fca757c83df576 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 20:38:07 +0100 Subject: [PATCH 02/12] ENH: style.ipynb updates --- doc/source/user_guide/style.ipynb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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" ] From 8718bcc5dbe3193263b42357fb842b60c776dc3a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 20:53:13 +0100 Subject: [PATCH 03/12] also to tooltips --- pandas/io/formats/style.py | 7 ++++++- pandas/tests/io/formats/test_style.py | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1eb3ed6bc6d0f..004a226238587 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1858,7 +1858,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): """ diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index d54c4fa98030b..1f3091067a5ab 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1191,16 +1191,16 @@ def test_table_styles(self): 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 = [("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 + 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') + _maybe_convert_css_to_tuples("err") def test_table_attributes(self): attributes = 'class="foo" data-bar' @@ -1922,6 +1922,18 @@ def test_tooltip_class(self): in s ) + # GH XXXXX + 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: From 283a9d9151747e954b79bdbee30457ee16f9ab9f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 20:59:23 +0100 Subject: [PATCH 04/12] doc updates --- pandas/io/formats/style.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 004a226238587..b6a64edebf925 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -279,8 +279,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 +311,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 From a8aecb6a0ab78f4b62991bbb8a799ce55c5dec30 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 21:11:27 +0100 Subject: [PATCH 05/12] issue --- pandas/tests/io/formats/test_style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 1f3091067a5ab..b705b65a3ba6e 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1184,7 +1184,7 @@ def test_table_styles(self): assert styler is result assert styler.table_styles == style - # GH XXXXX + # GH 39563 style = [{"selector": "th", "props": "foo:bar;"}] # css string format styler = self.df.style.set_table_styles(style) result = " ".join(styler.render().split()) @@ -1922,7 +1922,7 @@ def test_tooltip_class(self): in s ) - # GH XXXXX + # GH 39563 s = ( Styler(df, uuid_len=0) .set_tooltips(DataFrame([["tooltip"]])) From 20643986c825f3cebcd7998f76f7f6f2a8b3e32f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 2 Feb 2021 21:27:14 +0100 Subject: [PATCH 06/12] whats new --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index fc49177a4736b..c40586a7b82eb 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -56,6 +56,7 @@ Other enhancements - :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes. - :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. +- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments. .. --------------------------------------------------------------------------- From 351b615baf5e6ca0bed9c0bcafd099befb246453 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Feb 2021 10:15:09 +0100 Subject: [PATCH 07/12] typing --- pandas/io/formats/style.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b6a64edebf925..664060e598e94 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2049,17 +2049,19 @@ def _maybe_wrap_formatter( raise TypeError(msg) -def _maybe_convert_css_to_tuples(style): +def _maybe_convert_css_to_tuples( + style: Union[str, Sequence[Tuple[str, Union[str, int, float]]]] +): """ - Check if need to convert to list of tuples format: - 'color:red; border:1px solid black;' - -> [('color', 'red'), ('border','1px solid red')] + 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: s = [ - (x.split(":")[0].strip(), x.split(":")[1].strip()) + (x.split(":")[0].strip(), x.split(":")[1].strip()) # type: ignore for x in s if x.strip() != "" ] From 7ded508db05dd82b9e152122c6f2948349d315b2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Feb 2021 19:00:34 +0100 Subject: [PATCH 08/12] typing --- pandas/io/formats/style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 664060e598e94..4c89e138680eb 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2060,12 +2060,12 @@ def _maybe_convert_css_to_tuples( if isinstance(style, str): s = style.split(";") try: - s = [ - (x.split(":")[0].strip(), x.split(":")[1].strip()) # type: ignore + ret = [ + (x.split(":")[0].strip(), x.split(":")[1].strip()) for x in s if x.strip() != "" ] - return s + return ret except IndexError: raise ValueError( "Styles supplied as string must follow CSS rule formats, " From edaa1a0451729a939af4594bf324bd536ac0c602 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Feb 2021 20:28:46 +0100 Subject: [PATCH 09/12] typing --- pandas/io/formats/style.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4c89e138680eb..bdb89bd8959e8 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -42,7 +42,11 @@ 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 = Sequence[Dict[str, CSSProperties]] +# Union[CSSStyles, Dict[Any, CSSStyles]] try: from matplotlib import colors @@ -267,7 +271,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 @@ -1833,7 +1837,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), @@ -1847,7 +1851,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): @@ -2049,9 +2053,7 @@ def _maybe_wrap_formatter( raise TypeError(msg) -def _maybe_convert_css_to_tuples( - style: Union[str, Sequence[Tuple[str, Union[str, int, float]]]] -): +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'), From 9b56e1d3c67f2000aa586eb32192014362dfa472 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Feb 2021 20:55:46 +0100 Subject: [PATCH 10/12] typing --- pandas/io/formats/style.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index bdb89bd8959e8..ee88304b7c91e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -44,9 +44,7 @@ jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") CSSSequence = Sequence[Tuple[str, Union[str, int, float]]] CSSProperties = Union[str, CSSSequence] -CSSStyles = Sequence[Dict[str, CSSProperties]] - -# Union[CSSStyles, Dict[Any, CSSStyles]] +CSSStyles = List[Dict[str, CSSProperties]] try: from matplotlib import colors @@ -151,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, @@ -2062,12 +2060,11 @@ def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence: if isinstance(style, str): s = style.split(";") try: - ret = [ + return [ (x.split(":")[0].strip(), x.split(":")[1].strip()) for x in s if x.strip() != "" ] - return ret except IndexError: raise ValueError( "Styles supplied as string must follow CSS rule formats, " From 39a09e57d46b817e2a06ba55899433803088bb31 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Feb 2021 20:59:29 +0100 Subject: [PATCH 11/12] whats new fix --- doc/source/whatsnew/v1.3.0.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 10b8d92cfb6c3..30fac493ef5f3 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -53,10 +53,11 @@ 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. -- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments. + .. --------------------------------------------------------------------------- From d53b77b55143bfed02af6cf53881711c09cde535 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Feb 2021 21:35:05 +0100 Subject: [PATCH 12/12] typing improvemnet --- pandas/io/formats/style.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ee88304b7c91e..6eac9ba87c73d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1122,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. @@ -1199,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: @@ -1207,20 +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 ] - - table_styles = [ - { - "selector": s["selector"], - "props": _maybe_convert_css_to_tuples(s["props"]), - } - for s in table_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)