diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e6f9503a1e6f2..d0bd59021c238 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -861,6 +861,7 @@ def to_latex( multicol_align=multicol_align, environment=environment, convert_css=convert_css, + siunitx=siunitx, ) encoding = encoding or get_option("styler.render.encoding") diff --git a/pandas/io/formats/templates/latex_longtable.tpl b/pandas/io/formats/templates/latex_longtable.tpl index fdb45aea83c16..4358cf207559a 100644 --- a/pandas/io/formats/templates/latex_longtable.tpl +++ b/pandas/io/formats/templates/latex_longtable.tpl @@ -29,7 +29,7 @@ \{{toprule}} {% endif %} {% for row in head %} -{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ +{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, siunitx)}}{% endfor %} \\ {% endfor %} {% set midrule = parse_table(table_styles, 'midrule') %} {% if midrule is not none %} @@ -45,7 +45,7 @@ \{{toprule}} {% endif %} {% for row in head %} -{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ +{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, siunitx)}}{% endfor %} \\ {% endfor %} {% if midrule is not none %} \{{midrule}} diff --git a/pandas/io/formats/templates/latex_table.tpl b/pandas/io/formats/templates/latex_table.tpl index dfdd160351d02..5445f077dc6fd 100644 --- a/pandas/io/formats/templates/latex_table.tpl +++ b/pandas/io/formats/templates/latex_table.tpl @@ -31,7 +31,7 @@ \{{toprule}} {% endif %} {% for row in head %} -{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ +{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, siunitx)}}{% endfor %} \\ {% endfor %} {% set midrule = parse_table(table_styles, 'midrule') %} {% if midrule is not none %} diff --git a/pandas/tests/io/formats/style/test_non_unique.py b/pandas/tests/io/formats/style/test_non_unique.py index 5bb593f20e9d5..608f3d9cbd441 100644 --- a/pandas/tests/io/formats/style/test_non_unique.py +++ b/pandas/tests/io/formats/style/test_non_unique.py @@ -131,7 +131,7 @@ def test_latex_non_unique(styler): assert result == dedent( """\ \\begin{tabular}{lrrr} - {} & {c} & {d} & {d} \\\\ + & c & d & d \\\\ i & 1.000000 & 2.000000 & 3.000000 \\\\ j & 4.000000 & 5.000000 & 6.000000 \\\\ j & 7.000000 & 8.000000 & 9.000000 \\\\ diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 671219872bb34..18050d51e6b8c 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -33,7 +33,7 @@ def test_minimal_latex_tabular(styler): expected = dedent( """\ \\begin{tabular}{lrrl} - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ 0 & 0 & -0.61 & ab \\\\ 1 & 1 & -1.22 & cd \\\\ \\end{tabular} @@ -47,7 +47,7 @@ def test_tabular_hrules(styler): """\ \\begin{tabular}{lrrl} \\toprule - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ \\midrule 0 & 0 & -0.61 & ab \\\\ 1 & 1 & -1.22 & cd \\\\ @@ -69,7 +69,7 @@ def test_tabular_custom_hrules(styler): """\ \\begin{tabular}{lrrl} \\hline - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ 0 & 0 & -0.61 & ab \\\\ 1 & 1 & -1.22 & cd \\\\ \\otherline @@ -169,7 +169,7 @@ def test_cell_styling(styler): expected = dedent( """\ \\begin{tabular}{lrrl} - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ 0 & 0 & \\itshape {\\Huge -0.61} & ab \\\\ 1 & \\itshape {\\Huge 1} & -1.22 & \\itshape {\\Huge cd} \\\\ \\end{tabular} @@ -184,8 +184,8 @@ def test_multiindex_columns(df): expected = dedent( """\ \\begin{tabular}{lrrl} - {} & \\multicolumn{2}{r}{A} & {B} \\\\ - {} & {a} & {b} & {c} \\\\ + & \\multicolumn{2}{r}{A} & B \\\\ + & a & b & c \\\\ 0 & 0 & -0.61 & ab \\\\ 1 & 1 & -1.22 & cd \\\\ \\end{tabular} @@ -198,8 +198,8 @@ def test_multiindex_columns(df): expected = dedent( """\ \\begin{tabular}{lrrl} - {} & {A} & {A} & {B} \\\\ - {} & {a} & {b} & {c} \\\\ + & A & A & B \\\\ + & a & b & c \\\\ 0 & 0 & -0.61 & ab \\\\ 1 & 1 & -1.22 & cd \\\\ \\end{tabular} @@ -217,7 +217,7 @@ def test_multiindex_row(df): expected = dedent( """\ \\begin{tabular}{llrrl} - {} & {} & {A} & {B} & {C} \\\\ + & & A & B & C \\\\ \\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\ & b & 1 & -1.22 & cd \\\\ B & c & 2 & -2.22 & de \\\\ @@ -231,7 +231,7 @@ def test_multiindex_row(df): expected = dedent( """\ \\begin{tabular}{llrrl} - {} & {} & {A} & {B} & {C} \\\\ + & & A & B & C \\\\ A & a & 0 & -0.61 & ab \\\\ A & b & 1 & -1.22 & cd \\\\ B & c & 2 & -2.22 & de \\\\ @@ -250,8 +250,8 @@ def test_multiindex_row_and_col(df): expected = dedent( """\ \\begin{tabular}{llrrl} - {} & {} & \\multicolumn{2}{l}{Z} & {Y} \\\\ - {} & {} & {a} & {b} & {c} \\\\ + & & \\multicolumn{2}{l}{Z} & Y \\\\ + & & a & b & c \\\\ \\multirow[b]{2}{*}{A} & a & 0 & -0.61 & ab \\\\ & b & 1 & -1.22 & cd \\\\ B & c & 2 & -2.22 & de \\\\ @@ -265,8 +265,8 @@ def test_multiindex_row_and_col(df): expected = dedent( """\ \\begin{tabular}{llrrl} - {} & {} & {Z} & {Z} & {Y} \\\\ - {} & {} & {a} & {b} & {c} \\\\ + & & Z & Z & Y \\\\ + & & a & b & c \\\\ A & a & 0 & -0.61 & ab \\\\ A & b & 1 & -1.22 & cd \\\\ B & c & 2 & -2.22 & de \\\\ @@ -286,15 +286,15 @@ def test_multi_options(df): expected = dedent( """\ - {} & {} & \\multicolumn{2}{r}{Z} & {Y} \\\\ - {} & {} & {a} & {b} & {c} \\\\ + & & \\multicolumn{2}{r}{Z} & Y \\\\ + & & a & b & c \\\\ \\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\ """ ) assert expected in styler.to_latex() with option_context("styler.latex.multicol_align", "l"): - assert "{} & {} & \\multicolumn{2}{l}{Z} & {Y} \\\\" in styler.to_latex() + assert " & & \\multicolumn{2}{l}{Z} & Y \\\\" in styler.to_latex() with option_context("styler.latex.multirow_align", "b"): assert "\\multirow[b]{2}{*}{A} & a & 0 & -0.61 & ab \\\\" in styler.to_latex() @@ -341,7 +341,7 @@ def test_hidden_index(styler): expected = dedent( """\ \\begin{tabular}{rrl} - {A} & {B} & {C} \\\\ + A & B & C \\\\ 0 & -0.61 & ab \\\\ 1 & -1.22 & cd \\\\ \\end{tabular} @@ -384,8 +384,8 @@ def test_comprehensive(df, environment): \\rowcolors{3}{pink}{} \\begin{tabular}{rlrlr} \\toprule -{} & {} & \\multicolumn{2}{r}{Z} & {Y} \\\\ -{} & {} & {a} & {b} & {c} \\\\ + & & \\multicolumn{2}{r}{Z} & Y \\\\ + & & a & b & c \\\\ \\midrule \\multirow[c]{2}{*}{A} & a & 0 & \\textbf{\\cellcolor[rgb]{1,1,0.6}{-0.61}} & ab \\\\ & b & 1 & -1.22 & cd \\\\ @@ -579,12 +579,12 @@ def test_longtable_comprehensive(styler): \\begin{longtable}{lrrl} \\caption[short]{full} \\label{fig:A} \\\\ \\toprule - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ \\midrule \\endfirsthead \\caption[]{full} \\\\ \\toprule - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ \\midrule \\endhead \\midrule @@ -606,9 +606,9 @@ def test_longtable_minimal(styler): expected = dedent( """\ \\begin{longtable}{lrrl} - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ \\endfirsthead - {} & {A} & {B} & {C} \\\\ + & A & B & C \\\\ \\endhead \\multicolumn{4}{r}{Continued on next page} \\\\ \\endfoot @@ -622,27 +622,34 @@ def test_longtable_minimal(styler): @pytest.mark.parametrize( - "sparse, exp", + "sparse, exp, siunitx", [ - (True, "{} & \\multicolumn{2}{r}{A} & {B}"), - (False, "{} & {A} & {A} & {B}"), + (True, "{} & \\multicolumn{2}{r}{A} & {B}", True), + (False, "{} & {A} & {A} & {B}", True), + (True, " & \\multicolumn{2}{r}{A} & B", False), + (False, " & A & A & B", False), ], ) -def test_longtable_multiindex_columns(df, sparse, exp): +def test_longtable_multiindex_columns(df, sparse, exp, siunitx): cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) df.columns = cidx + with_si = "{} & {a} & {b} & {c} \\\\" + without_si = " & a & b & c \\\\" expected = dedent( f"""\ - \\begin{{longtable}}{{lrrl}} + \\begin{{longtable}}{{l{"SS" if siunitx else "rr"}l}} {exp} \\\\ - {{}} & {{a}} & {{b}} & {{c}} \\\\ + {with_si if siunitx else without_si} \\endfirsthead {exp} \\\\ - {{}} & {{a}} & {{b}} & {{c}} \\\\ + {with_si if siunitx else without_si} \\endhead """ ) - assert expected in df.style.to_latex(environment="longtable", sparse_columns=sparse) + result = df.style.to_latex( + environment="longtable", sparse_columns=sparse, siunitx=siunitx + ) + assert expected in result @pytest.mark.parametrize( @@ -660,7 +667,7 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp): expected = dedent( f"""\ {cap_exp1}{lab_exp} \\\\ - {{}} & {{A}} & {{B}} & {{C}} \\\\ + & A & B & C \\\\ \\endfirsthead {cap_exp2} \\\\ """ @@ -671,8 +678,15 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp): @pytest.mark.parametrize("index", [True, False]) -@pytest.mark.parametrize("columns", [True, False]) -def test_apply_map_header_render_mi(df, index, columns): +@pytest.mark.parametrize( + "columns, siunitx", + [ + (True, True), + (True, False), + (False, False), + ], +) +def test_apply_map_header_render_mi(df, 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"] @@ -687,7 +701,7 @@ def test_apply_map_header_render_mi(df, index, columns): if columns: styler.applymap_index(func, axis="columns") - result = styler.to_latex() + result = styler.to_latex(siunitx=siunitx) expected_index = dedent( """\ @@ -698,13 +712,17 @@ def test_apply_map_header_render_mi(df, index, columns): ) assert (expected_index in result) is index - expected_columns = dedent( + exp_cols_si = dedent( """\ {} & {} & \\multicolumn{2}{r}{\\bfseries{Z}} & {Y} \\\\ {} & {} & {a} & {b} & {\\bfseries{c}} \\\\ """ ) - assert (expected_columns in result) is columns + exp_cols_no_si = """\ + & & \\multicolumn{2}{r}{\\bfseries{Z}} & Y \\\\ + & & a & b & \\bfseries{c} \\\\ +""" + assert ((exp_cols_si if siunitx else exp_cols_no_si) in result) is columns def test_repr_option(styler): @@ -713,3 +731,8 @@ def test_repr_option(styler): with option_context("styler.render.repr", "latex"): assert "\\begin{tabular}" in styler._repr_latex_()[:15] assert styler._repr_html_() is None + + +def test_siunitx_basic_headers(styler): + assert "{} & {A} & {B} & {C} \\\\" in styler.to_latex(siunitx=True) + assert " & A & B & C \\\\" in styler.to_latex() # default siunitx=False