From b7fda4c49a30140e265912b2d6f3ca34f81413aa Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 6 Sep 2020 13:45:46 +0200 Subject: [PATCH 1/8] ENH: add set_data_classes method for CSS class additions --- doc/source/whatsnew/v1.2.0.rst | 2 +- pandas/io/formats/style.py | 47 ++++++++++++++++++++++++++- pandas/tests/io/formats/test_style.py | 13 ++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index ff9e803b4990a..c7265880df82d 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -104,7 +104,7 @@ Other enhancements - Added :meth:`~DataFrame.set_flags` for setting table-wide flags on a ``Series`` or ``DataFrame`` (:issue:`28394`) - :class:`Index` with object dtype supports division and multiplication (:issue:`34160`) - :meth:`DataFrame.explode` and :meth:`Series.explode` now support exploding of sets (:issue:`35614`) -- +- `Styler` now allows direct CSS class name addition to individual data cells .. _whatsnew_120.api_breaking.python: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 023557dd6494d..7d7173e6640ff 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,6 +171,8 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep + self.cell_context: Dict = {} + # display_funcs maps (row, col) -> formatting function def default_display_func(x): @@ -262,7 +264,7 @@ def format_attr(pair): idx_lengths = _get_level_lengths(self.index) col_lengths = _get_level_lengths(self.columns, hidden_columns) - cell_context = dict() + cell_context = self.cell_context n_rlvls = self.data.index.nlevels n_clvls = self.data.columns.nlevels @@ -499,6 +501,49 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style self._display_funcs[(i, j)] = formatter return self + def set_data_classes(self, classes: DataFrame) -> "Styler": + """ + Add string based CSS class names to data cells that will appear in the + `Styler` HTML result. + + Parameters + ---------- + classes : DataFrame + DataFrame containing strings that will be translated to CSS classes. Empty + strings, None, or NaN values will be ignored. DataFrame must have + identical rows and columns to the underlying `Styler` data. + + Returns + ------- + self : Styler + + Examples + -------- + >>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=['A', 'B', 'C']) + >>> classes = pd.DataFrame([ + ... ['min-num red', '', 'blue'], + ... ['red', None, 'blue max-num'] + ... ], index=df.index, columns=df.columns) + >>> df.style.set_data_classes(classes) + """ + if not ( + self.columns.equals(classes.columns) and self.index.equals(classes.index) + ): + raise ValueError( + "CSS classes DataFrame must have identical column and index labelling " + "to underlying." + ) + + mask = (classes.isna()) | (classes.eq("")) + self.cell_context["data"] = { + r: {c: [classes.iloc[r, c]]} + for r, rn in enumerate(classes.index) + for c, cn in enumerate(classes.columns) + if not mask.iloc[r, c] + } + + return self + def render(self, **kwargs) -> str: """ Render the built up styles to HTML. diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 6025649e9dbec..9d39ef849abc3 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1691,6 +1691,19 @@ def test_no_cell_ids(self): s = styler.render() # render twice to ensure ctx is not updated assert s.find('') != -1 + def test_set_data_classes(self): + df = pd.DataFrame(data=[[0, 1], [2, 3]]) + classes = pd.DataFrame( + data=[["test-class", ""], [np.nan, None]], + columns=df.columns, + index=df.index, + ) + s = Styler(df, uuid="_", cell_ids=False).set_data_classes(classes).render() + assert '0' in s + assert '1' in s + assert '2' in s + assert '3' in s + @td.skip_if_no_mpl class TestStylerMatplotlibDep: From 3ae7887dd0b8cd0215788d18867ad4412158b631 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 6 Sep 2020 14:00:36 +0200 Subject: [PATCH 2/8] clear the Styler --- pandas/io/formats/style.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 7d7173e6640ff..8477149ffdeca 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -654,6 +654,7 @@ def clear(self) -> None: Returns None. """ self.ctx.clear() + self.cell_context = {} self._todo = [] def _compute(self): From d5b4ccbdd12f18d11aaaed7786eff24dc9e62d20 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 6 Sep 2020 14:08:32 +0200 Subject: [PATCH 3/8] GH number --- doc/source/whatsnew/v1.2.0.rst | 2 +- pandas/tests/io/formats/test_style.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index c7265880df82d..458cf14cd96e1 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -104,7 +104,7 @@ Other enhancements - Added :meth:`~DataFrame.set_flags` for setting table-wide flags on a ``Series`` or ``DataFrame`` (:issue:`28394`) - :class:`Index` with object dtype supports division and multiplication (:issue:`34160`) - :meth:`DataFrame.explode` and :meth:`Series.explode` now support exploding of sets (:issue:`35614`) -- `Styler` now allows direct CSS class name addition to individual data cells +- `Styler` now allows direct CSS class name addition to individual data cells (:issue:`36159`) .. _whatsnew_120.api_breaking.python: diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 9d39ef849abc3..bae13768eb55f 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1692,6 +1692,7 @@ def test_no_cell_ids(self): assert s.find('') != -1 def test_set_data_classes(self): + # GH 36159 df = pd.DataFrame(data=[[0, 1], [2, 3]]) classes = pd.DataFrame( data=[["test-class", ""], [np.nan, None]], From 6e76c9cad06eb8876c4e24661de9bdb65b04264a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 8 Sep 2020 06:46:05 +0200 Subject: [PATCH 4/8] typing requested --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 8477149ffdeca..aef3b701ca850 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,7 +171,7 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep - self.cell_context: Dict = {} + self.cell_context: Dict[str, Any] = {} # display_funcs maps (row, col) -> formatting function From 6a644afb47af7dcdecbc6416c112198f3e30d08e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 8 Sep 2020 17:47:33 +0200 Subject: [PATCH 5/8] using reindex --- pandas/io/formats/style.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index aef3b701ca850..9b2390fdd9331 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -509,9 +509,10 @@ def set_data_classes(self, classes: DataFrame) -> "Styler": Parameters ---------- classes : DataFrame - DataFrame containing strings that will be translated to CSS classes. Empty - strings, None, or NaN values will be ignored. DataFrame must have - identical rows and columns to the underlying `Styler` data. + DataFrame containing strings that will be translated to CSS classes, + mapped by identical column and index values that must exist on the + underlying `Styler` data. None, NaN values, and empty strings will + be ignored and not affect the rendered HTML. Returns ------- @@ -519,24 +520,27 @@ def set_data_classes(self, classes: DataFrame) -> "Styler": Examples -------- - >>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=['A', 'B', 'C']) + >>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"]) >>> classes = pd.DataFrame([ - ... ['min-num red', '', 'blue'], - ... ['red', None, 'blue max-num'] + ... ["min-val red", "", "blue"], + ... ["red", None, "blue max-val"] ... ], index=df.index, columns=df.columns) >>> df.style.set_data_classes(classes) + + Using `MultiIndex` columns and a `classes` `DataFrame` as a subset of the + underlying, + + >>> df = pd.DataFrame([[1,2],[3,4]], index=["a", "b"], + ... columns=[["level0", "level0"], ["level1a", "level1b"]]) + >>> classes = pd.DataFrame(["min-val"], index=["a"], + ... columns=[["level0"],["level1a"]]) + >>> df.style.set_data_classes(classes) """ - if not ( - self.columns.equals(classes.columns) and self.index.equals(classes.index) - ): - raise ValueError( - "CSS classes DataFrame must have identical column and index labelling " - "to underlying." - ) + classes = classes.reindex_like(self.data) mask = (classes.isna()) | (classes.eq("")) self.cell_context["data"] = { - r: {c: [classes.iloc[r, c]]} + r: {c: [str(classes.iloc[r, c])]} for r, rn in enumerate(classes.index) for c, cn in enumerate(classes.columns) if not mask.iloc[r, c] From 26f15bf33e8084a9ae0959f16247c585f95745ce Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 8 Sep 2020 20:18:21 +0200 Subject: [PATCH 6/8] change function name, add examples --- pandas/io/formats/style.py | 27 ++++++++++++++++++++++----- pandas/tests/io/formats/test_style.py | 2 +- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9b2390fdd9331..06201a34542ce 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -501,10 +501,10 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style self._display_funcs[(i, j)] = formatter return self - def set_data_classes(self, classes: DataFrame) -> "Styler": + def set_td_classes(self, classes: DataFrame) -> "Styler": """ - Add string based CSS class names to data cells that will appear in the - `Styler` HTML result. + Add string based CSS class names to data cells that will appear within the + `Styler` HTML result. These classes are added within specified `` elements. Parameters ---------- @@ -525,7 +525,7 @@ def set_data_classes(self, classes: DataFrame) -> "Styler": ... ["min-val red", "", "blue"], ... ["red", None, "blue max-val"] ... ], index=df.index, columns=df.columns) - >>> df.style.set_data_classes(classes) + >>> df.style.set_td_classes(classes) Using `MultiIndex` columns and a `classes` `DataFrame` as a subset of the underlying, @@ -534,7 +534,24 @@ def set_data_classes(self, classes: DataFrame) -> "Styler": ... columns=[["level0", "level0"], ["level1a", "level1b"]]) >>> classes = pd.DataFrame(["min-val"], index=["a"], ... columns=[["level0"],["level1a"]]) - >>> df.style.set_data_classes(classes) + >>> df.style.set_td_classes(classes) + + Form of the output with new additional css classes, + + >>> df = pd.DataFrame([[1]]) + >>> css = pd.DataFrame(["other-class"]) + >>> s = Styler(df, uuid="_", cell_ids=False).set_td_classes(css) + >>> s.hide_index().render() + '' + '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '
0
1
' + """ classes = classes.reindex_like(self.data) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index bae13768eb55f..ea78cae81ae5e 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1699,7 +1699,7 @@ def test_set_data_classes(self): columns=df.columns, index=df.index, ) - s = Styler(df, uuid="_", cell_ids=False).set_data_classes(classes).render() + s = Styler(df, uuid="_", cell_ids=False).set_td_classes(classes).render() assert '0' in s assert '1' in s assert '2' in s From 8d9d0171e1b9784d6d388faa22a3859629443f6a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Sep 2020 07:31:38 +0200 Subject: [PATCH 7/8] add test --- pandas/tests/io/formats/test_style.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 0303b53f70691..eeb14e23c114a 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1691,17 +1691,24 @@ def test_no_cell_ids(self): s = styler.render() # render twice to ensure ctx is not updated assert s.find('') != -1 - def test_set_data_classes(self): + @pytest.mark.parametrize( + "classes", + [ + DataFrame( + data=[["", "test-class"], [np.nan, None]], + columns=["A", "B"], + index=["a", "b"], + ), + DataFrame(data=[["test-class"]], columns=["B"], index=["a"]), + DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]) + ], + ) + def test_set_data_classes(self, classes): # GH 36159 - df = pd.DataFrame(data=[[0, 1], [2, 3]]) - classes = pd.DataFrame( - data=[["test-class", ""], [np.nan, None]], - columns=df.columns, - index=df.index, - ) + df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"]) s = Styler(df, uuid="_", cell_ids=False).set_td_classes(classes).render() - assert '0' in s - assert '1' in s + assert '0' in s + assert '1' in s assert '2' in s assert '3' in s From 7704e61fdfdee0b20686782040856df116771349 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Sep 2020 08:49:41 +0200 Subject: [PATCH 8/8] black fix --- pandas/tests/io/formats/test_style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index eeb14e23c114a..e7583e1ce2ce2 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1700,7 +1700,7 @@ def test_no_cell_ids(self): index=["a", "b"], ), DataFrame(data=[["test-class"]], columns=["B"], index=["a"]), - DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]) + DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]), ], ) def test_set_data_classes(self, classes):