diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 440cb85d1d6a6..5fe619c749d42 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1380,8 +1380,12 @@ "metadata": {}, "outputs": [], "source": [ - "style1 = df2.style.applymap(style_negative, props='color:red;')\\\n", - " .applymap(lambda v: 'opacity: 20%;' if (v < 0.3) and (v > -0.3) else None)" + "style1 = df2.style\\\n", + " .applymap(style_negative, props='color:red;')\\\n", + " .applymap(lambda v: 'opacity: 20%;' if (v < 0.3) and (v > -0.3) else None)\\\n", + " .set_table_styles([{\"selector\": \"th\", \"props\": \"color: blue;\"}])\\\n", + " .hide_index()\n", + "style1" ] }, { diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index e13e6380905f2..7f6d2d247177e 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -82,6 +82,7 @@ Styler - :meth:`Styler.to_html` omits CSSStyle rules for hidden table elements (:issue:`43619`) - Custom CSS classes can now be directly specified without string replacement (:issue:`43686`) - Bug where row trimming failed to reflect hidden rows (:issue:`43703`) + - Update and expand the export and use mechanics (:issue:`40675`) Formerly Styler relied on ``display.html.use_mathjax``, which has now been replaced by ``styler.html.mathjax``. diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9f0806c9fda43..4b3405fb95bf5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1742,32 +1742,87 @@ def set_table_attributes(self, attributes: str) -> Styler: self.table_attributes = attributes return self - def export(self) -> list[tuple[Callable, tuple, dict]]: + def export(self) -> dict[str, Any]: """ - Export the styles applied to the current ``Styler``. + Export the styles applied to the current Styler. Can be applied to a second Styler with ``Styler.use``. Returns ------- - styles : list + styles : dict See Also -------- - Styler.use: Set the styles on the current ``Styler``. + Styler.use: Set the styles on the current Styler. + Styler.copy: Create a copy of the current Styler. + + Notes + ----- + This method is designed to copy non-data dependent attributes of + one Styler to another. It differs from ``Styler.copy`` where data and + data dependent attributes are also copied. + + The following items are exported since they are not generally data dependent: + + - Styling functions added by the ``apply`` and ``applymap`` + - Whether axes and names are hidden from the display, if unambiguous. + - Table attributes + - Table styles + + The following attributes are considered data dependent and therefore not + exported: + + - Caption + - UUID + - Tooltips + - Any hidden rows or columns identified by Index labels + - Any formatting applied using ``Styler.format`` + - Any CSS classes added using ``Styler.set_td_classes`` + + Examples + -------- + + >>> styler = DataFrame([[1, 2], [3, 4]]).style + >>> styler2 = DataFrame([[9, 9, 9]]).style + >>> styler.hide_index().highlight_max(axis=1) # doctest: +SKIP + >>> export = styler.export() + >>> styler2.use(export) # doctest: +SKIP """ - return self._todo + return { + "apply": copy.copy(self._todo), + "table_attributes": self.table_attributes, + "table_styles": copy.copy(self.table_styles), + "hide_index": all(self.hide_index_), + "hide_columns": all(self.hide_columns_), + "hide_index_names": self.hide_index_names, + "hide_column_names": self.hide_column_names, + "css": copy.copy(self.css), + } - def use(self, styles: list[tuple[Callable, tuple, dict]]) -> Styler: + def use(self, styles: dict[str, Any]) -> Styler: """ - Set the styles on the current ``Styler``. + Set the styles on the current Styler. Possibly uses styles from ``Styler.export``. Parameters ---------- - styles : list - List of style functions. + styles : dict(str, Any) + List of attributes to add to Styler. Dict keys should contain only: + - "apply": list of styler functions, typically added with ``apply`` or + ``applymap``. + - "table_attributes": HTML attributes, typically added with + ``set_table_attributes``. + - "table_styles": CSS selectors and properties, typically added with + ``set_table_styles``. + - "hide_index": whether the index is hidden, typically added with + ``hide_index``, or a boolean list for hidden levels. + - "hide_columns": whether column headers are hidden, typically added with + ``hide_columns``, or a boolean list for hidden levels. + - "hide_index_names": whether index names are hidden. + - "hide_column_names": whether column header names are hidden. + - "css": the css class names used. Returns ------- @@ -1775,9 +1830,41 @@ def use(self, styles: list[tuple[Callable, tuple, dict]]) -> Styler: See Also -------- - Styler.export : Export the styles to applied to the current ``Styler``. - """ - self._todo.extend(styles) + Styler.export : Export the non data dependent attributes to the current Styler. + + Examples + -------- + + >>> styler = DataFrame([[1, 2], [3, 4]]).style + >>> styler2 = DataFrame([[9, 9, 9]]).style + >>> styler.hide_index().highlight_max(axis=1) # doctest: +SKIP + >>> export = styler.export() + >>> styler2.use(export) # doctest: +SKIP + """ + self._todo.extend(styles.get("apply", [])) + table_attributes: str = self.table_attributes or "" + obj_table_atts: str = ( + "" + if styles.get("table_attributes") is None + else str(styles.get("table_attributes")) + ) + self.set_table_attributes((table_attributes + " " + obj_table_atts).strip()) + if styles.get("table_styles"): + self.set_table_styles(styles.get("table_styles"), overwrite=False) + + for obj in ["index", "columns"]: + hide_obj = styles.get("hide_" + obj) + if hide_obj is not None: + if isinstance(hide_obj, bool): + n = getattr(self, obj).nlevels + setattr(self, "hide_" + obj + "_", [hide_obj] * n) + else: + setattr(self, "hide_" + obj + "_", hide_obj) + + self.hide_index_names = styles.get("hide_index_names", False) + self.hide_column_names = styles.get("hide_column_names", False) + if styles.get("css"): + self.css = styles.get("css") # type: ignore[assignment] return self def set_uuid(self, uuid: str) -> Styler: diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 9cc75f71bfc4b..a7a4970e33b15 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -42,6 +42,7 @@ def mi_styler(mi_df): @pytest.fixture def mi_styler_comp(mi_styler): # comprehensively add features to mi_styler + mi_styler = mi_styler._copy(deepcopy=True) mi_styler.css = {**mi_styler.css, **{"row": "ROW", "col": "COL"}} mi_styler.uuid_len = 5 mi_styler.uuid = "abcde" @@ -257,6 +258,10 @@ def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp): "cellstyle_map", # render time vars.. "cellstyle_map_columns", "cellstyle_map_index", + "template_latex", # render templates are class level + "template_html", + "template_html_style", + "template_html_table", ] if not deepcopy: # check memory locations are equal for all included attributes for attr in [a for a in styler.__dict__ if (not callable(a) and a not in excl)]: @@ -311,6 +316,10 @@ def test_clear(mi_styler_comp): "cellstyle_map_index", # execution time only "precision", # deprecated "na_rep", # deprecated + "template_latex", # render templates are class level + "template_html", + "template_html_style", + "template_html_table", ] # tests vars are not same vals on obj and clean copy before clear (except for excl) for attr in [a for a in styler.__dict__ if not (callable(a) or a in excl)]: @@ -324,6 +333,32 @@ def test_clear(mi_styler_comp): assert all(res) if hasattr(res, "__iter__") else res +def test_export(mi_styler_comp, mi_styler): + exp_attrs = [ + "_todo", + "hide_index_", + "hide_index_names", + "hide_columns_", + "hide_column_names", + "table_attributes", + "table_styles", + "css", + ] + for attr in exp_attrs: + check = getattr(mi_styler, attr) == getattr(mi_styler_comp, attr) + assert not ( + all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check + ) + + export = mi_styler_comp.export() + used = mi_styler.use(export) + for attr in exp_attrs: + check = getattr(used, attr) == getattr(mi_styler_comp, attr) + assert all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check + + used.to_html() + + def test_hide_raises(mi_styler): msg = "`subset` and `level` cannot be passed simultaneously" with pytest.raises(ValueError, match=msg):