Skip to content

ENH: added optional caption and label arguments to DataFrame.to_latex() #25437

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 27 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8982b90
ENH: added optional caption and label support to DataFrame.to_latex()…
jeschwar Feb 25, 2019
2ce8c67
PEP8 compliance for ENH #25436
jeschwar Feb 25, 2019
0e9d6c7
added versionadded lines in NDFrame.to_latex() docstring (#25436)
jeschwar Mar 2, 2019
4e615d2
removed UserWarning when only caption is passed to NDFrame.to_latex()…
jeschwar Mar 2, 2019
911a491
moved code for table beginning and end writing into functions (#25436)
jeschwar Mar 4, 2019
1a19e3a
Merge remote-tracking branch 'upstream/master' into tolatex_caption_l…
jeschwar Mar 22, 2019
59e7f04
removed use of pytest fixture 'frame' which was not doing anything (#…
jeschwar Mar 22, 2019
3e9cf63
LatexFormatter methods for creating begin/end latex environments retu…
jeschwar Apr 12, 2019
5e31aa5
revised docstring for NDFrame.to_latex() (#25436)
jeschwar Apr 12, 2019
86b3d31
added whatsnew entry (#25436)
jeschwar Apr 27, 2019
3994a65
revised docstrings to follow numpydoc format (#25436)
jeschwar May 5, 2019
961aed3
merge master (#25436)
jeschwar Jun 27, 2019
3eb433b
reverted to methodology of commit 911a491 (#25436)
jeschwar Jun 27, 2019
a7df686
added Parameters to docstrings in pandas/io/formats/latex.py (#25436)
jeschwar Jul 19, 2019
4969d3b
changed location of LatexFormatter._write_tabular_end() in source cod…
jeschwar Jul 19, 2019
1f4ef45
negated logic used in if-statements in LatexFormatter._write_*() meth…
jeschwar Jul 19, 2019
b9640de
revised docstring for NDFrame.to_latex() (#25436)
jeschwar Jul 19, 2019
7bdc15f
merge master (#25436)
jeschwar Jul 29, 2019
4a1cd9d
removed entry from whatsnew/v0.25.0.rst (#25436)
jeschwar Jul 30, 2019
b6a52aa
revised version numbers in rst directives to 0.26.0 (#25436)
jeschwar Jul 30, 2019
3d45fe5
created whatsnew/v0.26.0.rst and added entry (#25436)
jeschwar Jul 30, 2019
bf34040
moved whatsnew entry from v0.26.0.rst to v1.0.0.rst (#25436)
jeschwar Aug 21, 2019
6c8bf33
ran black on latex.py and test_to_latex.py (#25436)
jeschwar Aug 21, 2019
6366cfa
merge master (#25436)
jeschwar Aug 29, 2019
6da6760
corrected version number in NDFrame.to_latex() docstring (#25436)
jeschwar Aug 31, 2019
b97b083
Merge remote-tracking branch 'upstream/master' into tolatex_caption_l…
TomAugspurger Sep 3, 2019
2c0f993
fix docstring
TomAugspurger Sep 3, 2019
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
23 changes: 17 additions & 6 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2781,17 +2781,20 @@ class (index) object 'bird' 'bird' 'mammal' 'mammal'
def to_latex(self, buf=None, columns=None, col_space=None, header=True,
index=True, na_rep='NaN', formatters=None, float_format=None,
sparsify=None, index_names=True, bold_rows=False,
column_format=None, longtable=None, escape=None,
encoding=None, decimal='.', multicolumn=None,
multicolumn_format=None, multirow=None):
column_format=None, longtable=None, caption=None,
label=None, escape=None, encoding=None, decimal='.',
multicolumn=None, multicolumn_format=None, multirow=None):
r"""
Render an object to a LaTeX tabular environment table.
Render an object to a LaTeX tabular, longtable, or nested
table/tabular environment.

Render an object to a tabular environment table. You can splice
this into a LaTeX document. Requires \usepackage{booktabs}.

.. versionchanged:: 0.20.2
Added to Series
Added to Series.
.. versionchanged:: 0.25.0
Added caption and label arguments.

Parameters
----------
Expand Down Expand Up @@ -2831,6 +2834,13 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
By default, the value will be read from the pandas config
module. Use a longtable environment instead of tabular. Requires
adding a \usepackage{longtable} to your LaTeX preamble.
caption : str, optional
The LaTeX caption to be placed inside \caption{} in the output.
.. versionadded:: 0.25.0
label : str, optional
The LaTeX label to be placed inside \label{} in the output.
This is used with \ref{} in the main .tex file.
.. versionadded:: 0.25.0
escape : bool, optional
By default, the value will be read from the pandas config
module. When set to False prevents from escaping latex special
Expand Down Expand Up @@ -2908,7 +2918,8 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
index_names=index_names,
escape=escape, decimal=decimal)
formatter.to_latex(column_format=column_format, longtable=longtable,
encoding=encoding, multicolumn=multicolumn,
caption=caption, label=label, encoding=encoding,
multicolumn=multicolumn,
multicolumn_format=multicolumn_format,
multirow=multirow)

Expand Down
8 changes: 5 additions & 3 deletions pandas/io/formats/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,15 +676,17 @@ def _join_multiline(self, *strcols):
st = ed
return '\n\n'.join(str_lst)

def to_latex(self, column_format=None, longtable=False, encoding=None,
multicolumn=False, multicolumn_format=None, multirow=False):
def to_latex(self, column_format=None, longtable=False, caption=None,
label=None, encoding=None, multicolumn=False,
multicolumn_format=None, multirow=False):
"""
Render a DataFrame to a LaTeX tabular/longtable environment output.
"""

from pandas.io.formats.latex import LatexFormatter
latex_renderer = LatexFormatter(self, column_format=column_format,
longtable=longtable,
longtable=longtable, caption=caption,
label=label,
multicolumn=multicolumn,
multicolumn_format=multicolumn_format,
multirow=multirow)
Expand Down
98 changes: 85 additions & 13 deletions pandas/io/formats/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,23 @@ class LatexFormatter(TableFormatter):
"""

def __init__(self, formatter, column_format=None, longtable=False,
multicolumn=False, multicolumn_format=None, multirow=False):
caption=None, label=None, multicolumn=False,
multicolumn_format=None, multirow=False):
self.fmt = formatter
self.frame = self.fmt.frame
self.bold_rows = self.fmt.kwds.get('bold_rows', False)
self.column_format = column_format
self.longtable = longtable
self.caption = caption
self.label = label
self.multicolumn = multicolumn
self.multicolumn_format = multicolumn_format
self.multirow = multirow

def write_result(self, buf):
"""
Render a DataFrame to a LaTeX tabular/longtable environment output.
Render a DataFrame to a LaTeX tabular, longtable, or table/tabular
environment output.
"""

# string representation of the columns
Expand Down Expand Up @@ -106,14 +110,12 @@ def pad_empties(x):
raise AssertionError('column_format must be str or unicode, '
'not {typ}'.format(typ=type(column_format)))

if not self.longtable:
buf.write('\\begin{{tabular}}{{{fmt}}}\n'
.format(fmt=column_format))
buf.write('\\toprule\n')
if self.longtable:
self._write_longtable_begin(buf, column_format)
else:
buf.write('\\begin{{longtable}}{{{fmt}}}\n'
.format(fmt=column_format))
buf.write('\\toprule\n')
self._write_tabular_begin(buf, column_format)

buf.write('\\toprule\n')

ilevels = self.frame.index.nlevels
clevels = self.frame.columns.nlevels
Expand Down Expand Up @@ -164,11 +166,10 @@ def pad_empties(x):
if self.multirow and i < len(strrows) - 1:
self._print_cline(buf, i, len(strcols))

if not self.longtable:
buf.write('\\bottomrule\n')
buf.write('\\end{tabular}\n')
if self.longtable:
self._write_longtable_end(buf)
else:
buf.write('\\end{longtable}\n')
self._write_tabular_end(buf)

def _format_multicolumn(self, row, ilevels):
r"""
Expand Down Expand Up @@ -244,3 +245,74 @@ def _print_cline(self, buf, i, icol):
.format(cl=cl[1], icol=icol))
# remove entries that have been written to buffer
self.clinebuf = [x for x in self.clinebuf if x[0] != i]

def _write_tabular_begin(self, buf, column_format):
"""
write the beginning of a tabular environment or
nested table/tabular environments including caption and label
"""
if self.caption is None and self.label is None:
# then write output only in a tabular environment
pass
else:
# then write output in a nested table/tabular environment
if self.caption is None:
caption_ = ''
else:
caption_ = '\n\\caption{{{}}}'.format(self.caption)

if self.label is None:
label_ = ''
else:
label_ = '\n\\label{{{}}}'.format(self.label)

buf.write('\\begin{{table}}\n\\centering{}{}\n'.format(
caption_,
label_
))

buf.write('\\begin{{tabular}}{{{fmt}}}\n'.format(fmt=column_format))

def _write_longtable_begin(self, buf, column_format):
"""
write the beginning of a longtable environment including caption and
label if provided by user
"""
buf.write('\\begin{{longtable}}{{{fmt}}}\n'.format(fmt=column_format))

if self.caption is None and self.label is None:
pass
else:
if self.caption is None:
pass
else:
buf.write('\\caption{{{}}}'.format(self.caption))

if self.label is None:
pass
else:
buf.write('\\label{{{}}}'.format(self.label))

# a double-backslash is required at the end of the line
# as discussed here:
# https://tex.stackexchange.com/questions/219138
buf.write('\\\\\n')

def _write_tabular_end(self, buf):
"""
write the end of a tabular environment or nested table/tabular
environment
"""
buf.write('\\bottomrule\n')
buf.write('\\end{tabular}\n')
if self.caption is None and self.label is None:
pass
else:
buf.write('\\end{table}\n')

@staticmethod
def _write_longtable_end(buf):
"""
write the end of a longtable environment
"""
buf.write('\\end{longtable}\n')
156 changes: 153 additions & 3 deletions pandas/tests/io/formats/test_to_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,7 @@ def test_to_latex_special_escape(self):
"""
assert escaped_result == escaped_expected

def test_to_latex_longtable(self, frame):
frame.to_latex(longtable=True)

def test_to_latex_longtable(self):
df = DataFrame({'a': [1, 2], 'b': ['b1', 'b2']})
withindex_result = df.to_latex(longtable=True)
withindex_expected = r"""\begin{longtable}{lrl}
Expand Down Expand Up @@ -442,6 +440,158 @@ def test_to_latex_longtable(self, frame):
with3columns_result = df.to_latex(index=False, longtable=True)
assert r"\multicolumn{3}" in with3columns_result

def test_to_latex_caption_label(self):
# GH 25436
the_caption = 'a table in a \\texttt{table/tabular} environment'
the_label = 'tab:table_tabular'

df = DataFrame({'a': [1, 2], 'b': ['b1', 'b2']})
Copy link
Member

@gfyoung gfyoung Feb 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see multiple tests here in this one that you've written. In the interests of modularity, let's break up into multiple test methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem but it would not be consistent with the other test methods in test_to_latex.py; would that be an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gfyoung no problem but it would not be consistent with the other test methods in test_to_latex.py; would that be an issue?


# test when only the caption is provided
result_c = df.to_latex(
caption=the_caption
)

expected_c = r"""\begin{table}
\centering
\caption{a table in a \texttt{table/tabular} environment}
\begin{tabular}{lrl}
\toprule
{} & a & b \\
\midrule
0 & 1 & b1 \\
1 & 2 & b2 \\
\bottomrule
\end{tabular}
\end{table}
"""
assert result_c == expected_c

# test when only the label is provided
result_l = df.to_latex(
label=the_label
)

expected_l = r"""\begin{table}
\centering
\label{tab:table_tabular}
\begin{tabular}{lrl}
\toprule
{} & a & b \\
\midrule
0 & 1 & b1 \\
1 & 2 & b2 \\
\bottomrule
\end{tabular}
\end{table}
"""
assert result_l == expected_l

# test when the caption and the label are provided
result_cl = df.to_latex(
caption=the_caption,
label=the_label
)

expected_cl = r"""\begin{table}
\centering
\caption{a table in a \texttt{table/tabular} environment}
\label{tab:table_tabular}
\begin{tabular}{lrl}
\toprule
{} & a & b \\
\midrule
0 & 1 & b1 \\
1 & 2 & b2 \\
\bottomrule
\end{tabular}
\end{table}
"""
assert result_cl == expected_cl

def test_to_latex_longtable_caption_label(self):
# GH 25436
the_caption = 'a table in a \\texttt{longtable} environment'
the_label = 'tab:longtable'

df = DataFrame({'a': [1, 2], 'b': ['b1', 'b2']})

# test when only the caption is provided
result_c = df.to_latex(
longtable=True,
caption=the_caption
)

expected_c = r"""\begin{longtable}{lrl}
\caption{a table in a \texttt{longtable} environment}\\
\toprule
{} & a & b \\
\midrule
\endhead
\midrule
\multicolumn{3}{r}{{Continued on next page}} \\
\midrule
\endfoot

\bottomrule
\endlastfoot
0 & 1 & b1 \\
1 & 2 & b2 \\
\end{longtable}
"""
assert result_c == expected_c

# test when only the label is provided
result_l = df.to_latex(
longtable=True,
label=the_label,
)

expected_l = r"""\begin{longtable}{lrl}
\label{tab:longtable}\\
\toprule
{} & a & b \\
\midrule
\endhead
\midrule
\multicolumn{3}{r}{{Continued on next page}} \\
\midrule
\endfoot

\bottomrule
\endlastfoot
0 & 1 & b1 \\
1 & 2 & b2 \\
\end{longtable}
"""
assert result_l == expected_l

# test when the caption and the label are provided
result_cl = df.to_latex(
longtable=True,
caption=the_caption,
label=the_label,
)

expected_cl = r"""\begin{longtable}{lrl}
\caption{a table in a \texttt{longtable} environment}\label{tab:longtable}\\
\toprule
{} & a & b \\
\midrule
\endhead
\midrule
\multicolumn{3}{r}{{Continued on next page}} \\
\midrule
\endfoot

\bottomrule
\endlastfoot
0 & 1 & b1 \\
1 & 2 & b2 \\
\end{longtable}
"""
assert result_cl == expected_cl

def test_to_latex_escape_special_chars(self):
special_characters = ['&', '%', '$', '#', '_', '{', '}', '~', '^',
'\\']
Expand Down