From ece885514892ec377bdc4b1979d63985e5102872 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Jun 2021 14:36:32 +0200 Subject: [PATCH 01/20] add apply across index --- pandas/io/formats/style.py | 67 ++++++++++++++++++++++ pandas/io/formats/style_render.py | 16 +++++- pandas/io/formats/templates/html_style.tpl | 7 +++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 93c3843b36846..91ab211eb6b59 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -915,6 +915,27 @@ def _update_ctx(self, attrs: DataFrame) -> None: i, j = self.index.get_loc(rn), self.columns.get_loc(cn) self.ctx[(i, j)].extend(css_list) + def _update_ctx_index(self, attrs: DataFrame) -> None: + """ + Update the state of the ``Styler`` for index cells. + + Collects a mapping of {index_label: [('', ''), ..]}. + + Parameters + ---------- + attrs : Series + Should contain strings of ': ;: ', and an + integer index. + Whitespace shouldn't matter and the final trailing ';' shouldn't + matter. + """ + for j in attrs.columns: + for i, c in attrs[[j]].itertuples(): + if not c: + continue + css_list = maybe_convert_css_to_tuples(c) + self.ctx_index[(i, j)].extend(css_list) + def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( self.data, @@ -1091,6 +1112,52 @@ def apply( ) return self + def _apply_index( + self, func: Callable[..., Styler], levels: list(int) | None = None, **kwargs + ) -> Styler: + if isinstance(self.index, pd.MultiIndex) and levels is not None: + levels = [levels] if isinstance(levels, int) else levels + data = DataFrame(self.index.to_list()).loc[:, levels] + else: + data = DataFrame(self.index.to_list()) + result = data.apply(func, axis=0, **kwargs) + self._update_ctx_index(result) + return self + + def apply_index( + self, + func: Callable[..., Styler], + levels: list(int) | int | None = None, + **kwargs, + ) -> Styler: + """ + Apply a CSS-styling function to the index. + + Updates the HTML representation with the result. + + .. versionadded:: 1.3.0 + + Parameters + ---------- + func : function + ``func`` should take a Series + + .. versionchanged:: 1.3.0 + + levels : int, list of ints, optional + If index is MultiIndex the level(s) over which to apply the function. + **kwargs : dict + Pass along to ``func``. + + Returns + ------- + self : Styler + """ + self._todo.append( + (lambda instance: getattr(instance, "_apply_index"), (func, levels), kwargs) + ) + return self + def _applymap( self, func: Callable, subset: Subset | None = None, **kwargs ) -> Styler: diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 7686d8a340c37..55d3900b0ef80 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -100,6 +100,7 @@ def __init__( self.hidden_index: bool = False self.hidden_columns: Sequence[int] = [] self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) + self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str) self._todo: list[tuple[Callable, tuple, dict]] = [] self.tooltips: Tooltips | None = None @@ -146,6 +147,7 @@ def _compute(self): (application method, *args, **kwargs) """ self.ctx.clear() + self.ctx_index.clear() r = self for func, args, kwargs in self._todo: r = func(self)(*args, **kwargs) @@ -209,6 +211,9 @@ def _translate(self, sparse_index: bool, sparse_cols: bool, blank: str = "  self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict( list ) + self.cellstyle_map_index: DefaultDict[ + tuple[CSSPair, ...], list[str] + ] = defaultdict(list) body = self._translate_body( DATA_CLASS, ROW_HEADING_CLASS, @@ -224,7 +229,11 @@ def _translate(self, sparse_index: bool, sparse_cols: bool, blank: str = "  {"props": list(props), "selectors": selectors} for props, selectors in self.cellstyle_map.items() ] - d.update({"cellstyle": cellstyle}) + cellstyle_index: list[dict[str, CSSList | list[str]]] = [ + {"props": list(props), "selectors": selectors} + for props, selectors in self.cellstyle_map_index.items() + ] + d.update({"cellstyle": cellstyle, "cellstyle_index": cellstyle_index}) table_attr = self.table_attributes use_mathjax = get_option("display.html.use_mathjax") @@ -472,6 +481,11 @@ def _translate_body( ) for c, value in enumerate(rlabels[r]) ] + for c, _ in enumerate(rlabels[r]): # add for index css id styling + if (r, c) in self.ctx_index and self.ctx_index[r, c]: # if non-empty + self.cellstyle_map_index[tuple(self.ctx_index[r, c])].append( + f"level{c}_row{r}" + ) data = [] for c, value in enumerate(row_tup[1:]): diff --git a/pandas/io/formats/templates/html_style.tpl b/pandas/io/formats/templates/html_style.tpl index b34893076bedd..5873b1c909a63 100644 --- a/pandas/io/formats/templates/html_style.tpl +++ b/pandas/io/formats/templates/html_style.tpl @@ -19,6 +19,13 @@ {% endfor %} } {% endfor %} +{% for s in cellstyle_index %} +{% for selector in s.selectors %}{% if not loop.first %}, {% endif %}#T_{{uuid}}{{selector}}{% endfor %} { +{% for p,val in s.props %} + {{p}}: {{val}}; +{% endfor %} +} +{% endfor %} {% endblock cellstyle %} {% endblock style %} From a3a88e5cc0a191ed3fa4e0865b9cbb3541534dff Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Jun 2021 14:48:19 +0200 Subject: [PATCH 02/20] add applymap across index --- pandas/io/formats/style.py | 60 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 91ab211eb6b59..a1ae42eaa865f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1113,7 +1113,10 @@ def apply( return self def _apply_index( - self, func: Callable[..., Styler], levels: list(int) | None = None, **kwargs + self, + func: Callable[..., Styler], + levels: list[int] | int | None = None, + **kwargs, ) -> Styler: if isinstance(self.index, pd.MultiIndex) and levels is not None: levels = [levels] if isinstance(levels, int) else levels @@ -1127,7 +1130,7 @@ def _apply_index( def apply_index( self, func: Callable[..., Styler], - levels: list(int) | int | None = None, + levels: list[int] | int | None = None, **kwargs, ) -> Styler: """ @@ -1224,6 +1227,59 @@ def applymap( ) return self + def _applymap_index( + self, + func: Callable[..., Styler], + levels: list[int] | int | None = None, + **kwargs, + ) -> Styler: + if isinstance(self.index, pd.MultiIndex) and levels is not None: + levels = [levels] if isinstance(levels, int) else levels + data = DataFrame(self.index.to_list()).loc[:, levels] + else: + data = DataFrame(self.index.to_list()) + result = data.applymap(func, **kwargs) + self._update_ctx_index(result) + return self + + def applymap_index( + self, + func: Callable[..., Styler], + levels: list[int] | int | None = None, + **kwargs, + ) -> Styler: + """ + Apply a CSS-styling function to the index, element-wise. + + Updates the HTML representation with the result. + + .. versionadded:: 1.3.0 + + Parameters + ---------- + func : function + ``func`` should take a Series + + .. versionchanged:: 1.3.0 + + levels : int, list of ints, optional + If index is MultiIndex the level(s) over which to apply the function. + **kwargs : dict + Pass along to ``func``. + + Returns + ------- + self : Styler + """ + self._todo.append( + ( + lambda instance: getattr(instance, "_applymap_index"), + (func, levels), + kwargs, + ) + ) + return self + def where( self, cond: Callable, From 066e4f3b910769b206668099426f6909c057e54d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Jun 2021 16:06:40 +0200 Subject: [PATCH 03/20] improve docs --- pandas/io/formats/style.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a1ae42eaa865f..25c2779be90de 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1138,15 +1138,12 @@ def apply_index( Updates the HTML representation with the result. - .. versionadded:: 1.3.0 + .. versionadded:: 1.4.0 Parameters ---------- func : function - ``func`` should take a Series - - .. versionchanged:: 1.3.0 - + ``func`` should take a Series, being the index or level of a MultiIndex. levels : int, list of ints, optional If index is MultiIndex the level(s) over which to apply the function. **kwargs : dict @@ -1253,15 +1250,12 @@ def applymap_index( Updates the HTML representation with the result. - .. versionadded:: 1.3.0 + .. versionadded:: 1.4.0 Parameters ---------- func : function ``func`` should take a Series - - .. versionchanged:: 1.3.0 - levels : int, list of ints, optional If index is MultiIndex the level(s) over which to apply the function. **kwargs : dict From 5e4c1c0144d9825eea6b445b83b2005d77fe1417 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 12 Jun 2021 18:52:44 +0200 Subject: [PATCH 04/20] add column header styling and amend tests --- pandas/io/formats/style.py | 68 ++++--- pandas/io/formats/style_render.py | 63 +++++-- pandas/io/formats/templates/html_style.tpl | 9 +- pandas/io/formats/templates/html_table.tpl | 4 +- pandas/tests/io/formats/style/test_html.py | 9 +- pandas/tests/io/formats/style/test_style.py | 194 +++++--------------- 6 files changed, 137 insertions(+), 210 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 25c2779be90de..b714a4c41df31 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -915,7 +915,7 @@ def _update_ctx(self, attrs: DataFrame) -> None: i, j = self.index.get_loc(rn), self.columns.get_loc(cn) self.ctx[(i, j)].extend(css_list) - def _update_ctx_index(self, attrs: DataFrame) -> None: + def _update_ctx_header(self, attrs: DataFrame, axis: str) -> None: """ Update the state of the ``Styler`` for index cells. @@ -928,13 +928,18 @@ def _update_ctx_index(self, attrs: DataFrame) -> None: integer index. Whitespace shouldn't matter and the final trailing ';' shouldn't matter. + axis : str + Identifies whether the ctx object being updated is the index or columns """ for j in attrs.columns: for i, c in attrs[[j]].itertuples(): if not c: continue css_list = maybe_convert_css_to_tuples(c) - self.ctx_index[(i, j)].extend(css_list) + if axis == "index": + self.ctx_index[(i, j)].extend(css_list) + else: + self.ctx_columns[(j, i)].extend(css_list) def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( @@ -1112,24 +1117,41 @@ def apply( ) return self - def _apply_index( + def _apply_header( self, func: Callable[..., Styler], + axis: int | str = 0, levels: list[int] | int | None = None, + method: str = "apply", **kwargs, ) -> Styler: - if isinstance(self.index, pd.MultiIndex) and levels is not None: + if axis in [0, "index"]: + obj, axis = self.index, "index" + elif axis in [1, "columns"]: + obj, axis = self.columns, "columns" + else: + raise ValueError( + f"`axis` must be one of 0, 1, 'index', 'columns', got {axis}" + ) + + if isinstance(obj, pd.MultiIndex) and levels is not None: levels = [levels] if isinstance(levels, int) else levels - data = DataFrame(self.index.to_list()).loc[:, levels] + data = DataFrame(obj.to_list()).loc[:, levels] else: - data = DataFrame(self.index.to_list()) - result = data.apply(func, axis=0, **kwargs) - self._update_ctx_index(result) + data = DataFrame(obj.to_list()) + + if method == "apply": + result = data.apply(func, axis=0, **kwargs) + elif method == "applymap": + result = data.applymap(func, **kwargs) + + self._update_ctx_header(result, axis) return self - def apply_index( + def apply_header( self, func: Callable[..., Styler], + axis: int | str = 0, levels: list[int] | int | None = None, **kwargs, ) -> Styler: @@ -1154,7 +1176,11 @@ def apply_index( self : Styler """ self._todo.append( - (lambda instance: getattr(instance, "_apply_index"), (func, levels), kwargs) + ( + lambda instance: getattr(instance, "_apply_header"), + (func, axis, levels, "apply"), + kwargs, + ) ) return self @@ -1224,24 +1250,10 @@ def applymap( ) return self - def _applymap_index( - self, - func: Callable[..., Styler], - levels: list[int] | int | None = None, - **kwargs, - ) -> Styler: - if isinstance(self.index, pd.MultiIndex) and levels is not None: - levels = [levels] if isinstance(levels, int) else levels - data = DataFrame(self.index.to_list()).loc[:, levels] - else: - data = DataFrame(self.index.to_list()) - result = data.applymap(func, **kwargs) - self._update_ctx_index(result) - return self - - def applymap_index( + def applymap_header( self, func: Callable[..., Styler], + axis: int | str = 0, levels: list[int] | int | None = None, **kwargs, ) -> Styler: @@ -1267,8 +1279,8 @@ def applymap_index( """ self._todo.append( ( - lambda instance: getattr(instance, "_applymap_index"), - (func, levels), + lambda instance: getattr(instance, "_apply_header"), + (func, axis, levels, "applymap"), kwargs, ) ) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 55d3900b0ef80..9f8ecaa8a4925 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -101,6 +101,7 @@ def __init__( self.hidden_columns: Sequence[int] = [] self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) + self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str) self._todo: list[tuple[Callable, tuple, dict]] = [] self.tooltips: Tooltips | None = None @@ -148,6 +149,7 @@ def _compute(self): """ self.ctx.clear() self.ctx_index.clear() + self.ctx_columns.clear() r = self for func, args, kwargs in self._todo: r = func(self)(*args, **kwargs) @@ -197,6 +199,9 @@ def _translate(self, sparse_index: bool, sparse_cols: bool, blank: str = "  len(self.data.index), len(self.data.columns), max_elements ) + self.cellstyle_map_columns: DefaultDict[ + tuple[CSSPair, ...], list[str] + ] = defaultdict(list) head = self._translate_header( BLANK_CLASS, BLANK_VALUE, @@ -233,7 +238,17 @@ def _translate(self, sparse_index: bool, sparse_cols: bool, blank: str = "  {"props": list(props), "selectors": selectors} for props, selectors in self.cellstyle_map_index.items() ] - d.update({"cellstyle": cellstyle, "cellstyle_index": cellstyle_index}) + cellstyle_columns: list[dict[str, CSSList | list[str]]] = [ + {"props": list(props), "selectors": selectors} + for props, selectors in self.cellstyle_map_columns.items() + ] + d.update( + { + "cellstyle": cellstyle, + "cellstyle_index": cellstyle_index, + "cellstyle_columns": cellstyle_columns, + } + ) table_attr = self.table_attributes use_mathjax = get_option("display.html.use_mathjax") @@ -322,8 +337,9 @@ def _translate_header( ] if clabels: - column_headers = [ - _element( + column_headers = [] + for c, value in enumerate(clabels[r]): + header_element = _element( "th", f"{col_heading_class} level{r} col{c}", value, @@ -334,8 +350,16 @@ def _translate_header( else "" ), ) - for c, value in enumerate(clabels[r]) - ] + + if self.cell_ids: + header_element["id"] = f"level{r}_col{c}" + if (r, c) in self.ctx_columns and self.ctx_columns[r, c]: + header_element["id"] = f"level{r}_col{c}" + self.cellstyle_map_columns[ + tuple(self.ctx_columns[r, c]) + ].append(f"level{r}_col{c}") + + column_headers.append(header_element) if len(self.data.columns) > max_cols: # add an extra column with `...` value to indicate trimming @@ -466,27 +490,31 @@ def _translate_body( body.append(index_headers + data) break - index_headers = [ - _element( + index_headers = [] + for c, value in enumerate(rlabels[r]): + header_element = _element( "th", f"{row_heading_class} level{c} row{r}", value, (_is_visible(r, c, idx_lengths) and not self.hidden_index), - id=f"level{c}_row{r}", attributes=( f'rowspan="{idx_lengths.get((c, r), 0)}"' if idx_lengths.get((c, r), 0) > 1 else "" ), ) - for c, value in enumerate(rlabels[r]) - ] - for c, _ in enumerate(rlabels[r]): # add for index css id styling - if (r, c) in self.ctx_index and self.ctx_index[r, c]: # if non-empty + + if self.cell_ids: + header_element["id"] = f"level{c}_row{r}" # id is specified + if (r, c) in self.ctx_index and self.ctx_index[r, c]: + # always add id if a style is specified + header_element["id"] = f"level{c}_row{r}" self.cellstyle_map_index[tuple(self.ctx_index[r, c])].append( f"level{c}_row{r}" ) + index_headers.append(header_element) + data = [] for c, value in enumerate(row_tup[1:]): if c >= max_cols: @@ -515,13 +543,12 @@ def _translate_body( display_value=self._display_funcs[(r, c)](value), ) - # only add an id if the cell has a style - if self.cell_ids or (r, c) in self.ctx: + if self.cell_ids: data_element["id"] = f"row{r}_col{c}" - if (r, c) in self.ctx and self.ctx[r, c]: # only add if non-empty - self.cellstyle_map[tuple(self.ctx[r, c])].append( - f"row{r}_col{c}" - ) + if (r, c) in self.ctx and self.ctx[r, c]: + # always add id if needed due to specified style + data_element["id"] = f"row{r}_col{c}" + self.cellstyle_map[tuple(self.ctx[r, c])].append(f"row{r}_col{c}") data.append(data_element) diff --git a/pandas/io/formats/templates/html_style.tpl b/pandas/io/formats/templates/html_style.tpl index 5873b1c909a63..5b0e7a2ed882b 100644 --- a/pandas/io/formats/templates/html_style.tpl +++ b/pandas/io/formats/templates/html_style.tpl @@ -12,19 +12,14 @@ {% endblock table_styles %} {% block before_cellstyle %}{% endblock before_cellstyle %} {% block cellstyle %} -{% for s in cellstyle %} +{% for cs in [cellstyle, cellstyle_index, cellstyle_columns] %} +{% for s in cs %} {% for selector in s.selectors %}{% if not loop.first %}, {% endif %}#T_{{uuid}}{{selector}}{% endfor %} { {% for p,val in s.props %} {{p}}: {{val}}; {% endfor %} } {% endfor %} -{% for s in cellstyle_index %} -{% for selector in s.selectors %}{% if not loop.first %}, {% endif %}#T_{{uuid}}{{selector}}{% endfor %} { -{% for p,val in s.props %} - {{p}}: {{val}}; -{% endfor %} -} {% endfor %} {% endblock cellstyle %} diff --git a/pandas/io/formats/templates/html_table.tpl b/pandas/io/formats/templates/html_table.tpl index 33153af6f0882..3e3a40b9fdaa6 100644 --- a/pandas/io/formats/templates/html_table.tpl +++ b/pandas/io/formats/templates/html_table.tpl @@ -27,7 +27,7 @@ {% else %} {% for c in r %} {% if c.is_visible != False %} - <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}} + <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.value}} {% endif %} {% endfor %} {% endif %} @@ -49,7 +49,7 @@ {% endif %}{% endfor %} {% else %} {% for c in r %}{% if c.is_visible != False %} - <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} + <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} {% endif %}{% endfor %} {% endif %} diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 74b4c7ea3977c..29bcf339e5a56 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -97,7 +97,7 @@ def test_w3_html_format(styler):   - A + A @@ -127,10 +127,7 @@ def test_rowspan_w3(): # GH 38533 df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]]) styler = Styler(df, uuid="_", cell_ids=False) - assert ( - 'l0' in styler.render() - ) + assert 'l0' in styler.render() def test_styles(styler): @@ -154,7 +151,7 @@ def test_styles(styler):   - A + A diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 281170ab6c7cb..61ebb1eb09f8e 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -393,161 +393,58 @@ def test_empty_index_name_doesnt_display(self): # https://github.com/pandas-dev/pandas/pull/12090#issuecomment-180695902 df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) result = df.style._translate(True, True) - - expected = [ - [ - { - "class": "blank level0", - "type": "th", - "value": self.blank_value, - "is_visible": True, - "display_value": self.blank_value, - }, - { - "class": "col_heading level0 col0", - "display_value": "A", - "type": "th", - "value": "A", - "is_visible": True, - "attributes": "", - }, - { - "class": "col_heading level0 col1", - "display_value": "B", - "type": "th", - "value": "B", - "is_visible": True, - "attributes": "", - }, - { - "class": "col_heading level0 col2", - "display_value": "C", - "type": "th", - "value": "C", - "is_visible": True, - "attributes": "", - }, - ] - ] - - assert result["head"] == expected + assert len(result["head"]) == 1 + expected = { + "class": "blank level0", + "type": "th", + "value": self.blank_value, + "is_visible": True, + "display_value": self.blank_value, + } + assert expected.items() <= result["head"][0][0].items() def test_index_name(self): # https://github.com/pandas-dev/pandas/issues/11655 - # TODO: this test can be minimised to address the test more directly df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) result = df.set_index("A").style._translate(True, True) - - expected = [ - [ - { - "class": "blank level0", - "type": "th", - "value": self.blank_value, - "display_value": self.blank_value, - "is_visible": True, - }, - { - "class": "col_heading level0 col0", - "type": "th", - "value": "B", - "display_value": "B", - "is_visible": True, - "attributes": "", - }, - { - "class": "col_heading level0 col1", - "type": "th", - "value": "C", - "display_value": "C", - "is_visible": True, - "attributes": "", - }, - ], - [ - { - "class": "index_name level0", - "type": "th", - "value": "A", - "is_visible": True, - "display_value": "A", - }, - { - "class": "blank col0", - "type": "th", - "value": self.blank_value, - "is_visible": True, - "display_value": self.blank_value, - }, - { - "class": "blank col1", - "type": "th", - "value": self.blank_value, - "is_visible": True, - "display_value": self.blank_value, - }, - ], - ] - - assert result["head"] == expected + expected = { + "class": "index_name level0", + "type": "th", + "value": "A", + "is_visible": True, + "display_value": "A", + } + assert expected.items() <= result["head"][1][0].items() def test_multiindex_name(self): # https://github.com/pandas-dev/pandas/issues/11655 - # TODO: this test can be minimised to address the test more directly df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) result = df.set_index(["A", "B"]).style._translate(True, True) expected = [ - [ - { - "class": "blank", - "type": "th", - "value": self.blank_value, - "display_value": self.blank_value, - "is_visible": True, - }, - { - "class": "blank level0", - "type": "th", - "value": self.blank_value, - "display_value": self.blank_value, - "is_visible": True, - }, - { - "class": "col_heading level0 col0", - "type": "th", - "value": "C", - "display_value": "C", - "is_visible": True, - "attributes": "", - }, - ], - [ - { - "class": "index_name level0", - "type": "th", - "value": "A", - "is_visible": True, - "display_value": "A", - }, - { - "class": "index_name level1", - "type": "th", - "value": "B", - "is_visible": True, - "display_value": "B", - }, - { - "class": "blank col0", - "type": "th", - "value": self.blank_value, - "is_visible": True, - "display_value": self.blank_value, - }, - ], + { + "class": "index_name level0", + "type": "th", + "value": "A", + "is_visible": True, + "display_value": "A", + }, + { + "class": "index_name level1", + "type": "th", + "value": "B", + "is_visible": True, + "display_value": "B", + }, + { + "class": "blank col0", + "type": "th", + "value": self.blank_value, + "is_visible": True, + "display_value": self.blank_value, + }, ] - - assert result["head"] == expected + assert result["head"][1] == expected def test_numeric_columns(self): # https://github.com/pandas-dev/pandas/issues/12125 @@ -1064,7 +961,6 @@ def test_mi_sparse_index_names(self): assert head == expected def test_mi_sparse_column_names(self): - # TODO this test is verbose - could be minimised df = DataFrame( np.arange(16).reshape(4, 4), index=MultiIndex.from_arrays( @@ -1075,7 +971,7 @@ def test_mi_sparse_column_names(self): [["C1", "C1", "C2", "C2"], [1, 0, 1, 0]], names=["col_0", "col_1"] ), ) - result = df.style._translate(True, True) + result = Styler(df, cell_ids=False)._translate(True, True) head = result["head"][1] expected = [ { @@ -1265,7 +1161,7 @@ def test_no_cell_ids(self): styler = Styler(df, uuid="_", cell_ids=False) styler.render() s = styler.render() # render twice to ensure ctx is not updated - assert s.find('') != -1 + assert s.find('') != -1 @pytest.mark.parametrize( "classes", @@ -1283,10 +1179,10 @@ def test_set_data_classes(self, classes): # GH 36159 df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"]) s = Styler(df, uuid_len=0, cell_ids=False).set_td_classes(classes).render() - assert '0' in s - assert '1' in s - assert '2' in s - assert '3' in s + assert '0' in s + assert '1' in s + assert '2' in s + assert '3' in s # GH 39317 s = Styler(df, uuid_len=0, cell_ids=True).set_td_classes(classes).render() assert '0' in s From 20ac7e0c9b590ec14e8c8f681959bd939cd00370 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 09:15:20 +0200 Subject: [PATCH 05/20] doc sharing --- doc/source/_static/style/appmaphead1.png | Bin 0 -> 5023 bytes doc/source/_static/style/appmaphead2.png | Bin 0 -> 7641 bytes doc/source/reference/style.rst | 2 + pandas/io/formats/style.py | 120 +++++++++++++++-------- 4 files changed, 83 insertions(+), 39 deletions(-) create mode 100644 doc/source/_static/style/appmaphead1.png create mode 100644 doc/source/_static/style/appmaphead2.png diff --git a/doc/source/_static/style/appmaphead1.png b/doc/source/_static/style/appmaphead1.png new file mode 100644 index 0000000000000000000000000000000000000000..905bcaa63e900b8577812b75eb604580150fa3c1 GIT binary patch literal 5023 zcmZuz2RNKv(_W&k8U#_2Mf9?SD60jlx5W}AdT*<(61{~e5rh>z(V}__5luc0v*Se zcaHLtA|gC?w(fY!G`DZVlx$uA6V#aCP?}p2p}=*VyYMLwK@bnmd17Ju7wOi3;~cN0 zb97K6VzKmzH<|))M*DhHJB%Hmg0TgLw?ExX(iulC;DrJhK7q)EjqV1fy#>jJ&aIs9 z9tYD8Ds2^w2Udh!UzuwrGv{{U0)X=33E5q0il?M2*?b>C)Bu|MG0x>9YolJbyC-l9 z2;miznB_3Z!LMsM=VlaF$1L12Wi{^AWaqzf7-Z+DHgUe{30pQwCHN#?u2qE z2jg-D5o1wN(a0~4Nbl7v-RNJ986J5V8XU^KB+h4ce!d6-+2_qy*9!^Pt|i>veG(#+ zoSeY62rM>psc}uj{PwRETwJ>JVY3a#9O~!aEaN^8xyPB=h)ym(5fNqYbW5Uc$|2Rs z)==7DC7B3UJX#f&lBZ4QHq20e!?eztA(q2Znx0M1Q1i)XVJ}n>twO$UI^;p!hu>|X zGfIrBJt9Hca3D`4G$KqrO^2=PnVGj-*@b@149Wg{om$ zT$oxQ4Lv(=+zVKm__)aX!pD^x^$kDN^&Zt4U*DTg5)L7_fTK%Yja4#Fkz-tUpHOxh z8~)z0UlgPuTn`Z0S{TqX7#Vh3kkFv>p7m z(L}a2oHMyD7!3`H2k72|NKtvow}Hi!JkJy4y~jpjl1Q8uAep@HJ0S-u|64)fI)Zmm zQK^=$b9f)ZOfAgCY0U7wz9YpM(P1Z@?C$J=Oo%XW=j`&=J+2Gf93>nYVnv0LckwV$ zE-^TWhaokT>aA%00j#fB4bE12YdWqahC#6%8U(c-hl?<~Mt)EZ$nP2BLqhz8{l!{n zOydx8CvV;I2FCO@s_21A#MYrTJ%WNs^yGH@9CUi|CH*4S0+tAC4#C|TLq4*yh=zWD z>-=ifIkLIMxt=)=7djtG$%O3wW%mQ|TUoaRCWwE@nkiA}BeC>Y%E5I7jCK#k|Ber`tV+MSN7CIt>ntYewF)!x;8oho?+GT5`e>O_jVTdi#v>wxS zz^~VinXCp>ta1tZE_8N0C*f=B$f6TkV zTMjcUbB8V`cdIKpDh4Q`ta4Vk`vTOelRxTKPY@JZs|Be|4)F|`4FzHphUikwOUK|@ z`pNKEIJ!imR9Bx_zqwSg6sw;*X}ckCNaIi39xYQ?RIpyb0hcVDsS+)#%)eLk(X?Il z@*ROrj}n_)zDmA{wVIy(q`6nVUBtRHe>m!;LEem9g;~X2SLSV^?dy3O|v^lgUeDi!}U|~brj;&9%k|?-($Z$4l5oKOpUV&%u(%|(>#!Sr&W`<34 zLR3z))wOcY%Ei>Ran5HxxW2A-xL(rsL3I36XO&^e(F@p`yU@AknkK)8rt9K=q*e z=bPum7my3fko(t{R}{GQxPCYlVW7m@O|pBIiCQrKF)(j6Z@589g&&m*ZJSV&SR_L& zM}f>*P_eUv+{UHgqM)UrZrPaUAnLh%QOr$|4f_u--1 zt5iY*tzN_awSLm738MzY^z<~aOwS-N&xD5%CH(mDa zuHhRfuPvu=?wQRo<7`f7taNL?`fd6%*Dk>Bx*n%Jtl?ci{(QeHCufMOwNLX3la4au56$OwY0d<;M&paVje=>%h=$-g#TWPC_QNy$j z*^}2%P%wMQOc=gy8>+6&fJaYRYwR7IBiAhdK|*5^DjqWv(TTvCDcN-?wOg z%|b}!dl$8(&u7izHk7-iJ88NwyT&;&x|}%f?o{tOT=ku%WoR{^YwI4`lb~;pVzM$N zbAy6g;+BpMaIxKfCmB2EbZp>nKa9pFLk3SKQipTWz{f3zIUOtRg}+J{r;nXQXN{{& zOx-r?ww$k~4`$bc%dSw2Gh*`uE`$(*G>RNXPlguv$;)+&NxaFd8{xt|CUVA)!c@W! z{iB1hUlvnZt9ES{Guu0kaMtC%Da1tgy-K;Ke^qp1GFd#m@kQvQ=Hmd%tKC<1{pJ@r z`{&r0EY1|pK5hNn8EN5=8Hr5^iM>F~uNb-ux;n7eb>c<-Me0tOa0ul3i`X5BNBhQm z{yQT(jR$Vd-}nP&rI|%8H-dhge@SnnIJoa5J0G}pNwdGXxivr9G|Fy%_z@X+botvt zz#EY;N+Z~zibJ#FE{_Xva{|1DvdrU*kMyRJ{65W;Bgwd847=_nVfdYCgacb8eLQCX zxS)LLiRZx|Iru5SjNm!s)?Byz+I4+A9?7rg#x~GLRx+PQ+Vu$lvpAh}T?-3a%gxPT zeU&<2oDDVeG0d+h#lv&e&r(MvE(F(3*8mw~`~zl53PAt>C({vbh&0sHfLOb^3Rv2> zS=kEsxw_w6XaN8zKgiA2)fQ<9@^f`TctQN6S^u(t-0c5|!K|RaOpwmftcIE}kb;}1 zE$ESeh=3rg3=jkYNqO4XL3E)?|AycENwYd2k?s&M*w@!rz*ktn&C?z%Bq1RI7JLYP z_>lj`g5T>I0%_^TkMLsqmy`eY1GV+C_H=YdI=Uf1fBafnxp^a{Sy}%O{b&6vPFp|6 zznKtT|4!>BLGT|9SV%w+{2$*pP^mvs2+YyX*2NI&=xU4by5W!!6O#JN{QoHaCj1A` z=x?C7$UlMqQ2ZMx1^yHMKhpVEt^ShU6iWsu1^!RrWq=eSEo=Y)A-O744(^Auo$0J` zN10~eTB`J5PY$4WUnhdW94h~esGr_i!zE&`3vUW)atVUIgm)C@0xm(9uv1+g{8H|5 z`bV^~TrYK~AIWRN0|7z~l;beWyHoi!-DRKAF?RWroW<;)7_*7=wOZektaOQ!Mti$7 z3HysI<1n5mybH3b(zx)@@E-B-@ci$Bc=SmJS>pg~UmV?Bt1ozEWu@A(hhS=UR@lyj z+O*yO&espF3QwM-H2`}WHgtBjC%%6DsiC6s_6gA5-@ipX;lAv*FH=(wZ09q@+_HRk zzj-SuDoSu_=WN}q6emL61{tryI65#8q^PKvhxzg22MgAx<$P~B0wDi(bCsMa_l=iX zyFWXV|Cd_Z6?$f7vhB*%y>_lw$5)R$?M$mP`+4WVsGU;8-m!+0{3;g)XU6w?GtQd(KaOiFB(ZArIuu|9q%rF-i zY_B0o*U-?w!po~(&V^X|$ZWhe)s`F}DOo(i)Q&a0YTk)`q=`jpeBlJ-^?F zCC}XA`?Do|_YN@X<)*Eu@rQH`_4T1roSdA5zt%^3a-;(&@7)t%eY@Te?bLK}Vp%*N zGRnZn=&?QDAcym8{3aN}(d}TPj^OsgwW3XE}e3)#edrO3+4cg^&>uJ+`qaC{{^C37E!STm-^i z%psD(6;^+r?yst7X({RIQq5GDL=U{WAA-taXqD|$xn3Q3CGP$mz{TZvx@T!)Z_ikg zZuXkNAm#FLrJAq9_0?r@aWN%xw$w8!v$eDIaSQjdAetR{Cj^aow^@16gzq9$vJr)P|%Y z7k5Sc4sBOyY3a&u;ne2VPfFB+cZQC}^>iG*u}E&yrrjRh*+KMtKxaurykr#1tFDH% zIc%z(>?~5h&P9EQVpC4=d*lsm9Q+|9E%&P&!k5;_gCGri1Q6odL-s)cD}WfXk6Zn06^h3oaiE?0i%5+}t`Dvi|Y*FEMhVk#7$%4eTG;Fx9;E+cM43}SBUmd6I;;jcMJ z-mcKrBbr?lH*|F5b})MKHArg9-b6Ard~#xj-kbx;XPY+(=TJ8`*gHWN-@7!=3`wTRnrz z)w0+zigyFsh3aXMg!%>svCyJ7Z|K{3hX?e_3}Pgrx;qZvS^BT`lWon_^msD15I1N=HzIvl@NTi|Bdp-N3L^qnxy6)1AL)$VPt=r| yED!Hl0RI0c){#0$Z}XZ}wz2~g#Q=QR$dJN literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/appmaphead2.png b/doc/source/_static/style/appmaphead2.png new file mode 100644 index 0000000000000000000000000000000000000000..9adde619083782a62984ed6e55b24ed9fcaf14bb GIT binary patch literal 7641 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 NQBDm~A!GLb{{UlZLE!)Z literal 0 HcmV?d00001 diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 5a2ff803f0323..a085d1eff4e7a 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -34,6 +34,8 @@ Style application Styler.apply Styler.applymap + Styler.apply_header + Styler.applymap_header Styler.where Styler.format Styler.set_td_classes diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b714a4c41df31..dfaa61179314d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1081,6 +1081,8 @@ def apply( See Also -------- + Styler.applymap_header: Apply a CSS-styling function to headers elementwise. + Styler.apply_header: Apply a CSS-styling function to headers level-wise. Styler.applymap: Apply a CSS-styling function elementwise. Notes @@ -1148,6 +1150,17 @@ def _apply_header( self._update_ctx_header(result, axis) return self + @doc( + this="apply", + alt="applymap", + wise="level-wise", + func="take a Series and return a string array of the same length", + input_note="the index as a Series, if an Index, or a level of a MultiIndex", + output_note="an identically sized array of CSS styles as strings", + var="s", + ret='np.where(s == "B", "background-color: yellow;", "")', + ret2='np.where(["x" in v for v in s], "background-color: yellow;", "")', + ) def apply_header( self, func: Callable[..., Styler], @@ -1156,7 +1169,7 @@ def apply_header( **kwargs, ) -> Styler: """ - Apply a CSS-styling function to the index. + Apply a CSS-styling function to the index, {wise}. Updates the HTML representation with the result. @@ -1165,7 +1178,9 @@ def apply_header( Parameters ---------- func : function - ``func`` should take a Series, being the index or level of a MultiIndex. + ``func`` should {func}. + axis : {0, 1, "index", "columns"} + The headers over which to apply the function. levels : int, list of ints, optional If index is MultiIndex the level(s) over which to apply the function. **kwargs : dict @@ -1174,6 +1189,39 @@ def apply_header( Returns ------- self : Styler + + See Also + -------- + Styler.{alt}_header: Apply a CSS-styling function to headers {wise}. + Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. + Styler.applymap: Apply a CSS-styling function elementwise. + + Notes + ----- + Each input to ``func`` will be {input_note}. The output of ``func`` should be + {output_note}, in the format 'attribute: value; attribute2: value2; ...' + or, if nothing is to be applied to that element, an empty string or ``None``. + + Examples + -------- + Basic usage to conditionally highlight values in the index. + + >>> df = pd.DataFrame([[1,2], [3,4]], index=["A", "B"]) + >>> def color_b(s): + ... return {ret} + >>> df.style.{this}_header(color_b) + + .. figure:: ../../_static/style/appmaphead1.png + + Selectively applying to specific levels of MultiIndex columns. + + >>> midx = pd.MultiIndex.from_product([['ix', 'jy'], [0, 1], ['x3', 'z4']]) + >>> df = pd.DataFrame([np.arange(8)], columns=midx) + >>> def highlight_x({var}): + ... return {ret2} + >>> df.style.{this}_header(highlight_x, axis="columns", levels=[0, 2]) + + .. figure:: ../../_static/style/appmaphead1.png """ self._todo.append( ( @@ -1184,6 +1232,34 @@ def apply_header( ) return self + @doc( + apply_header, + this="applymap", + alt="apply", + wise="elementwise", + func="take a scalar and return a string", + input_note="an index value, if an Index, or a level value of a MultiIndex", + output_note="CSS styles as a string", + var="v", + ret='"background-color: yellow;" if v == "B" else None', + ret2='"background-color: yellow;" if "x" in v else None', + ) + def applymap_header( + self, + func: Callable[..., Styler], + axis: int | str = 0, + levels: list[int] | int | None = None, + **kwargs, + ) -> Styler: + self._todo.append( + ( + lambda instance: getattr(instance, "_apply_header"), + (func, axis, levels, "applymap"), + kwargs, + ) + ) + return self + def _applymap( self, func: Callable, subset: Subset | None = None, **kwargs ) -> Styler: @@ -1206,7 +1282,7 @@ def applymap( Parameters ---------- func : function - ``func`` should take a scalar and return a scalar. + ``func`` should take a scalar and return a string. subset : label, array-like, IndexSlice, optional A valid 2d input to `DataFrame.loc[]`, or, in the case of a 1d input or single key, to `DataFrame.loc[:, ]` where the columns are @@ -1220,6 +1296,8 @@ def applymap( See Also -------- + Styler.applymap_header: Apply a CSS-styling function to headers elementwise. + Styler.apply_header: Apply a CSS-styling function to headers level-wise. Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. Notes @@ -1250,42 +1328,6 @@ def applymap( ) return self - def applymap_header( - self, - func: Callable[..., Styler], - axis: int | str = 0, - levels: list[int] | int | None = None, - **kwargs, - ) -> Styler: - """ - Apply a CSS-styling function to the index, element-wise. - - Updates the HTML representation with the result. - - .. versionadded:: 1.4.0 - - Parameters - ---------- - func : function - ``func`` should take a Series - levels : int, list of ints, optional - If index is MultiIndex the level(s) over which to apply the function. - **kwargs : dict - Pass along to ``func``. - - Returns - ------- - self : Styler - """ - self._todo.append( - ( - lambda instance: getattr(instance, "_apply_header"), - (func, axis, levels, "applymap"), - kwargs, - ) - ) - return self - def where( self, cond: Callable, From 26c53406fbf75b1a26f7f29b7e598705b8a3a7f2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 10:31:23 +0200 Subject: [PATCH 06/20] doc fix --- pandas/io/formats/style.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index dfaa61179314d..35c155a5effa2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1152,14 +1152,16 @@ def _apply_header( @doc( this="apply", - alt="applymap", wise="level-wise", + alt="applymap", + altwise="elementwise", func="take a Series and return a string array of the same length", + axis='{0, 1, "index", "columns"}', input_note="the index as a Series, if an Index, or a level of a MultiIndex", output_note="an identically sized array of CSS styles as strings", var="s", ret='np.where(s == "B", "background-color: yellow;", "")', - ret2='np.where(["x" in v for v in s], "background-color: yellow;", "")', + ret2='["background-color: yellow;" if "x" in v else "" for v in s]', ) def apply_header( self, @@ -1169,7 +1171,7 @@ def apply_header( **kwargs, ) -> Styler: """ - Apply a CSS-styling function to the index, {wise}. + Apply a CSS-styling function to the index or column headers, {wise}. Updates the HTML representation with the result. @@ -1179,7 +1181,7 @@ def apply_header( ---------- func : function ``func`` should {func}. - axis : {0, 1, "index", "columns"} + axis : {axis} The headers over which to apply the function. levels : int, list of ints, optional If index is MultiIndex the level(s) over which to apply the function. @@ -1192,7 +1194,7 @@ def apply_header( See Also -------- - Styler.{alt}_header: Apply a CSS-styling function to headers {wise}. + Styler.{alt}_header: Apply a CSS-styling function to headers {altwise}. Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. Styler.applymap: Apply a CSS-styling function elementwise. @@ -1221,7 +1223,7 @@ def apply_header( ... return {ret2} >>> df.style.{this}_header(highlight_x, axis="columns", levels=[0, 2]) - .. figure:: ../../_static/style/appmaphead1.png + .. figure:: ../../_static/style/appmaphead2.png """ self._todo.append( ( @@ -1235,9 +1237,11 @@ def apply_header( @doc( apply_header, this="applymap", - alt="apply", wise="elementwise", + alt="apply", + altwise="level-wise", func="take a scalar and return a string", + axis='{0, 1, "index", "columns"}', input_note="an index value, if an Index, or a level value of a MultiIndex", output_note="CSS styles as a string", var="v", From 2437b7277fa9df8a61b257cc42a6c2686dc11962 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 10:36:27 +0200 Subject: [PATCH 07/20] doc fix --- 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 35c155a5effa2..2171477bc0aa3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -917,7 +917,7 @@ def _update_ctx(self, attrs: DataFrame) -> None: def _update_ctx_header(self, attrs: DataFrame, axis: str) -> None: """ - Update the state of the ``Styler`` for index cells. + Update the state of the ``Styler`` for header cells. Collects a mapping of {index_label: [('', ''), ..]}. From 312a6e624bba674d1ff40a3ce87a8accd2500e8f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 14:09:33 +0200 Subject: [PATCH 08/20] collapse the cellstyle maps --- pandas/io/formats/style_render.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 9f8ecaa8a4925..9ab572654b362 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -230,25 +230,17 @@ def _translate(self, sparse_index: bool, sparse_cols: bool, blank: str = "  ) d.update({"body": body}) - cellstyle: list[dict[str, CSSList | list[str]]] = [ - {"props": list(props), "selectors": selectors} - for props, selectors in self.cellstyle_map.items() - ] - cellstyle_index: list[dict[str, CSSList | list[str]]] = [ - {"props": list(props), "selectors": selectors} - for props, selectors in self.cellstyle_map_index.items() - ] - cellstyle_columns: list[dict[str, CSSList | list[str]]] = [ - {"props": list(props), "selectors": selectors} - for props, selectors in self.cellstyle_map_columns.items() - ] - d.update( - { - "cellstyle": cellstyle, - "cellstyle_index": cellstyle_index, - "cellstyle_columns": cellstyle_columns, - } - ) + ctx_maps = { + "cellstyle": "cellstyle_map", + "cellstyle_index": "cellstyle_map_index", + "cellstyle_columns": "cellstyle_map_columns", + } # add the cell_ids styles map to the render dictionary in right format + for k, attr in ctx_maps.items(): + map = [ + {"props": list(props), "selectors": selectors} + for props, selectors in getattr(self, attr).items() + ] + d.update({k: map}) table_attr = self.table_attributes use_mathjax = get_option("display.html.use_mathjax") From 553426fef6060a386b4224726401792a7768d78e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 15:58:33 +0200 Subject: [PATCH 09/20] add basic test --- pandas/tests/io/formats/style/test_style.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 61ebb1eb09f8e..8f033f2cc7661 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -156,6 +156,33 @@ def test_render_trimming_mi(): assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items() +def test_apply_map_header_index(): + df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"]) + func = lambda s: ["attr: val" if ("A" in v or "D" in v) else "" for v in s] + func_map = lambda v: "attr: val" if ("A" in v or "D" in v) else "" + + # test over index + result = df.style.apply_header(func, axis="index") + assert len(result._todo) == 1 + assert len(result.ctx_index) == 0 + result._compute() + result_map = df.style.applymap_header(func_map, axis="index") + expected = { + (1, 0): [("attr", "val")], + } + assert result.ctx_index == expected + assert result_map.ctx_index == expected + + # test over columns + result = df.style.apply_header(func, axis="columns")._compute() + result_map = df.style.applymap_header(func_map, axis="columns")._compute() + expected = { + (0, 0): [("attr", "val")], + } + assert result.ctx_columns == expected + assert result_map.ctx_columns == expected + + class TestStyler: def setup_method(self, method): np.random.seed(24) From f01dfee5886a1cca26c91973f331eebb57f586a4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 16:23:30 +0200 Subject: [PATCH 10/20] add basic test --- pandas/tests/io/formats/style/test_style.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 8f033f2cc7661..9c62f40fe2edd 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -156,17 +156,19 @@ def test_render_trimming_mi(): assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items() -def test_apply_map_header_index(): +def test_apply_map_header(): df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"]) func = lambda s: ["attr: val" if ("A" in v or "D" in v) else "" for v in s] func_map = lambda v: "attr: val" if ("A" in v or "D" in v) else "" - # test over index + # test execution added to todo result = df.style.apply_header(func, axis="index") assert len(result._todo) == 1 assert len(result.ctx_index) == 0 + + # test over index result._compute() - result_map = df.style.applymap_header(func_map, axis="index") + result_map = df.style.applymap_header(func_map, axis="index")._compute() expected = { (1, 0): [("attr", "val")], } @@ -183,6 +185,18 @@ def test_apply_map_header_index(): assert result_map.ctx_columns == expected +@pytest.mark.parametrize("method", ["apply", "applymap"]) +@pytest.mark.parametrize("axis", ["index", "columns"]) +def test_apply_map_header_mi(mi_styler, method, axis): + func = { + "apply": lambda s: ["attr: val;" if "b" in v else "" for v in s], + "applymap": lambda v: "attr: val" if "b" in v else "", + } + result = getattr(mi_styler, method + "_header")(func[method], axis=axis)._compute() + expected = {(1, 1): [("attr", "val")]} + assert getattr(result, f"ctx_{axis}") == expected + + class TestStyler: def setup_method(self, method): np.random.seed(24) From f9401659a49cc40b3be8b6dcad03e88f676f864f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 22:16:56 +0200 Subject: [PATCH 11/20] parametrise tests --- pandas/tests/io/formats/style/test_style.py | 31 ++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 9c62f40fe2edd..2f9b0902b8889 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -156,33 +156,26 @@ def test_render_trimming_mi(): assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items() -def test_apply_map_header(): +@pytest.mark.parametrize("method", ["applymap", "apply"]) +@pytest.mark.parametrize("axis", ["index", "columns"]) +def test_apply_map_header(method, axis): df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"]) - func = lambda s: ["attr: val" if ("A" in v or "D" in v) else "" for v in s] - func_map = lambda v: "attr: val" if ("A" in v or "D" in v) else "" + func = { + "apply": lambda s: ["attr: val" if ("A" in v or "C" in v) else "" for v in s], + "applymap": lambda v: "attr: val" if ("A" in v or "C" in v) else "", + } # test execution added to todo - result = df.style.apply_header(func, axis="index") + result = getattr(df.style, f"{method}_header")(func[method], axis=axis) assert len(result._todo) == 1 - assert len(result.ctx_index) == 0 + assert len(getattr(result, f"ctx_{axis}")) == 0 - # test over index + # test ctx object on compute result._compute() - result_map = df.style.applymap_header(func_map, axis="index")._compute() - expected = { - (1, 0): [("attr", "val")], - } - assert result.ctx_index == expected - assert result_map.ctx_index == expected - - # test over columns - result = df.style.apply_header(func, axis="columns")._compute() - result_map = df.style.applymap_header(func_map, axis="columns")._compute() expected = { (0, 0): [("attr", "val")], } - assert result.ctx_columns == expected - assert result_map.ctx_columns == expected + assert getattr(result, f"ctx_{axis}") == expected @pytest.mark.parametrize("method", ["apply", "applymap"]) @@ -192,7 +185,7 @@ def test_apply_map_header_mi(mi_styler, method, axis): "apply": lambda s: ["attr: val;" if "b" in v else "" for v in s], "applymap": lambda v: "attr: val" if "b" in v else "", } - result = getattr(mi_styler, method + "_header")(func[method], axis=axis)._compute() + result = getattr(mi_styler, f"{method}_header")(func[method], axis=axis)._compute() expected = {(1, 1): [("attr", "val")]} assert getattr(result, f"ctx_{axis}") == expected From 6f5b46c8071f76c8d4153e64e066b55f15449bb8 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 22:20:09 +0200 Subject: [PATCH 12/20] test for raises ValueError --- pandas/tests/io/formats/style/test_style.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 2f9b0902b8889..c9099a18d9346 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -190,6 +190,11 @@ def test_apply_map_header_mi(mi_styler, method, axis): assert getattr(result, f"ctx_{axis}") == expected +def test_apply_map_header_raises(mi_styler): + with pytest.raises(ValueError, match="`axis` must be one of 0, 1, 'index', 'col"): + mi_styler.applymap_header(lambda v: "attr: val;", axis="bad-axis")._compute() + + class TestStyler: def setup_method(self, method): np.random.seed(24) From 75cf6ca5b95bcc0c7afd3a6665f20df97d17bc9a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 22:40:12 +0200 Subject: [PATCH 13/20] test html working --- pandas/tests/io/formats/style/test_html.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 29bcf339e5a56..e6779c6f2fbbb 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -233,3 +233,35 @@ def test_from_custom_template(tmpdir): def test_caption_as_sequence(styler): styler.set_caption(("full cap", "short cap")) assert "full cap" in styler.render() + + +@pytest.mark.parametrize("index", [True, False]) +@pytest.mark.parametrize("columns", [True, False]) +def test_applymap_header_cell_ids(styler, index, columns): + func = lambda v: "attr: val;" + styler.uuid, styler.cell_ids = "", False + if index: + styler.applymap_header(func, axis="index") + if columns: + styler.applymap_header(func, axis="columns") + + result = styler.to_html() + + # test no data cell ids + assert '2.610000' in result + assert '2.690000' in result + + # test index header ids where needed and css styles + assert ( + 'a' in result + ) is index + assert ( + 'b' in result + ) is index + assert ("#T_level0_row0, #T_level0_row1 {\n attr: val;\n}" in result) is index + + # test column header ids where needed and css styles + assert ( + 'A' in result + ) is columns + assert ("#T_level0_col0 {\n attr: val;\n}" in result) is columns From d6541393970d7005a548ea8c804614058d7991a2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Jun 2021 22:42:07 +0200 Subject: [PATCH 14/20] test html working --- pandas/tests/io/formats/style/test_html.py | 1 + pandas/tests/io/formats/style/test_style.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index e6779c6f2fbbb..183cb8f4937df 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -238,6 +238,7 @@ def test_caption_as_sequence(styler): @pytest.mark.parametrize("index", [True, False]) @pytest.mark.parametrize("columns", [True, False]) def test_applymap_header_cell_ids(styler, index, columns): + # GH 41893 func = lambda v: "attr: val;" styler.uuid, styler.cell_ids = "", False if index: diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index c9099a18d9346..7db0cbfe8ef75 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -159,6 +159,7 @@ def test_render_trimming_mi(): @pytest.mark.parametrize("method", ["applymap", "apply"]) @pytest.mark.parametrize("axis", ["index", "columns"]) def test_apply_map_header(method, axis): + # GH 41893 df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"]) func = { "apply": lambda s: ["attr: val" if ("A" in v or "C" in v) else "" for v in s], @@ -181,6 +182,7 @@ def test_apply_map_header(method, axis): @pytest.mark.parametrize("method", ["apply", "applymap"]) @pytest.mark.parametrize("axis", ["index", "columns"]) def test_apply_map_header_mi(mi_styler, method, axis): + # GH 41893 func = { "apply": lambda s: ["attr: val;" if "b" in v else "" for v in s], "applymap": lambda v: "attr: val" if "b" in v else "", @@ -191,6 +193,7 @@ def test_apply_map_header_mi(mi_styler, method, axis): def test_apply_map_header_raises(mi_styler): + # GH 41893 with pytest.raises(ValueError, match="`axis` must be one of 0, 1, 'index', 'col"): mi_styler.applymap_header(lambda v: "attr: val;", axis="bad-axis")._compute() From 17787ef66b6211e8c9ba1e20789e170c0beba5b1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 14 Jun 2021 08:18:34 +0200 Subject: [PATCH 15/20] whats new 1.4.0 --- doc/source/whatsnew/v1.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 166ea2f0d4164..eb650ed599dcd 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -29,7 +29,7 @@ enhancement2 Other enhancements ^^^^^^^^^^^^^^^^^^ -- +- :meth:`.Styler.apply_header` and :meth:`.Styler.applymap_header` added to allow conditional styling of index and column header values (:issue:`41893`) - .. --------------------------------------------------------------------------- From 8d8e88f88c71cb2472e9655ecbd618df59f5afde Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Jul 2021 19:08:36 +0200 Subject: [PATCH 16/20] update tests --- pandas/io/formats/style.py | 2 ++ pandas/tests/io/formats/style/test_style.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d7a34269cd40b..e61e6e2c751f0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1029,6 +1029,8 @@ def _copy(self, deepcopy: bool = False) -> Styler: "hidden_rows", "hidden_columns", "ctx", + "ctx_index", + "ctx_columns", "cell_context", "_todo", "table_styles", diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 751d4bcf56f0b..fb57da3523436 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -52,6 +52,8 @@ def mi_styler_comp(mi_styler): mi_styler.set_table_attributes('class="box"') mi_styler.format(na_rep="MISSING", precision=3) mi_styler.highlight_max(axis=None) + mi_styler.applymap_header(lambda x: "color: white;", axis=0) + mi_styler.applymap_header(lambda x: "color: black;", axis=1) mi_styler.set_td_classes( DataFrame( [["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns @@ -198,7 +200,14 @@ def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp): if render: styler.to_html() - excl = ["na_rep", "precision", "uuid", "cellstyle_map"] # deprecated or special var + excl = [ + "na_rep", # deprecated + "precision", # deprecated + "uuid", # special + "cellstyle_map", # render time vars.. + "cellstyle_map_columns", + "cellstyle_map_index", + ] 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)]: assert id(getattr(s2, attr)) == id(getattr(styler, attr)) @@ -245,6 +254,8 @@ def test_clear(mi_styler_comp): "uuid_len", "cell_ids", "cellstyle_map", # execution time only + "cellstyle_map_columns", # execution time only + "cellstyle_map_index", # execution time only "precision", # deprecated "na_rep", # deprecated ] From 81f3a3c04629f0c162dc3519798d8217de9aca25 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 9 Aug 2021 11:38:09 +0200 Subject: [PATCH 17/20] rename: applymap_header -->> applymap_index --- doc/source/reference/style.rst | 4 ++-- doc/source/whatsnew/v1.4.0.rst | 2 +- pandas/io/formats/style.py | 26 ++++++++++----------- pandas/io/formats/style_render.py | 1 - pandas/tests/io/formats/style/test_html.py | 4 ++-- pandas/tests/io/formats/style/test_style.py | 10 ++++---- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 73da17c7c72a3..ac4fc314c6c07 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -36,8 +36,8 @@ Style application Styler.apply Styler.applymap - Styler.apply_header - Styler.applymap_header + Styler.apply_index + Styler.applymap_index Styler.format Styler.hide_index Styler.hide_columns diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 66d21658e597d..769f3c168a4a8 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -38,7 +38,7 @@ Other enhancements - :meth:`Series.ewm`, :meth:`DataFrame.ewm`, now support a ``method`` argument with a ``'table'`` option that performs the windowing operation over an entire :class:`DataFrame`. See :ref:`Window Overview ` for performance and functional benefits (:issue:`42273`) - Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`) - Added keyword argument ``environment`` to :meth:`.Styler.to_latex` also allowing a specific "longtable" entry with a separate jinja2 template (:issue:`41866`) -- :meth:`.Styler.apply_header` and :meth:`.Styler.applymap_header` added to allow conditional styling of index and column header values (:issue:`41893`) +- :meth:`.Styler.apply_index` and :meth:`.Styler.applymap_index` added to allow conditional styling of index and column header values (:issue:`41893`) - :meth:`.GroupBy.cummin` and :meth:`.GroupBy.cummax` now support the argument ``skipna`` (:issue:`34047`) - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a08b82a5920dd..363a1186ea068 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1199,8 +1199,8 @@ def apply( See Also -------- - Styler.applymap_header: Apply a CSS-styling function to headers elementwise. - Styler.apply_header: Apply a CSS-styling function to headers level-wise. + Styler.applymap_index: Apply a CSS-styling function to headers elementwise. + Styler.apply_index: Apply a CSS-styling function to headers level-wise. Styler.applymap: Apply a CSS-styling function elementwise. Notes @@ -1244,7 +1244,7 @@ def apply( ) return self - def _apply_header( + def _apply_index( self, func: Callable[..., Styler], axis: int | str = 0, @@ -1288,7 +1288,7 @@ def _apply_header( ret='np.where(s == "B", "background-color: yellow;", "")', ret2='["background-color: yellow;" if "x" in v else "" for v in s]', ) - def apply_header( + def apply_index( self, func: Callable[..., Styler], axis: int | str = 0, @@ -1319,7 +1319,7 @@ def apply_header( See Also -------- - Styler.{alt}_header: Apply a CSS-styling function to headers {altwise}. + Styler.{alt}_index: Apply a CSS-styling function to headers {altwise}. Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. Styler.applymap: Apply a CSS-styling function elementwise. @@ -1336,7 +1336,7 @@ def apply_header( >>> df = pd.DataFrame([[1,2], [3,4]], index=["A", "B"]) >>> def color_b(s): ... return {ret} - >>> df.style.{this}_header(color_b) + >>> df.style.{this}_index(color_b) .. figure:: ../../_static/style/appmaphead1.png @@ -1346,13 +1346,13 @@ def apply_header( >>> df = pd.DataFrame([np.arange(8)], columns=midx) >>> def highlight_x({var}): ... return {ret2} - >>> df.style.{this}_header(highlight_x, axis="columns", levels=[0, 2]) + >>> df.style.{this}_index(highlight_x, axis="columns", levels=[0, 2]) .. figure:: ../../_static/style/appmaphead2.png """ self._todo.append( ( - lambda instance: getattr(instance, "_apply_header"), + lambda instance: getattr(instance, "_apply_index"), (func, axis, levels, "apply"), kwargs, ) @@ -1360,7 +1360,7 @@ def apply_header( return self @doc( - apply_header, + apply_index, this="applymap", wise="elementwise", alt="apply", @@ -1373,7 +1373,7 @@ def apply_header( ret='"background-color: yellow;" if v == "B" else None', ret2='"background-color: yellow;" if "x" in v else None', ) - def applymap_header( + def applymap_index( self, func: Callable[..., Styler], axis: int | str = 0, @@ -1382,7 +1382,7 @@ def applymap_header( ) -> Styler: self._todo.append( ( - lambda instance: getattr(instance, "_apply_header"), + lambda instance: getattr(instance, "_apply_index"), (func, axis, levels, "applymap"), kwargs, ) @@ -1425,8 +1425,8 @@ def applymap( See Also -------- - Styler.applymap_header: Apply a CSS-styling function to headers elementwise. - Styler.apply_header: Apply a CSS-styling function to headers level-wise. + Styler.applymap_index: Apply a CSS-styling function to headers elementwise. + Styler.apply_index: Apply a CSS-styling function to headers level-wise. Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. Notes diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 2e989c2d08585..e4de6615bafd5 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -500,7 +500,6 @@ def _translate_body( f"{row_heading_class} level{c} row{r}", value, (_is_visible(r, c, idx_lengths) and not self.hide_index_[c]), - id=f"level{c}_row{r}", attributes=( f'rowspan="{idx_lengths.get((c, r), 0)}"' if idx_lengths.get((c, r), 0) > 1 diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 85020d0b094f9..bcf3c4dbad3a8 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -406,9 +406,9 @@ def test_applymap_header_cell_ids(styler, index, columns): func = lambda v: "attr: val;" styler.uuid, styler.cell_ids = "", False if index: - styler.applymap_header(func, axis="index") + styler.applymap_index(func, axis="index") if columns: - styler.applymap_header(func, axis="columns") + styler.applymap_index(func, axis="columns") result = styler.to_html() diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index d178d04f70a02..d9f2eea0a7fb9 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -52,8 +52,8 @@ def mi_styler_comp(mi_styler): mi_styler.set_table_attributes('class="box"') mi_styler.format(na_rep="MISSING", precision=3) mi_styler.highlight_max(axis=None) - mi_styler.applymap_header(lambda x: "color: white;", axis=0) - mi_styler.applymap_header(lambda x: "color: black;", axis=1) + mi_styler.applymap_index(lambda x: "color: white;", axis=0) + mi_styler.applymap_index(lambda x: "color: black;", axis=1) mi_styler.set_td_classes( DataFrame( [["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns @@ -308,7 +308,7 @@ def test_apply_map_header(method, axis): } # test execution added to todo - result = getattr(df.style, f"{method}_header")(func[method], axis=axis) + result = getattr(df.style, f"{method}_index")(func[method], axis=axis) assert len(result._todo) == 1 assert len(getattr(result, f"ctx_{axis}")) == 0 @@ -328,7 +328,7 @@ def test_apply_map_header_mi(mi_styler, method, axis): "apply": lambda s: ["attr: val;" if "b" in v else "" for v in s], "applymap": lambda v: "attr: val" if "b" in v else "", } - result = getattr(mi_styler, f"{method}_header")(func[method], axis=axis)._compute() + result = getattr(mi_styler, f"{method}_index")(func[method], axis=axis)._compute() expected = {(1, 1): [("attr", "val")]} assert getattr(result, f"ctx_{axis}") == expected @@ -336,7 +336,7 @@ def test_apply_map_header_mi(mi_styler, method, axis): def test_apply_map_header_raises(mi_styler): # GH 41893 with pytest.raises(ValueError, match="`axis` must be one of 0, 1, 'index', 'col"): - mi_styler.applymap_header(lambda v: "attr: val;", axis="bad-axis")._compute() + mi_styler.applymap_index(lambda v: "attr: val;", axis="bad-axis")._compute() class TestStyler: From e7097776b46328d9e83b29a2b2fded98d6a60972 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 9 Aug 2021 11:40:07 +0200 Subject: [PATCH 18/20] skip doctests --- pandas/io/formats/style.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 363a1186ea068..8bd2151942e53 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1336,7 +1336,7 @@ def apply_index( >>> df = pd.DataFrame([[1,2], [3,4]], index=["A", "B"]) >>> def color_b(s): ... return {ret} - >>> df.style.{this}_index(color_b) + >>> df.style.{this}_index(color_b) # doctest: +SKIP .. figure:: ../../_static/style/appmaphead1.png @@ -1347,6 +1347,7 @@ def apply_index( >>> def highlight_x({var}): ... return {ret2} >>> df.style.{this}_index(highlight_x, axis="columns", levels=[0, 2]) + ... # doctest: +SKIP .. figure:: ../../_static/style/appmaphead2.png """ From ab237b085bd95672c2e47c9247ccd624423cc835 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 9 Aug 2021 12:58:58 +0200 Subject: [PATCH 19/20] rename levels -->> level --- pandas/io/formats/style.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 7f7fbaad0cb8c..a72de753d6a8a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1249,7 +1249,7 @@ def _apply_index( self, func: Callable[..., Styler], axis: int | str = 0, - levels: list[int] | int | None = None, + level: Level | list[Level] | None = None, method: str = "apply", **kwargs, ) -> Styler: @@ -1262,11 +1262,8 @@ def _apply_index( f"`axis` must be one of 0, 1, 'index', 'columns', got {axis}" ) - if isinstance(obj, pd.MultiIndex) and levels is not None: - levels = [levels] if isinstance(levels, int) else levels - data = DataFrame(obj.to_list()).loc[:, levels] - else: - data = DataFrame(obj.to_list()) + levels_ = _refactor_levels(level, obj) + data = DataFrame(obj.to_list()).loc[:, levels_] if method == "apply": result = data.apply(func, axis=0, **kwargs) @@ -1293,7 +1290,7 @@ def apply_index( self, func: Callable[..., Styler], axis: int | str = 0, - levels: list[int] | int | None = None, + level: Level | list[Level] | None = None, **kwargs, ) -> Styler: """ @@ -1309,7 +1306,7 @@ def apply_index( ``func`` should {func}. axis : {axis} The headers over which to apply the function. - levels : int, list of ints, optional + level : int, str, list, optional If index is MultiIndex the level(s) over which to apply the function. **kwargs : dict Pass along to ``func``. @@ -1347,7 +1344,7 @@ def apply_index( >>> df = pd.DataFrame([np.arange(8)], columns=midx) >>> def highlight_x({var}): ... return {ret2} - >>> df.style.{this}_index(highlight_x, axis="columns", levels=[0, 2]) + >>> df.style.{this}_index(highlight_x, axis="columns", level=[0, 2]) ... # doctest: +SKIP .. figure:: ../../_static/style/appmaphead2.png @@ -1355,7 +1352,7 @@ def apply_index( self._todo.append( ( lambda instance: getattr(instance, "_apply_index"), - (func, axis, levels, "apply"), + (func, axis, level, "apply"), kwargs, ) ) @@ -1379,13 +1376,13 @@ def applymap_index( self, func: Callable[..., Styler], axis: int | str = 0, - levels: list[int] | int | None = None, + level: Level | list[Level] | None = None, **kwargs, ) -> Styler: self._todo.append( ( lambda instance: getattr(instance, "_apply_index"), - (func, axis, levels, "applymap"), + (func, axis, level, "applymap"), kwargs, ) ) From 9b7ec063ea087cf30ed772dff9a7fc5fbb644e1a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 9 Aug 2021 20:13:50 +0200 Subject: [PATCH 20/20] update user guide --- doc/source/user_guide/style.ipynb | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index f77d134d75988..10ef65a68eefa 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -225,13 +225,15 @@ "\n", "- Using [.set_table_styles()][table] to control broader areas of the table with specified internal CSS. Although table styles allow the flexibility to add CSS selectors and properties controlling all individual parts of the table, they are unwieldy for individual cell specifications. Also, note that table styles cannot be exported to Excel. \n", "- Using [.set_td_classes()][td_class] to directly link either external CSS classes to your data cells or link the internal CSS classes created by [.set_table_styles()][table]. See [here](#Setting-Classes-and-Linking-to-External-CSS). These cannot be used on column header rows or indexes, and also won't export to Excel. \n", - "- Using the [.apply()][apply] and [.applymap()][applymap] functions to add direct internal CSS to specific data cells. See [here](#Styler-Functions). These cannot be used on column header rows or indexes, but only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n", + "- Using the [.apply()][apply] and [.applymap()][applymap] functions to add direct internal CSS to specific data cells. See [here](#Styler-Functions). As of v1.4.0 there are also methods that work directly on column header rows or indexes; [.apply_index()][applyindex] and [.applymap_index()][applymapindex]. Note that only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n", "\n", "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.rst\n", "[styler]: ../reference/api/pandas.io.formats.style.Styler.rst\n", "[td_class]: ../reference/api/pandas.io.formats.style.Styler.set_td_classes.rst\n", "[apply]: ../reference/api/pandas.io.formats.style.Styler.apply.rst\n", "[applymap]: ../reference/api/pandas.io.formats.style.Styler.applymap.rst\n", + "[applyindex]: ../reference/api/pandas.io.formats.style.Styler.apply_index.rst\n", + "[applymapindex]: ../reference/api/pandas.io.formats.style.Styler.applymap_index.rst\n", "[dfapply]: ../reference/api/pandas.DataFrame.apply.rst\n", "[dfapplymap]: ../reference/api/pandas.DataFrame.applymap.rst" ] @@ -432,6 +434,8 @@ "source": [ "## Styler Functions\n", "\n", + "### Acting on Data\n", + "\n", "We use the following methods to pass your style functions. Both of those methods take a function (and some other keyword arguments) and apply it to the DataFrame in a certain way, rendering CSS styles.\n", "\n", "- [.applymap()][applymap] (elementwise): accepts a function that takes a single value and returns a string with the CSS attribute-value pair.\n", @@ -533,6 +537,18 @@ " .apply(highlight_max, props='color:white;background-color:purple', axis=None)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s2.set_uuid('after_apply_again')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -548,6 +564,33 @@ "" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Acting on the Index and Column Headers\n", + "\n", + "Similar application is acheived for headers by using:\n", + " \n", + "- [.applymap_index()][applymapindex] (elementwise): accepts a function that takes a single value and returns a string with the CSS attribute-value pair.\n", + "- [.apply_index()][applyindex] (level-wise): accepts a function that takes a Series and returns a Series, or numpy array with an identical shape where each element is a string with a CSS attribute-value pair. This method passes each level of your Index one-at-a-time. To style the index use `axis=0` and to style the column headers use `axis=1`.\n", + "\n", + "You can select a `level` of a `MultiIndex` but currently no similar `subset` application is available for these methods.\n", + "\n", + "[applyindex]: ../reference/api/pandas.io.formats.style.Styler.apply_index.rst\n", + "[applymapindex]: ../reference/api/pandas.io.formats.style.Styler.applymap_index.rst" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s2.applymap_index(lambda v: \"color:pink;\" if v>4 else \"color:darkblue;\", axis=0)\n", + "s2.apply_index(lambda s: np.where(s.isin([\"A\", \"B\"]), \"color:pink;\", \"color:darkblue;\"), axis=1)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1931,6 +1974,7 @@ } ], "metadata": { + "celltoolbar": "Edit Metadata", "kernelspec": { "display_name": "Python 3", "language": "python",