Skip to content

ENH: multirow naive implementation Styler.to_latex part1 #43369

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

Merged
merged 9 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all 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/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Styler
- :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index``, ``sparse_columns``, ``bold_headers``, ``caption`` (:issue:`41946`, :issue:`43149`).
- Keyword arguments ``level`` and ``names`` added to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for additional control of visibility of MultiIndexes and index names (:issue:`25475`, :issue:`43404`, :issue:`43346`)
- Global options have been extended to configure default ``Styler`` properties including formatting and encoding and mathjax options and LaTeX (:issue:`41395`)
- Naive sparsification is now possible for LaTeX without the multirow package (:issue:`43369`)

Formerly Styler relied on ``display.html.use_mathjax``, which has now been replaced by ``styler.html.mathjax``.

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ def register_converter_cb(key):
"latex.multirow_align",
"c",
styler_multirow_align,
validator=is_one_of_factory(["c", "t", "b"]),
validator=is_one_of_factory(["c", "t", "b", "naive"]),
)

cf.register_option(
Expand Down
9 changes: 6 additions & 3 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,10 +520,13 @@ def to_latex(
Whether to sparsify the display of a hierarchical index. Setting to False
will display each explicit level element in a hierarchical key for each
column. Defaults to ``pandas.options.styler.sparse.columns`` value.
multirow_align : {"c", "t", "b"}, optional
multirow_align : {"c", "t", "b", "naive"}, optional
If sparsifying hierarchical MultiIndexes whether to align text centrally,
at the top or bottom. If not given defaults to
``pandas.options.styler.latex.multirow_align``
at the top or bottom using the multirow package. If not given defaults to
``pandas.options.styler.latex.multirow_align``. If "naive" is given renders
without multirow.

.. versionchanged:: 1.4.0
multicol_align : {"r", "c", "l"}, optional
If sparsifying hierarchical MultiIndex columns whether to align text at
the left, centrally, or at the right. If not given defaults to
Expand Down
2 changes: 2 additions & 0 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,8 @@ def _parse_latex_header_span(
colspan = int(colspan[: colspan.find('"')])
return f"\\multicolumn{{{colspan}}}{{{multicol_align}}}{{{display_val}}}"
elif 'rowspan="' in attrs:
if multirow_align == "naive":
return display_val
rowspan = attrs[attrs.find('rowspan="') + 9 :]
rowspan = int(rowspan[: rowspan.find('"')])
return f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}{{{display_val}}}"
Expand Down
128 changes: 72 additions & 56 deletions pandas/tests/io/formats/style/test_to_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ def df():
return DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]})


@pytest.fixture
def df_ext():
return DataFrame(
{"A": [0, 1, 2], "B": [-0.61, -1.22, -2.22], "C": ["ab", "cd", "de"]}
)


@pytest.fixture
def styler(df):
return Styler(df, uuid_len=0, precision=2)
Expand Down Expand Up @@ -210,11 +217,9 @@ def test_multiindex_columns(df):
assert expected == s.to_latex(sparse_columns=False)


def test_multiindex_row(df):
def test_multiindex_row(df_ext):
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
df.loc[2, :] = [2, -2.22, "de"]
df = df.astype({"A": int})
df.index = ridx
df_ext.index = ridx
expected = dedent(
"""\
\\begin{tabular}{llrrl}
Expand All @@ -225,8 +230,9 @@ def test_multiindex_row(df):
\\end{tabular}
"""
)
s = df.style.format(precision=2)
assert expected == s.to_latex()
styler = df_ext.style.format(precision=2)
result = styler.to_latex()
assert expected == result

