-
-
Notifications
You must be signed in to change notification settings - Fork 18.6k
ENH: Added multicolumn/multirow support for latex #14184
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -650,13 +650,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): | ||
def to_latex(self, column_format=None, longtable=False, encoding=None, | ||
multicolumn=False, multicolumn_format=None, multirow=False): | ||
""" | ||
Render a DataFrame to a LaTeX tabular/longtable environment output. | ||
""" | ||
|
||
latex_renderer = LatexFormatter(self, column_format=column_format, | ||
longtable=longtable) | ||
longtable=longtable, | ||
multicolumn=multicolumn, | ||
multicolumn_format=multicolumn_format, | ||
multirow=multirow) | ||
|
||
if encoding is None: | ||
encoding = 'ascii' if compat.PY2 else 'utf-8' | ||
|
@@ -824,11 +828,15 @@ class LatexFormatter(TableFormatter): | |
HTMLFormatter | ||
""" | ||
|
||
def __init__(self, formatter, column_format=None, longtable=False): | ||
def __init__(self, formatter, column_format=None, longtable=False, | ||
multicolumn=False, multicolumn_format=None, multirow=False): | ||
self.fmt = formatter | ||
self.frame = self.fmt.frame | ||
self.column_format = column_format | ||
self.longtable = longtable | ||
self.multicolumn = multicolumn | ||
self.multicolumn_format = multicolumn_format | ||
self.multirow = multirow | ||
|
||
def write_result(self, buf): | ||
""" | ||
|
@@ -850,14 +858,21 @@ def get_col_type(dtype): | |
else: | ||
return 'l' | ||
|
||
# reestablish the MultiIndex that has been joined by _to_str_column | ||
if self.fmt.index and isinstance(self.frame.index, MultiIndex): | ||
clevels = self.frame.columns.nlevels | ||
strcols.pop(0) | ||
name = any(self.frame.index.names) | ||
cname = any(self.frame.columns.names) | ||
lastcol = self.frame.index.nlevels - 1 | ||
for i, lev in enumerate(self.frame.index.levels): | ||
lev2 = lev.format() | ||
blank = ' ' * len(lev2[0]) | ||
lev3 = [blank] * clevels | ||
# display column names in last index-column | ||
if cname and i == lastcol: | ||
lev3 = [x if x else '{}' for x in self.frame.columns.names] | ||
else: | ||
lev3 = [blank] * clevels | ||
if name: | ||
lev3.append(lev.name) | ||
for level_idx, group in itertools.groupby( | ||
|
@@ -885,10 +900,15 @@ def get_col_type(dtype): | |
buf.write('\\begin{longtable}{%s}\n' % column_format) | ||
buf.write('\\toprule\n') | ||
|
||
nlevels = self.frame.columns.nlevels | ||
ilevels = self.frame.index.nlevels | ||
clevels = self.frame.columns.nlevels | ||
nlevels = clevels | ||
if any(self.frame.index.names): | ||
nlevels += 1 | ||
for i, row in enumerate(zip(*strcols)): | ||
strrows = list(zip(*strcols)) | ||
self.clinebuf = [] | ||
|
||
for i, row in enumerate(strrows): | ||
if i == nlevels and self.fmt.header: | ||
buf.write('\\midrule\n') # End of header | ||
if self.longtable: | ||
|
@@ -910,15 +930,98 @@ def get_col_type(dtype): | |
if x else '{}') for x in row] | ||
else: | ||
crow = [x if x else '{}' for x in row] | ||
if i < clevels and self.fmt.header and self.multicolumn: | ||
# sum up columns to multicolumns | ||
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. Can you put the content contained in this if statement in a separate method on the class? Eg call it something like 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. Are class-methods preferred over nested-functions? |
||
crow = self._format_multicolumn(crow, ilevels) | ||
if (i >= nlevels and self.fmt.index and self.multirow and | ||
ilevels > 1): | ||
# sum up rows to multirows | ||
crow = self._format_multirow(crow, ilevels, i, strrows) | ||
buf.write(' & '.join(crow)) | ||
buf.write(' \\\\\n') | ||
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') | ||
else: | ||
buf.write('\\end{longtable}\n') | ||
|
||
def _format_multicolumn(self, row, ilevels): | ||
""" | ||
Combine columns belonging to a group to a single multicolumn entry | ||
according to self.multicolumn_format | ||
|
||
e.g.: | ||
a & & & b & c & | ||
will become | ||
\multicolumn{3}{l}{a} & b & \multicolumn{2}{l}{c} | ||
""" | ||
row2 = list(row[:ilevels]) | ||
ncol = 1 | ||
coltext = '' | ||
|
||
def append_col(): | ||
# write multicolumn if needed | ||
if ncol > 1: | ||
row2.append('\\multicolumn{{{0:d}}}{{{1:s}}}{{{2:s}}}' | ||
.format(ncol, self.multicolumn_format, | ||
coltext.strip())) | ||
# don't modify where not needed | ||
else: | ||
row2.append(coltext) | ||
for c in row[ilevels:]: | ||
# if next col has text, write the previous | ||
if c.strip(): | ||
if coltext: | ||
append_col() | ||
coltext = c | ||
ncol = 1 | ||
# if not, add it to the previous multicolumn | ||
else: | ||
ncol += 1 | ||
# write last column name | ||
if coltext: | ||
append_col() | ||
return row2 | ||
|
||
def _format_multirow(self, row, ilevels, i, rows): | ||
""" | ||
Check following rows, whether row should be a multirow | ||
|
||
e.g.: becomes: | ||
a & 0 & \multirow{2}{*}{a} & 0 & | ||
& 1 & & 1 & | ||
b & 0 & \cline{1-2} | ||
b & 0 & | ||
""" | ||
for j in range(ilevels): | ||
if row[j].strip(): | ||
nrow = 1 | ||
for r in rows[i + 1:]: | ||
if not r[j].strip(): | ||
nrow += 1 | ||
else: | ||
break | ||
if nrow > 1: | ||
# overwrite non-multirow entry | ||
row[j] = '\\multirow{{{0:d}}}{{*}}{{{1:s}}}'.format( | ||
nrow, row[j].strip()) | ||
# save when to end the current block with \cline | ||
self.clinebuf.append([i + nrow - 1, j + 1]) | ||
return row | ||
|
||
def _print_cline(self, buf, i, icol): | ||
""" | ||
Print clines after multirow-blocks are finished | ||
""" | ||
for cl in self.clinebuf: | ||
if cl[0] == i: | ||
buf.write('\cline{{{0:d}-{1:d}}}\n'.format(cl[1], icol)) | ||
# remove entries that have been written to buffer | ||
self.clinebuf = [x for x in self.clinebuf if x[0] != i] | ||
|
||
|
||
class HTMLFormatter(TableFormatter): | ||
|
||
|
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.
Is there a reason to have both nlevels and clevels
Uh oh!
There was an error while loading. Please reload this page.
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.
NoKinda.
I used clevels to keep from fiddling with the handling of the index-names-line.
On the compiled side there's no difference if i just use the nlevels, but the resulting code differs, and i'd rather not change the behaviour in parts that don't need to be modified.