Skip to content

Commit 32f10e5

Browse files
committed
ENH: implement excel table (pandas-dev#24862)
1 parent 2fa0835 commit 32f10e5

File tree

3 files changed

+126
-11
lines changed

3 files changed

+126
-11
lines changed

pandas/core/generic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,7 +2103,7 @@ def to_excel(self, excel_writer, sheet_name="Sheet1", na_rep="",
21032103
float_format=None, columns=None, header=True, index=True,
21042104
index_label=None, startrow=0, startcol=0, engine=None,
21052105
merge_cells=True, encoding=None, inf_rep="inf", verbose=True,
2106-
freeze_panes=None):
2106+
freeze_panes=None, as_table=False):
21072107
df = self if isinstance(self, ABCDataFrame) else self.to_frame()
21082108

21092109
from pandas.io.formats.excel import ExcelFormatter
@@ -2115,7 +2115,7 @@ def to_excel(self, excel_writer, sheet_name="Sheet1", na_rep="",
21152115
inf_rep=inf_rep)
21162116
formatter.write(excel_writer, sheet_name=sheet_name, startrow=startrow,
21172117
startcol=startcol, freeze_panes=freeze_panes,
2118-
engine=engine)
2118+
engine=engine, as_table=as_table)
21192119

21202120
def to_json(self, path_or_buf=None, orient=None, date_format=None,
21212121
double_precision=10, force_ascii=True, date_unit='ms',

pandas/io/excel.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,14 @@ def _convert_to_protection(cls, protection_dict):
15791579

15801580
return Protection(**protection_dict)
15811581

1582+
@classmethod
1583+
def _to_excel_range(cls, startrow, startcol, endrow, endcol):
1584+
"""Convert (zero based) numeric coordinates to excel range."""
1585+
from openpyxl.utils.cell import get_column_letter
1586+
return (f"{get_column_letter(startcol+1)}{startrow+1}"
1587+
":"
1588+
f"{get_column_letter(endcol+1)}{endrow+1}")
1589+
15821590
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
15831591
freeze_panes=None):
15841592
# Write the frame cells using openpyxl.
@@ -1621,10 +1629,10 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
16211629
if cell.mergestart is not None and cell.mergeend is not None:
16221630

16231631
wks.merge_cells(
1624-
start_row=startrow + cell.row + 1,
1632+
startrow=startrow + cell.row + 1,
16251633
start_column=startcol + cell.col + 1,
16261634
end_column=startcol + cell.mergeend + 1,
1627-
end_row=startrow + cell.mergestart + 1
1635+
endrow=startrow + cell.mergestart + 1
16281636
)
16291637

16301638
# When cells are merged only the top-left cell is preserved
@@ -1645,6 +1653,65 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
16451653
for k, v in style_kwargs.items():
16461654
setattr(xcell, k, v)
16471655

1656+
def write_table(self, cells, sheet_name=None, startrow=0, startcol=0,
1657+
freeze_panes=None, header=True):
1658+
# Write the frame to an excel table using openpyxl.
1659+
1660+
from openpyxl.worksheet.table import Table, TableStyleInfo
1661+
sheet_name = self._get_sheet_name(sheet_name)
1662+
1663+
_style_cache = {}
1664+
1665+
if sheet_name in self.sheets:
1666+
wks = self.sheets[sheet_name]
1667+
else:
1668+
wks = self.book.create_sheet()
1669+
wks.title = sheet_name
1670+
self.sheets[sheet_name] = wks
1671+
1672+
if _validate_freeze_panes(freeze_panes):
1673+
wks.freeze_panes = wks.cell(row=freeze_panes[0] + 1,
1674+
column=freeze_panes[1] + 1)
1675+
1676+
header_rows = 1 if header > 0 else 0
1677+
1678+
n_cols = 0
1679+
n_rows = 0
1680+
header_cells = {}
1681+
for cell in cells:
1682+
val, fmt = self._value_with_fmt(cell.val)
1683+
if header and cell.row == 0 and cell.val:
1684+
header_cells[cell.col] = cell.val
1685+
continue
1686+
xcell = wks.cell(
1687+
row=startrow + cell.row + 1,
1688+
column=startcol + cell.col + 1,
1689+
value=val,
1690+
)
1691+
n_cols = max(n_cols, cell.col)
1692+
n_rows = max(n_rows, cell.row)
1693+
1694+
# add generic name for every unnamed (index) column that is included
1695+
[wks.cell(
1696+
row=startrow + 1,
1697+
column=startcol + col + 1,
1698+
value=(str(header_cells[col])
1699+
if col in header_cells
1700+
else f'Column{col + 1}'),
1701+
) for col in range(n_cols + 1)]
1702+
1703+
ref = self._to_excel_range(startrow, startcol, startrow + n_rows,
1704+
startcol + n_cols)
1705+
tab = Table(displayName="Table1", ref=ref, headerRowCount=header_rows)
1706+
1707+
# Add a default style with striped rows
1708+
style = TableStyleInfo(
1709+
name="TableStyleMedium9", showFirstColumn=False,
1710+
showLastColumn=False, showRowStripes=True, showColumnStripes=False)
1711+
tab.tableStyleInfo = style
1712+
1713+
wks.add_table(tab)
1714+
16481715

