-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
ENH: add environment
, e.g. "longtable", to Styler.to_latex
#41866
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
Changes from 22 commits
cacb041
a4f75ab
1962c58
24d0e9c
084b2f6
3a054c3
bcf1168
2529b1d
c6e608b
bd7c5f8
dbe4154
b412a12
ea7f956
38fa8c4
cf70df1
7ad8802
ab7e6f1
e2ea44a
ec6ebc4
06962c6
8e3ff5b
67ffe24
903923b
17e090f
4ada8ba
78159fe
8164c2e
9d4972c
175e1e3
8081971
42151ac
2311db2
3da4ed7
4cd8263
3b7427d
3c5a2e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -428,6 +428,7 @@ def to_latex( | |
multirow_align: str = "c", | ||
multicol_align: str = "r", | ||
siunitx: bool = False, | ||
environment: str | None = None, | ||
encoding: str | None = None, | ||
convert_css: bool = False, | ||
): | ||
|
@@ -484,6 +485,11 @@ def to_latex( | |
the left, centrally, or at the right. | ||
siunitx : bool, default False | ||
Set to ``True`` to structure LaTeX compatible with the {siunitx} package. | ||
environment : str, optional | ||
If given, the environment that will replace 'table' in ``\\begin{table}``. | ||
If 'longtable' is specified then a more suitable template is | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small comment here. can you list the valid values? (or provide a link to them). can be a followon. |
||
rendered for which the ``position_float`` argument is nullified and does not | ||
impact the result. | ||
encoding : str, default "utf-8" | ||
Character encoding setting. | ||
convert_css : bool, default False | ||
|
@@ -787,6 +793,7 @@ def to_latex( | |
sparse_columns=sparse_columns, | ||
multirow_align=multirow_align, | ||
multicol_align=multicol_align, | ||
environment=environment, | ||
convert_css=convert_css, | ||
) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,5 @@ | ||
{% if parse_wrap(table_styles, caption) %} | ||
\begin{table} | ||
{%- set position = parse_table(table_styles, 'position') %} | ||
{%- if position is not none %} | ||
[{{position}}] | ||
{%- endif %} | ||
|
||
{% set position_float = parse_table(table_styles, 'position_float') %} | ||
{% if position_float is not none%} | ||
\{{position_float}} | ||
{% endif %} | ||
{% if caption and caption is string %} | ||
\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} | ||
|
||
{% elif caption and caption is sequence %} | ||
\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %} | ||
|
||
{% endif %} | ||
{% for style in table_styles %} | ||
{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format'] %} | ||
\{{style['selector']}}{{parse_table(table_styles, style['selector'])}} | ||
{% endif %} | ||
{% endfor %} | ||
{% endif %} | ||
\begin{tabular} | ||
{%- set column_format = parse_table(table_styles, 'column_format') %} | ||
{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %} | ||
|
||
{% set toprule = parse_table(table_styles, 'toprule') %} | ||
{% if toprule is not none %} | ||
\{{toprule}} | ||
{% endif %} | ||
{% for row in head %} | ||
{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ | ||
{% endfor %} | ||
{% set midrule = parse_table(table_styles, 'midrule') %} | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
{% for row in body %} | ||
{% for c in row %}{% if not loop.first %} & {% endif %} | ||
{%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %} | ||
{%- endfor %} \\ | ||
{% endfor %} | ||
{% set bottomrule = parse_table(table_styles, 'bottomrule') %} | ||
{% if bottomrule is not none %} | ||
\{{bottomrule}} | ||
{% endif %} | ||
\end{tabular} | ||
{% if parse_wrap(table_styles, caption) %} | ||
\end{table} | ||
{% if environment == "longtable" %} | ||
{% include "latex_longtable.tpl" %} | ||
{% else %} | ||
{% include "latex_table.tpl" %} | ||
{% endif %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
\begin{longtable} | ||
{%- set position = parse_table(table_styles, 'position') %} | ||
{%- if position is not none %} | ||
[{{position}}] | ||
{%- endif %} | ||
{%- set column_format = parse_table(table_styles, 'column_format') %} | ||
{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %} | ||
|
||
{% for style in table_styles %} | ||
{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format', 'label'] %} | ||
\{{style['selector']}}{{parse_table(table_styles, style['selector'])}} | ||
{% endif %} | ||
{% endfor %} | ||
{% if caption and caption is string %} | ||
\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} | ||
{%- set label = parse_table(table_styles, 'label') %} | ||
{%- if label is not none %} | ||
\label{{label}} | ||
{%- endif %} \\ | ||
{% elif caption and caption is sequence %} | ||
\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %} | ||
{%- set label = parse_table(table_styles, 'label') %} | ||
{%- if label is not none %} | ||
\label{{label}} | ||
{%- endif %} \\ | ||
{% endif %} | ||
{% set toprule = parse_table(table_styles, 'toprule') %} | ||
{% if toprule is not none %} | ||
\{{toprule}} | ||
{% endif %} | ||
{% for row in head %} | ||
{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ | ||
{% endfor %} | ||
{% set midrule = parse_table(table_styles, 'midrule') %} | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
\endfirsthead | ||
{% if caption and caption is string %} | ||
\caption[]{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} \\ | ||
{% elif caption and caption is sequence %} | ||
\caption[]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %} \\ | ||
{% endif %} | ||
{% if toprule is not none %} | ||
\{{toprule}} | ||
{% endif %} | ||
{% for row in head %} | ||
{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ | ||
{% endfor %} | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
\endhead | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
\multicolumn{% raw %}{{% endraw %}{{column_format|length}}{% raw %}}{% endraw %}{r}{Continued on next page} \\ | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
\endfoot | ||
{% set bottomrule = parse_table(table_styles, 'bottomrule') %} | ||
{% if bottomrule is not none %} | ||
\{{bottomrule}} | ||
{% endif %} | ||
\endlastfoot | ||
{% for row in body %} | ||
{% for c in row %}{% if not loop.first %} & {% endif %} | ||
{%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %} | ||
{%- endfor %} \\ | ||
{% endfor %} | ||
\end{longtable} | ||
{% raw %}{% endraw %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{% if environment or parse_wrap(table_styles, caption) %} | ||
\begin{% raw %}{{% endraw %}{{environment if environment else "table"}}{% raw %}}{% endraw %} | ||
{%- set position = parse_table(table_styles, 'position') %} | ||
{%- if position is not none %} | ||
[{{position}}] | ||
{%- endif %} | ||
|
||
{% set position_float = parse_table(table_styles, 'position_float') %} | ||
{% if position_float is not none%} | ||
\{{position_float}} | ||
{% endif %} | ||
{% if caption and caption is string %} | ||
\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} | ||
|
||
{% elif caption and caption is sequence %} | ||
\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %} | ||
|
||
{% endif %} | ||
{% for style in table_styles %} | ||
{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format'] %} | ||
\{{style['selector']}}{{parse_table(table_styles, style['selector'])}} | ||
{% endif %} | ||
{% endfor %} | ||
{% endif %} | ||
\begin{tabular} | ||
{%- set column_format = parse_table(table_styles, 'column_format') %} | ||
{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %} | ||
|
||
{% set toprule = parse_table(table_styles, 'toprule') %} | ||
{% if toprule is not none %} | ||
\{{toprule}} | ||
{% endif %} | ||
{% for row in head %} | ||
{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\ | ||
{% endfor %} | ||
{% set midrule = parse_table(table_styles, 'midrule') %} | ||
{% if midrule is not none %} | ||
\{{midrule}} | ||
{% endif %} | ||
{% for row in body %} | ||
{% for c in row %}{% if not loop.first %} & {% endif %} | ||
{%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %} | ||
{%- endfor %} \\ | ||
{% endfor %} | ||
{% set bottomrule = parse_table(table_styles, 'bottomrule') %} | ||
{% if bottomrule is not none %} | ||
\{{bottomrule}} | ||
{% endif %} | ||
\end{tabular} | ||
{% if environment or parse_wrap(table_styles, caption) %} | ||
\end{% raw %}{{% endraw %}{{environment if environment else "table"}}{% raw %}}{% endraw %} | ||
|
||
{% endif %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -484,8 +484,131 @@ def test_parse_latex_css_conversion(css, expected): | |
assert result == expected | ||
|
||
|
||
@pytest.mark.parametrize("environment", ["tabular", "longtable"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tabular or table? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "tabular" is correct, this is a quirk of the structure of longtable to parametrize the test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, but should it work with table as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no because {tabular} is a sub environment of any other environment including {table}, except {longtable}, i.e. we have:
this tests the inner environment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I think it is necessary to add one end-to-end test for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't necessary. There is already This test is designed to selectively test only the relevant parts of the template(s) which convert css to latex styles, which only occurs in data-cells. Since there are two templates, where the inner environment is defined as either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but in My point is as follows. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I can see some confusion has arisen here, since But with respect to broader testing I think cases are covered through dependencies, for example:
However, reflecting on your comments, I have removed |
||
@pytest.mark.parametrize( | ||
"convert, exp", [(True, "bfseries"), (False, "font-weightbold")] | ||
) | ||
def test_parse_latex_css_convert_minimal(styler, environment, convert, exp): | ||
# parameters ensure longtable template is also tested | ||
styler.highlight_max(props="font-weight:bold;") | ||
result = styler.to_latex(convert_css=convert, environment=environment) | ||
expected = dedent( | ||
f"""\ | ||
0 & 0 & \\{exp} -0.61 & ab \\\\ | ||
1 & \\{exp} 1 & -1.22 & \\{exp} cd \\\\ | ||
\\end{{{environment}}} | ||
""" | ||
) | ||
assert expected in result | ||
|
||
|
||
def test_parse_latex_css_conversion_option(): | ||
css = [("command", "option--latex--wrap")] | ||
expected = [("command", "option--wrap")] | ||
result = _parse_latex_css_conversion(css) | ||
assert result == expected | ||
|
||
|
||
def test_longtable_comprehensive(styler): | ||
result = styler.to_latex( | ||
environment="longtable", hrules=True, label="fig:A", caption=("full", "short") | ||
) | ||
expected = dedent( | ||
"""\ | ||
\\begin{longtable}{lrrl} | ||
\\caption[short]{full} \\label{fig:A} \\\\ | ||
\\toprule | ||
{} & {A} & {B} & {C} \\\\ | ||
\\midrule | ||
\\endfirsthead | ||
\\caption[]{full} \\\\ | ||
\\toprule | ||
{} & {A} & {B} & {C} \\\\ | ||
\\midrule | ||
\\endhead | ||
\\midrule | ||
\\multicolumn{4}{r}{Continued on next page} \\\\ | ||
\\midrule | ||
\\endfoot | ||
\\bottomrule | ||
\\endlastfoot | ||
0 & 0 & -0.61 & ab \\\\ | ||
1 & 1 & -1.22 & cd \\\\ | ||
\\end{longtable} | ||
""" | ||
) | ||
assert result == expected | ||
|
||
|
||
def test_longtable_minimal(styler): | ||
result = styler.to_latex(environment="longtable") | ||
expected = dedent( | ||
"""\ | ||
\\begin{longtable}{lrrl} | ||
{} & {A} & {B} & {C} \\\\ | ||
\\endfirsthead | ||
{} & {A} & {B} & {C} \\\\ | ||
\\endhead | ||
\\multicolumn{4}{r}{Continued on next page} \\\\ | ||
\\endfoot | ||
\\endlastfoot | ||
0 & 0 & -0.61 & ab \\\\ | ||
1 & 1 & -1.22 & cd \\\\ | ||
\\end{longtable} | ||
""" | ||
) | ||
assert result == expected | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"sparse, exp", | ||
[(True, "{} & \\multicolumn{2}{r}{A} & {B}"), (False, "{} & {A} & {A} & {B}")], | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
def test_longtable_multiindex_columns(df, sparse, exp): | ||
cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) | ||
df.columns = cidx | ||
expected = dedent( | ||
f"""\ | ||
\\begin{{longtable}}{{lrrl}} | ||
{exp} \\\\ | ||
{{}} & {{a}} & {{b}} & {{c}} \\\\ | ||
\\endfirsthead | ||
{exp} \\\\ | ||
{{}} & {{a}} & {{b}} & {{c}} \\\\ | ||
\\endhead | ||
""" | ||
) | ||
assert expected in df.style.to_latex(environment="longtable", sparse_columns=sparse) | ||
|
||
|
||
@pytest.mark.parametrize("caption", ["full", ("full", "short")]) | ||
@pytest.mark.parametrize("label", [None, "tab:A"]) | ||
def test_longtable_caption_label(styler, caption, label): | ||
if isinstance(caption, str): | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cap1 = f"\\caption{{{caption}}}" | ||
cap2 = f"\\caption[]{{{caption}}}" | ||
elif isinstance(caption, tuple): | ||
cap1 = f"\\caption[{caption[1]}]{{{caption[0]}}}" | ||
cap2 = f"\\caption[]{{{caption[0]}}}" | ||
|
||
lab = "" if label is None else f" \\label{{{label}}}" | ||
|
||
expected = dedent( | ||
f"""\ | ||
{cap1}{lab} \\\\ | ||
{{}} & {{A}} & {{B}} & {{C}} \\\\ | ||
\\endfirsthead | ||
{cap2} \\\\ | ||
""" | ||
) | ||
assert expected in styler.to_latex( | ||
environment="longtable", caption=caption, label=label | ||
) | ||
|
||
|
||
def test_latex_environment(styler): | ||
result = styler.to_latex(environment="figure*") | ||
assert "\\begin{table}" not in result | ||
assert "\\end{table}" not in result | ||
assert "\\begin{figure*}" in result | ||
assert "\\end{figure*}" in result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure i love this name, maybe template: and this should be 'longtable', 'table' ?
your comment on position_float is hard to interpret here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
environment is the proper LaTeX name for these blocks: LaTeX environments
'longtable' and 'table' are common environments for this but there are others. e.g. see #37443
some arguments are nullified by the use of the 'longtable' environment, such as 'position_float'. can try and rephrase this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe should raise ValueError if invalid combinations of parameters are given
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there was already validation on
position_float
so this was non-contentious addition.