From 38145a330e08b15dbf09ee472d24bd4dbaf2fcc8 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Tue, 1 Mar 2022 14:01:11 +0100 Subject: [PATCH] ENH: make `Styler.concat` compatible with `to_latex` output render (#46186) --- doc/source/whatsnew/v1.5.0.rst | 2 +- pandas/io/formats/style.py | 6 +-- pandas/io/formats/style_render.py | 42 ++++++++++++++----- .../tests/io/formats/style/test_to_latex.py | 21 +++++++++- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index a6fb2fcce430a..790f52bdcc7ad 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -21,7 +21,7 @@ Styler - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) - Added the ability to render ``border`` and ``border-{side}`` CSS properties in Excel (:issue:`42276`) - - Added a new method :meth:`.Styler.concat` which allows adding customised footer rows to visualise additional calculations on the data, e.g. totals and counts etc. (:issue:`43875`) + - Added a new method :meth:`.Styler.concat` which allows adding customised footer rows to visualise additional calculations on the data, e.g. totals and counts etc. (:issue:`43875`, :issue:`46186`) .. _whatsnew_150.enhancements.enhancement2: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f614ac74944ec..15b2381c40d24 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -300,10 +300,10 @@ def concat(self, other: Styler) -> Styler: ``format_index`` will be preserved. .. warning:: - Only the output methods ``to_html`` and ``to_string`` currently work with - concatenated Stylers. + Only the output methods ``to_html``, ``to_string`` and ``to_latex`` + currently work with concatenated Stylers. - The output methods ``to_latex`` and ``to_excel`` **do not** work with + Other output methods, including ``to_excel``, **do not** work with concatenated Stylers. The following should be noted: diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 475e49cb848b5..0b3500cb34f87 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -158,7 +158,10 @@ def _render( blank: str = "", ): """ - Computes and applies styles and then generates the general render dicts + Computes and applies styles and then generates the general render dicts. + + Also extends the `ctx` and `ctx_index` attributes with those of concatenated + stylers for use within `_translate_latex` """ self._compute() dx = None @@ -172,11 +175,17 @@ def _render( "row": f"{self.css['foot']}_{self.css['row']}", "foot": self.css["foot"], } - dx, _ = self.concatenated._render( + dx = self.concatenated._render( sparse_index, sparse_columns, max_rows, max_cols, blank ) + + for (r, c), v in self.concatenated.ctx.items(): + self.ctx[(r + len(self.index), c)] = v + for (r, c), v in self.concatenated.ctx_index.items(): + self.ctx_index[(r + len(self.index), c)] = v + d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank, dx) - return d, dx + return d def _render_html( self, @@ -190,7 +199,7 @@ def _render_html( Renders the ``Styler`` including all applied styles to HTML. Generates a dict with necessary kwargs passed to jinja2 template. """ - d, _ = self._render(sparse_index, sparse_columns, max_rows, max_cols, " ") + d = self._render(sparse_index, sparse_columns, max_rows, max_cols, " ") d.update(kwargs) return self.template_html.render( **d, @@ -204,7 +213,7 @@ def _render_latex( """ Render a Styler in latex format """ - d, _ = self._render(sparse_index, sparse_columns, None, None) + d = self._render(sparse_index, sparse_columns, None, None) self._translate_latex(d, clines=clines) self.template_latex.globals["parse_wrap"] = _parse_latex_table_wrapping self.template_latex.globals["parse_table"] = _parse_latex_table_styles @@ -224,7 +233,7 @@ def _render_string( """ Render a Styler in string format """ - d, _ = self._render(sparse_index, sparse_columns, max_rows, max_cols) + d = self._render(sparse_index, sparse_columns, max_rows, max_cols) d.update(kwargs) return self.template_string.render(**d) @@ -840,11 +849,24 @@ def _translate_latex(self, d: dict, clines: str | None) -> None: ] for r, row in enumerate(d["head"]) ] + + def concatenated_visible_rows(obj, n, row_indices): + """ + Extract all visible row indices recursively from concatenated stylers. + """ + row_indices.extend( + [r + n for r in range(len(obj.index)) if r not in obj.hidden_rows] + ) + return ( + row_indices + if obj.concatenated is None + else concatenated_visible_rows( + obj.concatenated, n + len(obj.index), row_indices + ) + ) + body = [] - for r, row in zip( - [r for r in range(len(self.data.index)) if r not in self.hidden_rows], - d["body"], - ): + for r, row in zip(concatenated_visible_rows(self, 0, []), d["body"]): # note: cannot enumerate d["body"] because rows were dropped if hidden # during _translate_body so must zip to acquire the true r-index associated # with the ctx obj which contains the cell styles. diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 387cd714c69d1..4f83b3dafcc0b 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -999,7 +999,6 @@ def test_col_format_len(styler): assert expected in result -@pytest.mark.xfail # concat not yet implemented for to_latex def test_concat(styler): result = styler.concat(styler.data.agg(["sum"]).style).to_latex() expected = dedent( @@ -1013,3 +1012,23 @@ def test_concat(styler): """ ) assert result == expected + + +def test_concat_recursion(): + # tests hidden row recursion and applied styles + styler1 = DataFrame([[1], [9]]).style.hide([1]).highlight_min(color="red") + styler2 = DataFrame([[9], [2]]).style.hide([0]).highlight_min(color="green") + styler3 = DataFrame([[3], [9]]).style.hide([1]).highlight_min(color="blue") + + result = styler1.concat(styler2.concat(styler3)).to_latex(convert_css=True) + expected = dedent( + """\ + \\begin{tabular}{lr} + & 0 \\\\ + 0 & {\\cellcolor{red}} 1 \\\\ + 1 & {\\cellcolor{green}} 2 \\\\ + 0 & {\\cellcolor{blue}} 3 \\\\ + \\end{tabular} + """ + ) + assert result == expected