Skip to content

Commit 73c473d

Browse files
authored
ENH: multirow naive implementation Styler.to_latex part1 (#43369)
* 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) <[email protected]>
1 parent 6a5dd0f commit 73c473d

File tree

5 files changed

+82
-60
lines changed

5 files changed

+82
-60
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Styler
7878
- :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index``, ``sparse_columns``, ``bold_headers``, ``caption`` (:issue:`41946`, :issue:`43149`).
7979
- 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`)
8080
- Global options have been extended to configure default ``Styler`` properties including formatting and encoding and mathjax options and LaTeX (:issue:`41395`)
81+
- Naive sparsification is now possible for LaTeX without the multirow package (:issue:`43369`)
8182

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

pandas/core/config_init.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ def register_converter_cb(key):
889889
"latex.multirow_align",
890890
"c",
891891
styler_multirow_align,
892-
validator=is_one_of_factory(["c", "t", "b"]),
892+
validator=is_one_of_factory(["c", "t", "b", "naive"]),
893893
)
894894

895895
cf.register_option(

pandas/io/formats/style.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,13 @@ def to_latex(
520520
Whether to sparsify the display of a hierarchical index. Setting to False
521521
will display each explicit level element in a hierarchical key for each
522522
column. Defaults to ``pandas.options.styler.sparse.columns`` value.
523-
multirow_align : {"c", "t", "b"}, optional
523+
multirow_align : {"c", "t", "b", "naive"}, optional
524524
If sparsifying hierarchical MultiIndexes whether to align text centrally,
525-
at the top or bottom. If not given defaults to
526-
``pandas.options.styler.latex.multirow_align``
525+
at the top or bottom using the multirow package. If not given defaults to
526+
``pandas.options.styler.latex.multirow_align``. If "naive" is given renders
527+
without multirow.
528+
529+
.. versionchanged:: 1.4.0
527530
multicol_align : {"r", "c", "l"}, optional
528531
If sparsifying hierarchical MultiIndex columns whether to align text at
529532
the left, centrally, or at the right. If not given defaults to

pandas/io/formats/style_render.py

+2
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,8 @@ def _parse_latex_header_span(
14121412
colspan = int(colspan[: colspan.find('"')])
14131413
return f"\\multicolumn{{{colspan}}}{{{multicol_align}}}{{{display_val}}}"
14141414
elif 'rowspan="' in attrs:
1415+
if multirow_align == "naive":
1416+
return display_val
14151417
rowspan = attrs[attrs.find('rowspan="') + 9 :]
14161418
rowspan = int(rowspan[: rowspan.find('"')])
14171419
return f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}{{{display_val}}}"

pandas/tests/io/formats/style/test_to_latex.py

+72-56
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ def df():
2424
return DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]})
2525

2626

27+
@pytest.fixture
28+
def df_ext():
29+
return DataFrame(
30+
{"A": [0, 1, 2], "B": [-0.61, -1.22, -2.22], "C": ["ab", "cd", "de"]}
31+
)
32+
33+
2734
@pytest.fixture
2835
def styler(df):
2936
return Styler(df, uuid_len=0, precision=2)
@@ -210,11 +217,9 @@ def test_multiindex_columns(df):
210217
assert expected == s.to_latex(sparse_columns=False)
211218

212219

213-
def test_multiindex_row(df):
220+
def test_multiindex_row(df_ext):
214221
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
215-
df.loc[2, :] = [2, -2.22, "de"]
216-
df = df.astype({"A": int})
217-
df.index = ridx
222+
df_ext.index = ridx
218223
expected = dedent(
219224
"""\
220225
\\begin{tabular}{llrrl}
@@ -225,8 +230,9 @@ def test_multiindex_row(df):
225230
\\end{tabular}
226231
"""
227232
)
228-
s = df.style.format(precision=2)
229-
assert expected == s.to_latex()
233+
styler = df_ext.style.format(precision=2)
234+
result = styler.to_latex()
235+
assert expected == result
230236

231237
# non-sparse
232238
expected = dedent(
@@ -239,15 +245,32 @@ def test_multiindex_row(df):
239245
\\end{tabular}
240246
"""
241247
)
242-
assert expected == s.to_latex(sparse_index=False)
248+
result = styler.to_latex(sparse_index=False)
249+
assert expected == result
250+
251+
252+
def test_multirow_naive(df_ext):
253+
ridx = MultiIndex.from_tuples([("X", "x"), ("X", "y"), ("Y", "z")])
254+
df_ext.index = ridx
255+
expected = dedent(
256+
"""\
257+
\\begin{tabular}{llrrl}
258+
& & A & B & C \\\\
259+
X & x & 0 & -0.61 & ab \\\\
260+
& y & 1 & -1.22 & cd \\\\
261+
Y & z & 2 & -2.22 & de \\\\
262+
\\end{tabular}
263+
"""
264+
)
265+
styler = df_ext.style.format(precision=2)
266+
result = styler.to_latex(multirow_align="naive")
267+
assert expected == result
243268

244269

245-
def test_multiindex_row_and_col(df):
270+
def test_multiindex_row_and_col(df_ext):
246271
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
247272
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
248-
df.loc[2, :] = [2, -2.22, "de"]
249-
df = df.astype({"A": int})
250-
df.index, df.columns = ridx, cidx
273+
df_ext.index, df_ext.columns = ridx, cidx
251274
expected = dedent(
252275
"""\
253276
\\begin{tabular}{llrrl}
@@ -259,8 +282,9 @@ def test_multiindex_row_and_col(df):
259282
\\end{tabular}
260283
"""
261284
)
262-
s = df.style.format(precision=2)
263-
assert s.to_latex(multirow_align="b", multicol_align="l") == expected
285+
styler = df_ext.style.format(precision=2)
286+
result = styler.to_latex(multirow_align="b", multicol_align="l")
287+
assert result == expected
264288

265289
# non-sparse
266290
expected = dedent(
@@ -274,16 +298,15 @@ def test_multiindex_row_and_col(df):
274298
\\end{tabular}
275299
"""
276300
)
277-
assert s.to_latex(sparse_index=False, sparse_columns=False) == expected
301+
result = styler.to_latex(sparse_index=False, sparse_columns=False)
302+
assert result == expected
278303

279304

280-
def test_multi_options(df):
305+
def test_multi_options(df_ext):
281306
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
282307
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
283-
df.loc[2, :] = [2, -2.22, "de"]
284-
df = df.astype({"A": int})
285-
df.index, df.columns = ridx, cidx
286-
styler = df.style.format(precision=2)
308+
df_ext.index, df_ext.columns = ridx, cidx
309+
styler = df_ext.style.format(precision=2)
287310

288311
expected = dedent(
289312
"""\
@@ -292,7 +315,8 @@ def test_multi_options(df):
292315
\\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\
293316
"""
294317
)
295-
assert expected in styler.to_latex()
318+
result = styler.to_latex()
319+
assert expected in result
296320

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

313337

314-
def test_sparse_options(df):
338+
@pytest.mark.parametrize(
339+
"option, value",
340+
[
341+
("styler.sparse.index", True),
342+
("styler.sparse.index", False),
343+
("styler.sparse.columns", True),
344+
("styler.sparse.columns", False),
345+
],
346+
)
347+
def test_sparse_options(df_ext, option, value):
315348
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
316349
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
317-
df.loc[2, :] = [2, -2.22, "de"]
318-
df.index, df.columns = ridx, cidx
319-
s = df.style
320-
321-
latex1 = s.to_latex()
322-
323-
with option_context("styler.sparse.index", True):
324-
latex2 = s.to_latex()
325-
assert latex1 == latex2
326-
327-
with option_context("styler.sparse.index", False):
328-
latex2 = s.to_latex()
329-
assert latex1 != latex2
350+
df_ext.index, df_ext.columns = ridx, cidx
351+
styler = df_ext.style
330352

331-
with option_context("styler.sparse.columns", True):
332-
latex2 = s.to_latex()
333-
assert latex1 == latex2
334-
335-
with option_context("styler.sparse.columns", False):
336-
latex2 = s.to_latex()
337-
assert latex1 != latex2
353+
latex1 = styler.to_latex()
354+
with option_context(option, value):
355+
latex2 = styler.to_latex()
356+
assert (latex1 == latex2) is value
338357

339358

340359
def test_hidden_index(styler):
@@ -352,16 +371,14 @@ def test_hidden_index(styler):
352371

353372

354373
@pytest.mark.parametrize("environment", ["table", "figure*", None])
355-
def test_comprehensive(df, environment):
374+
def test_comprehensive(df_ext, environment):
356375
# test as many low level features simultaneously as possible
357376
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
358377
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
359-
df.loc[2, :] = [2, -2.22, "de"]
360-
df = df.astype({"A": int})
361-
df.index, df.columns = ridx, cidx
362-
s = df.style
363-
s.set_caption("mycap")
364-
s.set_table_styles(
378+
df_ext.index, df_ext.columns = ridx, cidx
379+
stlr = df_ext.style
380+
stlr.set_caption("mycap")
381+
stlr.set_table_styles(
365382
[
366383
{"selector": "label", "props": ":{fig§item}"},
367384
{"selector": "position", "props": ":h!"},
@@ -373,8 +390,8 @@ def test_comprehensive(df, environment):
373390
{"selector": "rowcolors", "props": ":{3}{pink}{}"}, # custom command
374391
]
375392
)
376-
s.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap")
377-
s.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")])
393+
stlr.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap")
394+
stlr.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")])
378395

379396
expected = (
380397
"""\
@@ -398,7 +415,8 @@ def test_comprehensive(df, environment):
398415
\\end{table}
399416
"""
400417
).replace("table", environment if environment else "table")
401-
assert s.format(precision=2).to_latex(environment=environment) == expected
418+
result = stlr.format(precision=2).to_latex(environment=environment)
419+
assert result == expected
402420

403421

404422
def test_environment_option(styler):
@@ -687,13 +705,11 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp):
687705
(False, False),
688706
],
689707
)
690-
def test_apply_map_header_render_mi(df, index, columns, siunitx):
708+
def test_apply_map_header_render_mi(df_ext, index, columns, siunitx):
691709
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
692710
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
693-
df.loc[2, :] = [2, -2.22, "de"]
694-
df = df.astype({"A": int})
695-
df.index, df.columns = ridx, cidx
696-
styler = df.style
711+
df_ext.index, df_ext.columns = ridx, cidx
712+
styler = df_ext.style
697713

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

0 commit comments

Comments
 (0)