# non-sparse
expected = dedent(
Expand All @@ -239,15 +245,32 @@ def test_multiindex_row(df):
\\end{tabular}
"""
)
assert expected == s.to_latex(sparse_index=False)
result = styler.to_latex(sparse_index=False)
assert expected == result


def test_multirow_naive(df_ext):
ridx = MultiIndex.from_tuples([("X", "x"), ("X", "y"), ("Y", "z")])
df_ext.index = ridx
expected = dedent(
"""\
\\begin{tabular}{llrrl}
& & A & B & C \\\\
X & x & 0 & -0.61 & ab \\\\
& y & 1 & -1.22 & cd \\\\
Y & z & 2 & -2.22 & de \\\\
\\end{tabular}
"""
)
styler = df_ext.style.format(precision=2)
result = styler.to_latex(multirow_align="naive")
assert expected == result


def test_multiindex_row_and_col(df):
def test_multiindex_row_and_col(df_ext):
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
df_ext.index, df_ext.columns = ridx, cidx
expected = dedent(
"""\
\\begin{tabular}{llrrl}
Expand All @@ -259,8 +282,9 @@ def test_multiindex_row_and_col(df):
\\end{tabular}
"""
)
s = df.style.format(precision=2)
assert s.to_latex(multirow_align="b", multicol_align="l") == expected
styler = df_ext.style.format(precision=2)
result = styler.to_latex(multirow_align="b", multicol_align="l")
assert result == expected

# non-sparse
expected = dedent(
Expand All @@ -274,16 +298,15 @@ def test_multiindex_row_and_col(df):
\\end{tabular}
"""
)
assert s.to_latex(sparse_index=False, sparse_columns=False) == expected
result = styler.to_latex(sparse_index=False, sparse_columns=False)
assert result == expected


def test_multi_options(df):
def test_multi_options(df_ext):
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.format(precision=2)
df_ext.index, df_ext.columns = ridx, cidx
styler = df_ext.style.format(precision=2)

expected = dedent(
"""\
Expand All @@ -292,7 +315,8 @@ def test_multi_options(df):
\\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\
"""
)
assert expected in styler.to_latex()
result = styler.to_latex()
assert expected in result

with option_context("styler.latex.multicol_align", "l"):
assert " & & \\multicolumn{2}{l}{Z} & Y \\\\" in styler.to_latex()
Expand All @@ -311,30 +335,25 @@ def test_multiindex_columns_hidden():
assert "{tabular}{lrrr}" in s.to_latex()


def test_sparse_options(df):
@pytest.mark.parametrize(
"option, value",
[
("styler.sparse.index", True),
("styler.sparse.index", False),
("styler.sparse.columns", True),
("styler.sparse.columns", False),
],
)
def test_sparse_options(df_ext, option, value):
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.index, df.columns = ridx, cidx
s = df.style

latex1 = s.to_latex()

with option_context("styler.sparse.index", True):
latex2 = s.to_latex()
assert latex1 == latex2

with option_context("styler.sparse.index", False):
latex2 = s.to_latex()
assert latex1 != latex2
df_ext.index, df_ext.columns = ridx, cidx
styler = df_ext.style

with option_context("styler.sparse.columns", True):
latex2 = s.to_latex()
assert latex1 == latex2

with option_context("styler.sparse.columns", False):
latex2 = s.to_latex()
assert latex1 != latex2
latex1 = styler.to_latex()
with option_context(option, value):
latex2 = styler.to_latex()
assert (latex1 == latex2) is value


def test_hidden_index(styler):
Expand All @@ -352,16 +371,14 @@ def test_hidden_index(styler):


@pytest.mark.parametrize("environment", ["table", "figure*", None])
def test_comprehensive(df, environment):
def test_comprehensive(df_ext, environment):
# test as many low level features simultaneously as possible
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
s = df.style
s.set_caption("mycap")
s.set_table_styles(
df_ext.index, df_ext.columns = ridx, cidx
stlr = df_ext.style
stlr.set_caption("mycap")
stlr.set_table_styles(
[
{"selector": "label", "props": ":{fig§item}"},
{"selector": "position", "props": ":h!"},
Expand All @@ -373,8 +390,8 @@ def test_comprehensive(df, environment):
{"selector": "rowcolors", "props": ":{3}{pink}{}"}, # custom command
]
)
s.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap")
s.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")])
stlr.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap")
stlr.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")])

expected = (
"""\
Expand All @@ -398,7 +415,8 @@ def test_comprehensive(df, environment):
\\end{table}
"""
).replace("table", environment if environment else "table")
assert s.format(precision=2).to_latex(environment=environment) == expected
result = stlr.format(precision=2).to_latex(environment=environment)
assert result == expected


def test_environment_option(styler):
Expand Down Expand Up @@ -687,13 +705,11 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp):
(False, False),
],
)
def test_apply_map_header_render_mi(df, index, columns, siunitx):
def test_apply_map_header_render_mi(df_ext, index, columns, siunitx):
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
df_ext.index, df_ext.columns = ridx, cidx
styler = df_ext.style

func = lambda v: "bfseries: --rwrap" if "A" in v or "Z" in v or "c" in v else None

Expand Down