Skip to content

ENH: Added ability to freeze panes from DataFrame.to_excel() (#15160) #15291

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions doc/source/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2777,6 +2777,7 @@ Added support for Openpyxl >= 2.2
``'xlsxwriter'`` will produce an Excel 2007-format workbook (xlsx). If
omitted, an Excel 2007-formatted workbook is produced.


.. _io.excel.writers:

Excel writer engines
Expand Down Expand Up @@ -2823,6 +2824,18 @@ argument to ``to_excel`` and to ``ExcelWriter``. The built-in engines are:
df.to_excel('path_to_file.xlsx', sheet_name='Sheet1')
.. _io.excel.style:

Style and Formatting
''''''''''''''''''''

The look and feel of Excel worksheets created from pandas can be modified using the following parameters on the ``DataFrame``'s ``to_excel`` method.

- ``float_format`` : Format string for floating point numbers (default None)
- ``freeze_panes`` : A tuple of two integers representing the bottommost row and rightmost column to freeze. Each of these parameters is one-based, so (1, 1) will
freeze the first row and first column (default None)
Copy link
Member

Choose a reason for hiding this comment

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

this line will not be formatted nicely in the html docs (it needs two spaces at the start to have it indented in the list)



.. _io.clipboard:

Clipboard
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.20.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Other enhancements
- ``Series/DataFrame.resample.asfreq`` have gained a ``fill_value`` parameter, to fill missing values during resampling (:issue:`3715`).
- ``pandas.tools.hashing`` has gained a ``hash_tuples`` routine, and ``hash_pandas_object`` has gained the ability to hash a ``MultiIndex`` (:issue:`15224`)
- ``Series/DataFrame.squeeze()`` have gained the ``axis`` parameter. (:issue:`15339`)
- ``DataFrame.to_excel()`` has a new ``freeze_panes`` parameter to turn on Freeze Panes when exporting to Excel (:issue:`15160`)

.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations

Expand Down
19 changes: 17 additions & 2 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,8 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None,
def to_excel(self, excel_writer, sheet_name='Sheet1', na_rep='',
float_format=None, columns=None, header=True, index=True,
index_label=None, startrow=0, startcol=0, engine=None,
merge_cells=True, encoding=None, inf_rep='inf', verbose=True):
merge_cells=True, encoding=None, inf_rep='inf', verbose=True,
freeze_panes=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Add freeze_panes to the docstring here, which this function uses

_shared_docs['to_excel'] = """

from pandas.io.excel import ExcelWriter
need_save = False
if encoding is None:
Expand All @@ -1406,12 +1407,26 @@ def to_excel(self, excel_writer, sheet_name='Sheet1', na_rep='',
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep)

formatted_cells = formatter.get_formatted_cells()
freeze_panes = self._validate_freeze_panes(freeze_panes)
excel_writer.write_cells(formatted_cells, sheet_name,
startrow=startrow, startcol=startcol)
startrow=startrow, startcol=startcol,
freeze_panes=freeze_panes)
if need_save:
excel_writer.save()

def _validate_freeze_panes(self, freeze_panes):
if freeze_panes is not None:
if (
len(freeze_panes) == 2 and
all(isinstance(item, int) for item in freeze_panes)
):
return freeze_panes

raise ValueError("freeze_panes must be of form (row, column)"
" where row and column are integers")

def to_stata(self, fname, convert_dates=None, write_index=True,
encoding="latin-1", byteorder=None, time_stamp=None,
data_label=None, variable_labels=None):
Expand Down
7 changes: 6 additions & 1 deletion pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ def __setstate__(self, state):
# I/O Methods

_shared_docs['to_excel'] = """
Write %(klass)s to a excel sheet
Write %(klass)s to an excel sheet
%(versionadded_to_excel)s
Parameters
----------
Expand Down Expand Up @@ -1072,6 +1072,11 @@ def __setstate__(self, state):
inf_rep : string, default 'inf'
Representation for infinity (there is no native representation for
infinity in Excel)
freeze_panes : tuple of integer (length 2), default None
Specifies the bottommost row and rightmost column that
is to be frozen
Copy link
Contributor

Choose a reason for hiding this comment

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

add a versionadded tag here

Copy link
Member

Choose a reason for hiding this comment

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

Does this start at zero or one?

.. versionadded:: 0.20.0
Notes
-----
Expand Down
34 changes: 27 additions & 7 deletions pandas/io/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,8 @@ def engine(self):
pass

@abc.abstractmethod
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
"""
Write given formated cells into Excel an excel sheet
Expand All @@ -705,6 +706,8 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
Name of Excel sheet, if None, then use self.cur_sheet
startrow: upper left cell row to dump data frame
startcol: upper left cell column to dump data frame
freeze_panes: integer tuple of length 2
contains the bottom-most row and right-most column to freeze
"""
pass

Expand Down Expand Up @@ -804,7 +807,8 @@ def save(self):
"""
return self.book.save(self.path)

def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using openpyxl.
from openpyxl.cell import get_column_letter

Expand Down Expand Up @@ -904,7 +908,8 @@ class _Openpyxl20Writer(_Openpyxl1Writer):
engine = 'openpyxl20'
openpyxl_majorver = 2

def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using openpyxl.
from openpyxl.cell import get_column_letter

Expand Down Expand Up @@ -1311,7 +1316,8 @@ class _Openpyxl22Writer(_Openpyxl20Writer):
engine = 'openpyxl22'
openpyxl_majorver = 2

def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using openpyxl.
sheet_name = self._get_sheet_name(sheet_name)

Expand All @@ -1324,6 +1330,10 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
wks.title = sheet_name
self.sheets[sheet_name] = wks

if freeze_panes is not None:
wks.freeze_panes = wks.cell(row=freeze_panes[0] + 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

vaidate that its a len 2 tuples

column=freeze_panes[1] + 1)

for cell in cells:
xcell = wks.cell(
row=startrow + cell.row + 1,
Expand Down Expand Up @@ -1396,7 +1406,8 @@ def save(self):
"""
return self.book.save(self.path)

def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using xlwt.

sheet_name = self._get_sheet_name(sheet_name)
Expand All @@ -1407,6 +1418,11 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
wks = self.book.add_sheet(sheet_name)
self.sheets[sheet_name] = wks

if freeze_panes is not None:
wks.set_panes_frozen(True)
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

wks.set_horz_split_pos(freeze_panes[0])
wks.set_vert_split_pos(freeze_panes[1])

style_dict = {}

for cell in cells:
Expand Down Expand Up @@ -1518,11 +1534,12 @@ def save(self):
"""
Save workbook to disk.
"""

return self.book.close()

def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
freeze_panes=None):
# Write the frame cells using xlsxwriter.

