Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -64,6 +64,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
27 changes: 22 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 @@ -950,15 +956,26 @@ 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,
Expand Down
112 changes: 112 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,58 @@ 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 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=env)
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