16491716
register_writer(_OpenpyxlWriter)
16501717

@@ -1992,5 +2059,43 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
19922059
startcol + cell.col,
19932060
val, style)
19942061

2062+
def write_table(self, cells, sheet_name=None, startrow=0, startcol=0,
2063+
freeze_panes=None, header=True):
2064+
# Write the frame to an excel table using xlsxwriter.
2065+
sheet_name = self._get_sheet_name(sheet_name)
2066+
2067+
if sheet_name in self.sheets:
2068+
wks = self.sheets[sheet_name]
2069+
else:
2070+
wks = self.book.add_worksheet(sheet_name)
2071+
self.sheets[sheet_name] = wks
2072+
2073+
if _validate_freeze_panes(freeze_panes):
2074+
wks.freeze_panes(*(freeze_panes))
2075+
2076+
n_cols = 0
2077+
n_rows = 0
2078+
header_cells = {}
2079+
for cell in cells:
2080+
val, fmt = self._value_with_fmt(cell.val)
2081+
if header and cell.row == 0 and cell.val:
2082+
header_cells[cell.col] = cell.val
2083+
continue
2084+
wks.write(startrow + cell.row,
2085+
startcol + cell.col,
2086+
val)
2087+
n_cols = max(n_cols, cell.col)
2088+
n_rows = max(n_rows, cell.row)
2089+
2090+
# add generic name for every unnamed (index) column that is included
2091+
columns = [{'header': str(header_cells[col])
2092+
if col in header_cells else f'Column{col + 1}'}
2093+
for col in range(n_cols + 1)]
2094+
2095+
options = {'columns': columns}
2096+
2097+
wks.add_table(startrow, startcol, startrow + n_rows,
2098+
startcol + n_cols, options)
2099+
19952100

19962101
register_writer(_XlsxWriter)

pandas/io/formats/excel.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -622,14 +622,18 @@ def _generate_body(self, coloffset):
622622
yield ExcelCell(self.rowcounter + i, colidx + coloffset, val,
623623
xlstyle)
624624

625-
def get_formatted_cells(self):
626-
for cell in itertools.chain(self._format_header(),
627-
self._format_body()):
625+
def get_formatted_cells(self, include_header=True):
626+
if include_header:
627+
cells = itertools.chain(self._format_header(),
628+
self._format_body())
629+
else:
630+
cells = self._format_body()
631+
for cell in cells:
628632
cell.val = self._format_value(cell.val)
629633
yield cell
630634

631635
def write(self, writer, sheet_name='Sheet1', startrow=0,
632-
startcol=0, freeze_panes=None, engine=None):
636+
startcol=0, freeze_panes=None, engine=None, as_table=False):
633637
"""
634638
writer : string or ExcelWriter object
635639
File path or existing ExcelWriter
@@ -657,8 +661,14 @@ def write(self, writer, sheet_name='Sheet1', startrow=0,
657661
need_save = True
658662

659663
formatted_cells = self.get_formatted_cells()
660-
writer.write_cells(formatted_cells, sheet_name,
661-
startrow=startrow, startcol=startcol,
662-
freeze_panes=freeze_panes)
664+
if as_table:
665+
writer.write_table(formatted_cells, sheet_name,
666+
startrow=startrow, startcol=startcol,
667+
freeze_panes=freeze_panes,
668+
header=self.header)
669+
else:
670+
writer.write_cells(formatted_cells, sheet_name,
671+
startrow=startrow, startcol=startcol,
672+
freeze_panes=freeze_panes)
663673
if need_save:
664674
writer.save()

0 commit comments

Comments
 (0)