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/18] 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/18] 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/18] 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/18] 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/18] 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 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 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 c9882aa9f598f47e828b51b8891a82279f830f46 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 14 Jun 2021 09:22:36 +0200 Subject: [PATCH 16/18] make apply_header compatible with to_latex --- doc/source/whatsnew/v1.4.0.rst | 2 +- pandas/io/formats/style_render.py | 27 +++++---- .../tests/io/formats/style/test_to_latex.py | 56 ++++++++++++++++++- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index eb650ed599dcd..f1865b764c569 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`) +- :meth:`.Styler.apply_header` and :meth:`.Styler.applymap_header` added to allow conditional styling of index and column header values for HTML and LaTeX (:issue:`41893`) - .. --------------------------------------------------------------------------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 9ab572654b362..f4ade11cea85b 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -557,7 +557,14 @@ def _translate_latex(self, d: dict) -> None: - Remove hidden indexes or reinsert missing th elements if part of multiindex or multirow sparsification (so that \multirow and \multicol work correctly). """ - d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] + d["head"] = [ + [ + {**col, "cellstyle": self.ctx_columns[r, c - self.index.nlevels]} + for c, col in enumerate(row) + if col["is_visible"] + ] + for r, row in enumerate(d["head"]) + ] body = [] for r, row in enumerate(d["body"]): if self.hidden_index: @@ -569,8 +576,9 @@ def _translate_latex(self, d: dict) -> None: "display_value": col["display_value"] if col["is_visible"] else "", + "cellstyle": self.ctx_index[r, c] if col["is_visible"] else [], } - for col in row + for c, col in enumerate(row) if col["type"] == "th" ] @@ -1352,26 +1360,21 @@ def _parse_latex_header_span( >>> _parse_latex_header_span(cell, 't', 'c') '\multicol{3}{c}{text}' """ + display_val = _parse_latex_cell_styles(cell["cellstyle"], cell["display_value"]) if "attributes" in cell: attrs = cell["attributes"] if 'colspan="' in attrs: colspan = attrs[attrs.find('colspan="') + 9 :] # len('colspan="') = 9 colspan = int(colspan[: colspan.find('"')]) - return ( - f"\\multicolumn{{{colspan}}}{{{multicol_align}}}" - f"{{{cell['display_value']}}}" - ) + return f"\\multicolumn{{{colspan}}}{{{multicol_align}}}{{{display_val}}}" elif 'rowspan="' in attrs: rowspan = attrs[attrs.find('rowspan="') + 9 :] rowspan = int(rowspan[: rowspan.find('"')]) - return ( - f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}" - f"{{{cell['display_value']}}}" - ) + return f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}{{{display_val}}}" if wrap: - return f"{{{cell['display_value']}}}" + return f"{{{display_val}}}" else: - return cell["display_value"] + return display_val def _parse_latex_options_strip(value: str | int | float, arg: str) -> str: diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 97347bddaa187..0131973187ff3 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -408,17 +408,20 @@ def test_parse_latex_cell_styles_braces(wrap_arg, expected): def test_parse_latex_header_span(): - cell = {"attributes": 'colspan="3"', "display_value": "text"} + cell = {"attributes": 'colspan="3"', "display_value": "text", "cellstyle": []} expected = "\\multicolumn{3}{Y}{text}" assert _parse_latex_header_span(cell, "X", "Y") == expected - cell = {"attributes": 'rowspan="5"', "display_value": "text"} + cell = {"attributes": 'rowspan="5"', "display_value": "text", "cellstyle": []} expected = "\\multirow[X]{5}{*}{text}" assert _parse_latex_header_span(cell, "X", "Y") == expected - cell = {"display_value": "text"} + cell = {"display_value": "text", "cellstyle": []} assert _parse_latex_header_span(cell, "X", "Y") == "text" + cell = {"display_value": "text", "cellstyle": [("bfseries", "--rwrap")]} + assert _parse_latex_header_span(cell, "X", "Y") == "\\bfseries{text}" + def test_parse_latex_table_wrapping(styler): styler.set_table_styles( @@ -443,3 +446,50 @@ def test_parse_latex_table_wrapping(styler): def test_short_caption(styler): result = styler.to_latex(caption=("full cap", "short cap")) assert "\\caption[short cap]{full cap}" in result + + +@pytest.mark.parametrize("index", [True, False]) +@pytest.mark.parametrize("columns", [True, False]) +def test_apply_map_header_render_mi(df, index, columns): + cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) + ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + df.loc[2, :] = [2, -2.22, "de"] + df = df.astype({"A": int}) + df.index, df.columns = ridx, cidx + styler = df.style + + func = lambda v: "bfseries: --rwrap" if "A" in v or "Z" in v or "c" in v else None + + if index: + styler.applymap_header(func, axis="index") + if columns: + styler.applymap_header(func, axis="columns") + + result = styler.to_latex() + + assert ( + ( + dedent( + """\ + \\multirow[c]{2}{*}{\\bfseries{A}} & a & 0 & -0.610000 & ab \\\\ + & b & 1 & -1.220000 & cd \\\\ + B & \\bfseries{c} & 2 & -2.220000 & de \\\\ + """ + ) + in result + ) + is index + ) + + assert ( + ( + dedent( + """\ + {} & {} & \\multicolumn{2}{r}{\\bfseries{Z}} & {Y} \\\\ + {} & {} & {a} & {b} & {\\bfseries{c}} \\\\ + """ + ) + in result + ) + is columns + ) From 8d8e88f88c71cb2472e9655ecbd618df59f5afde Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Jul 2021 19:08:36 +0200 Subject: [PATCH 17/18] 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 57ce47888cb68dd15f0406cede863e2657c16d2e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 12 Aug 2021 09:50:06 +0200 Subject: [PATCH 18/18] format tests --- .../tests/io/formats/style/test_to_latex.py | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 134c2b2019f17..ac164f2de9fb2 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -653,35 +653,25 @@ def test_apply_map_header_render_mi(df, index, columns): func = lambda v: "bfseries: --rwrap" if "A" in v or "Z" in v or "c" in v else None 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_latex() - assert ( - ( - dedent( - """\ - \\multirow[c]{2}{*}{\\bfseries{A}} & a & 0 & -0.610000 & ab \\\\ - & b & 1 & -1.220000 & cd \\\\ - B & \\bfseries{c} & 2 & -2.220000 & de \\\\ + expected_index = dedent( + """\ + \\multirow[c]{2}{*}{\\bfseries{A}} & a & 0 & -0.610000 & ab \\\\ + & b & 1 & -1.220000 & cd \\\\ + B & \\bfseries{c} & 2 & -2.220000 & de \\\\ """ - ) - in result - ) - is index ) + assert (expected_index in result) is index - assert ( - ( - dedent( - """\ - {} & {} & \\multicolumn{2}{r}{\\bfseries{Z}} & {Y} \\\\ - {} & {} & {a} & {b} & {\\bfseries{c}} \\\\ + expected_columns = dedent( + """\ + {} & {} & \\multicolumn{2}{r}{\\bfseries{Z}} & {Y} \\\\ + {} & {} & {a} & {b} & {\\bfseries{c}} \\\\ """ - ) - in result - ) - is columns ) + assert (expected_columns in result) is columns