Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Be able to add lines for all index levels, not just visible ones [fix #59877] #59916

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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:
Expand Down
20 changes: 15 additions & 5 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,17 +717,27 @@ 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
.. versionchanged:: 3.0.0
label : str, optional
The LaTeX label included as: \\label{<label>}.
This is used with \\ref{<label>} in the main .tex file.
Expand Down
24 changes: 19 additions & 5 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,12 +933,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
Expand All @@ -951,14 +957,22 @@ def concatenated_visible_rows(obj):
i for i in range(index_levels) if 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 range(index_levels):
if self.hide_index_[lvl] and "invisible" not in clines:
continue
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,
Expand Down
111 changes: 111 additions & 0 deletions pandas/tests/io/formats/style/test_to_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ""),
],
)
Expand Down Expand Up @@ -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"])
Expand All @@ -998,6 +1058,57 @@ 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}
"""
),
),
],
)
def test_clines_hiddenindex(clines, expected):
# 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
styler.hide([("-", "X"), ("-", "Y")])
styler.hide(axis=0)
result = styler.to_latex(clines=clines, environment="table")
assert expected in result


def test_col_format_len(styler):
# gh 46037
result = styler.to_latex(environment="longtable", column_format="lrr{10cm}")
Expand Down
Loading