sheet_name = self._get_sheet_name(sheet_name)

if sheet_name in self.sheets:
Expand All @@ -1533,6 +1550,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):

style_dict = {}

if freeze_panes is not None:
wks.freeze_panes(*(freeze_panes))
Copy link
Contributor

Choose a reason for hiding this comment

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

same (you may want to make a general validation for this somewhere)


for cell in cells:
val = _conv_value(cell.val)

Expand Down
12 changes: 10 additions & 2 deletions pandas/tests/io/test_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,14 @@ def test_true_and_false_value_options(self):
false_values=['bar'])
tm.assert_frame_equal(read_frame, expected)

def test_freeze_panes(self):
# GH15160
expected = DataFrame([[1, 2], [3, 4]], columns=['col1', 'col2'])
with ensure_clean(self.ext) as path:
expected.to_excel(path, "Sheet1", freeze_panes=(1, 1))
result = read_excel(path)
tm.assert_frame_equal(expected, result)


def raise_wrapper(major_ver):
def versioned_raise_wrapper(orig_method):
Expand Down Expand Up @@ -1873,7 +1881,7 @@ class OpenpyxlTests(ExcelWriterBase, tm.TestCase):
def test_to_excel_styleconverter(self):
_skip_if_no_openpyxl()
if not openpyxl_compat.is_compat(major_ver=1):
pytest.skip('incompatiable openpyxl version')
pytest.skip('incompatible openpyxl version')

import openpyxl

Expand Down Expand Up @@ -2095,7 +2103,7 @@ def test_to_excel_styleconverter(self):

def test_write_cells_merge_styled(self):
if not openpyxl_compat.is_compat(major_ver=2):
pytest.skip('incompatiable openpyxl version')
pytest.skip('incompatible openpyxl version')

from pandas.formats.format import ExcelCell

Expand Down