Skip to content

Commit

Permalink
ENH: multirow naive implementation Styler.to_latex part1 (#43369)
Browse files Browse the repository at this point in the history
* multirow naive implementation

* multirow naive implementation

* fix tests

* fixture

* clean up tests with fixture

* (ivanomg req) ref tests

* ivoanovmg req

Co-authored-by: JHM Darbyshire (iMac) <attack68@users.noreply.github.com>
  • Loading branch information
attack68 and attack68 authored Sep 7, 2021
1 parent 6a5dd0f commit 73c473d
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 60 deletions.
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

0 comments on commit 73c473d

Please sign in to comment.