From fadafbafb1b38873074cfb0f77453c22e0551854 Mon Sep 17 00:00:00 2001 From: Ed Bennett Date: Sat, 28 Sep 2024 16:50:13 +0100 Subject: [PATCH 01/11] be able to add lines for all indices, not just visible ones [fix #59877] - implement a new suffix for the `clines` option, `-invisible`, doubling the number of available options, specifying that hidden indices should be included when deciding whether to add \clines - add tests for this behaviour --- pandas/io/formats/style_render.py | 26 +++- .../tests/io/formats/style/test_to_latex.py | 113 ++++++++++++++++++ 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8a6383f7e8f82..a8defd599110f 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -930,12 +930,18 @@ def concatenated_visible_rows(obj): None, "all;data", "all;index", + "all-invisible;data", + "all-invisible;index", "skip-last;data", "skip-last;index", + "skip-last-invisible;data", + "skip-last-invisible;index", ]: raise ValueError( f"`clines` value of {clines} is invalid. Should either be None or one " - f"of 'all;data', 'all;index', 'skip-last;data', 'skip-last;index'." + f"of 'all;data', 'all;index', 'all-invisible;data', " + f"'all-invisible;index', 'skip-last;data', 'skip-last;index', " + f"'skip-last-invisible;data', 'skip-last-invisible;index'." ) if clines is not None: data_len = len(row_body_cells) if "data" in clines and d["body"] else 0 @@ -947,15 +953,25 @@ def concatenated_visible_rows(obj): visible_index_levels: list[int] = [ i for i in range(index_levels) if not self.hide_index_[i] ] + target_index_levels: list[int] = [ + i for i in range(index_levels) + if "invisible" in clines or not self.hide_index_[i] + ] for rn, r in enumerate(visible_row_indexes): - for lvln, lvl in enumerate(visible_index_levels): + lvln = 0 + for lvl in target_index_levels: if lvl == index_levels - 1 and "skip-last" in clines: continue idx_len = d["index_lengths"].get((lvl, r), None) if idx_len is not None: # i.e. not a sparsified entry - d["clines"][rn + idx_len].append( - f"\\cline{{{lvln+1}-{len(visible_index_levels)+data_len}}}" - ) + cline_start_col = lvln + 1 + cline_end_col = len(visible_index_levels) + data_len + if cline_end_col >= cline_start_col: + d["clines"][rn + idx_len].append( + f"\\cline{{{cline_start_col}-{cline_end_col}}}" + ) + if lvl in visible_index_levels: + lvln += 1 def format( self, diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 1abe6238d3922..8c8c4b7968666 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -896,8 +896,12 @@ def test_clines_validation(clines, styler): [ ("all;index", "\n\\cline{1-1}"), ("all;data", "\n\\cline{1-2}"), + ("all-invisible;index", "\n\\cline{1-1}"), + ("all-invisible;data", "\n\\cline{1-2}"), ("skip-last;index", ""), ("skip-last;data", ""), + ("skip-last-invisible;index", ""), + ("skip-last-invisible;data", ""), (None, ""), ], ) @@ -984,6 +988,62 @@ def test_clines_index(clines, exp, env): """ ), ), + ( + "skip-last-invisible;index", + dedent( + """\ + \\multirow[c]{2}{*}{A} & X & 1 \\\\ + & Y & 2 \\\\ + \\cline{1-2} \\cline{2-2} + \\multirow[c]{2}{*}{B} & X & 3 \\\\ + & Y & 4 \\\\ + \\cline{1-2} \\cline{2-2} + """ + ), + ), + ( + "skip-last-invisible;data", + dedent( + """\ + \\multirow[c]{2}{*}{A} & X & 1 \\\\ + & Y & 2 \\\\ + \\cline{1-3} \\cline{2-3} + \\multirow[c]{2}{*}{B} & X & 3 \\\\ + & Y & 4 \\\\ + \\cline{1-3} \\cline{2-3} + """ + ), + ), + ( + "all-invisible;index", + dedent( + """\ + \\multirow[c]{2}{*}{A} & X & 1 \\\\ + \\cline{2-2} + & Y & 2 \\\\ + \\cline{1-2} \\cline{2-2} \\cline{2-2} + \\multirow[c]{2}{*}{B} & X & 3 \\\\ + \\cline{2-2} + & Y & 4 \\\\ + \\cline{1-2} \\cline{2-2} \\cline{2-2} + """ + ), + ), + ( + "all-invisible;data", + dedent( + """\ + \\multirow[c]{2}{*}{A} & X & 1 \\\\ + \\cline{2-3} + & Y & 2 \\\\ + \\cline{1-3} \\cline{2-3} \\cline{2-3} + \\multirow[c]{2}{*}{B} & X & 3 \\\\ + \\cline{2-3} + & Y & 4 \\\\ + \\cline{1-3} \\cline{2-3} \\cline{2-3} + """ + ), + ), ], ) @pytest.mark.parametrize("env", ["table"]) @@ -998,6 +1058,59 @@ def test_clines_multiindex(clines, expected, env): assert expected in result +@pytest.mark.parametrize( + "clines, expected", + [ + (None, "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("all;data", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("all;index", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("skip-last;data", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("skip-last;index", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("all-invisible;index", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ("skip-last-invisible;index", "1 \\\\\n2 \\\\\n3 \\\\\n4 \\\\\n"), + ( + "all-invisible;data", + dedent( + """\ + 1 \\\\ + \\cline{1-1} + 2 \\\\ + \\cline{1-1} \\cline{1-1} + 3 \\\\ + \\cline{1-1} + 4 \\\\ + \\cline{1-1} \\cline{1-1} + """ + ), + ), + ( + "skip-last-invisible;data", + dedent( + """\ + 1 \\\\ + 2 \\\\ + \\cline{1-1} + 3 \\\\ + 4 \\\\ + \\cline{1-1} + """ + ), + ), + ] +) +@pytest.mark.parametrize("env", ["table"]) +def test_clines_hiddenindex(clines, expected, env): + # Make sure that \clines are correctly hidden or shown with all indixes hideen + midx = MultiIndex.from_product([["A", "-", "B"], ["X", "Y"]]) + df = DataFrame([[1], [2], [99], [99], [3], [4]], index=midx) + styler = df.style + styler.hide([("-", "X"), ("-", "Y")]) + styler.hide(axis=0) + result = styler.to_latex(clines=clines, environment=env) + assert expected in result + + + def test_col_format_len(styler): # gh 46037 result = styler.to_latex(environment="longtable", column_format="lrr{10cm}") From c3b4a542d476916b96fbdeeb3ba6e568f66c8ca4 Mon Sep 17 00:00:00 2001 From: Ed Bennett Date: Sat, 28 Sep 2024 16:59:39 +0100 Subject: [PATCH 02/11] placate pre-commit --- pandas/io/formats/style_render.py | 3 ++- pandas/tests/io/formats/style/test_to_latex.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index a8defd599110f..045317fd95253 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -954,7 +954,8 @@ def concatenated_visible_rows(obj): i for i in range(index_levels) if not self.hide_index_[i] ] target_index_levels: list[int] = [ - i for i in range(index_levels) + i + for i in range(index_levels) if "invisible" in clines or not self.hide_index_[i] ] for rn, r in enumerate(visible_row_indexes): diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 8c8c4b7968666..cbc1c879535d5 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -1096,11 +1096,11 @@ def test_clines_multiindex(clines, expected, env): """ ), ), - ] + ], ) @pytest.mark.parametrize("env", ["table"]) def test_clines_hiddenindex(clines, expected, env): - # Make sure that \clines are correctly hidden or shown with all indixes hideen + # Make sure that \clines are correctly hidden or shown with all indixes hidden midx = MultiIndex.from_product([["A", "-", "B"], ["X", "Y"]]) df = DataFrame([[1], [2], [99], [99], [3], [4]], index=midx) styler = df.style @@ -1110,7 +1110,6 @@ def test_clines_hiddenindex(clines, expected, env): assert expected in result - def test_col_format_len(styler): # gh 46037 result = styler.to_latex(environment="longtable", column_format="lrr{10cm}") From a90eba74c358b49e00f4d698188ab888441755d0 Mon Sep 17 00:00:00 2001 From: Ed Bennett Date: Sat, 28 Sep 2024 20:54:38 +0100 Subject: [PATCH 03/11] document changes to cline options --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/style.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 41ba80989a0ce..e4b9ec82322f8 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -62,6 +62,7 @@ Other enhancements - Support passing a :class:`Iterable[Hashable]` input to :meth:`DataFrame.drop_duplicates` (:issue:`59237`) - Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`) - Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`) +- :meth:`Styler.to_latex` accepts additional options to the ``clines`` parameter, allowing lines to be drawn between hidden index levels. .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6e5ae09485951..a1c8856b1c5dc 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -716,15 +716,25 @@ def to_latex( Possible values are: - `None`: no cline commands are added (default). - - `"all;data"`: a cline is added for every index value extending the - width of the table, including data entries. + - `"all;data"`: a cline is added for every visible index value extending + the width of the table, including data entries. - `"all;index"`: as above with lines extending only the width of the index entries. - - `"skip-last;data"`: a cline is added for each index value except the - last level (which is never sparsified), extending the widtn of the + - `"all-invisible;data"`: a cline is added for every index value, + including hidden indexes, extending the full width of the table, + including data entries. + - `"all-invisible;index"`: as above with lines extending only the width + of the index entries. + - `"skip-last;data"`: a cline is added for each visible index value except + the last level (which is never sparsified), extending the widtn of the table. - `"skip-last;index"`: as above with lines extending only the width of the index entries. + - `"skip-last-invisible;data"`: a cline is added for each index value, + including hidden index levels, but excluding the last (which is never + sparsified), extending the width of the table. + - `"skip-last-invisible;index"`: as above with lines extending only the + width of the index entries. .. versionadded:: 1.4.0 label : str, optional From e671c3be8ec08262bca45b718d8a15fae73afe5b Mon Sep 17 00:00:00 2001 From: Ed Bennett Date: Thu, 3 Oct 2024 10:11:10 +0100 Subject: [PATCH 04/11] add versionchanged to documentation for modified cline functionality --- 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 a1c8856b1c5dc..c00acbc092be9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -736,7 +736,7 @@ def to_latex( - `"skip-last-invisible;index"`: as above with lines extending only the width of the index entries. - .. versionadded:: 1.4.0 + .. versionchanged:: 3.0.0 label : str, optional The LaTeX label included as: \\label{