diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index a08315818366f..060db772b9682 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -3908,6 +3908,20 @@ The look and feel of Excel worksheets created from pandas can be modified using * ``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``). +.. note:: + + As of Pandas 3.0, by default spreadsheets created with the ``to_excel`` method + will not contain any styling. Users wishing to bold text, add bordered styles, + etc in a worksheet output by ``to_excel`` can do so by using :meth:`Styler.to_excel` + to create styled excel files. For documentation on styling spreadsheets, see + `here `__. + + +.. code-block:: python + + css = "border: 1px solid black; font-weight: bold;" + df.style.map_index(lambda x: css).map_index(lambda x: css, axis=1).to_excel("myfile.xlsx") + Using the `Xlsxwriter`_ engine provides many options for controlling the format of an Excel worksheet created with the ``to_excel`` method. Excellent examples can be found in the `Xlsxwriter`_ documentation here: https://xlsxwriter.readthedocs.io/working_with_pandas.html diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 35769d9c5f0d8..4724ad5e14eba 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -90,6 +90,7 @@ Other API changes - 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`) - :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`) - Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`) +- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. Custom styling can still be done using :meth:`Styler.to_excel` (:issue:`54154`) - pickle and HDF (``.h5``) files created with Python 2 are no longer explicitly supported (:issue:`57387`) - diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 892f69e76359b..504b5aa560670 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -582,19 +582,6 @@ def __init__( self.merge_cells = merge_cells self.inf_rep = inf_rep - @property - def header_style(self) -> dict[str, dict[str, str | bool]]: - return { - "font": {"bold": True}, - "borders": { - "top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin", - }, - "alignment": {"horizontal": "center", "vertical": "top"}, - } - def _format_value(self, val): if is_scalar(val) and missing.isna(val): val = self.na_rep @@ -642,7 +629,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset, val=name, - style=self.header_style, + style=None, ) for lnum, (spans, levels, level_codes) in enumerate( @@ -657,7 +644,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset + i + 1, val=values[i], - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=lnum, css_col=i, @@ -673,7 +660,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset + i + 1, val=v, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=lnum, css_col=i, @@ -706,7 +693,7 @@ def _format_header_regular(self) -> Iterable[ExcelCell]: row=self.rowcounter, col=colindex + coloffset, val=colname, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=0, css_col=colindex, @@ -729,7 +716,7 @@ def _format_header(self) -> Iterable[ExcelCell]: ] * len(self.columns) if functools.reduce(lambda x, y: x and y, (x != "" for x in row)): gen2 = ( - ExcelCell(self.rowcounter, colindex, val, self.header_style) + ExcelCell(self.rowcounter, colindex, val, None) for colindex, val in enumerate(row) ) self.rowcounter += 1 @@ -763,7 +750,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]: self.rowcounter += 1 if index_label and self.header is not False: - yield ExcelCell(self.rowcounter - 1, 0, index_label, self.header_style) + yield ExcelCell(self.rowcounter - 1, 0, index_label, None) # write index_values index_values = self.df.index @@ -775,7 +762,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + idx, col=0, val=idxval, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=idx, css_col=0, @@ -811,7 +798,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: # if index labels are not empty go ahead and dump if com.any_not_none(*index_labels) and self.header is not False: for cidx, name in enumerate(index_labels): - yield ExcelCell(self.rowcounter - 1, cidx, name, self.header_style) + yield ExcelCell(self.rowcounter - 1, cidx, name, None) if self.merge_cells: # Format hierarchical rows as merged cells. @@ -838,7 +825,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + i, col=gcolidx, val=values[i], - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=i, css_col=gcolidx, @@ -856,7 +843,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + idx, col=gcolidx, val=indexcolval, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=idx, css_col=gcolidx, diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 3ca8637885639..58b297e4520bb 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -31,6 +31,28 @@ def assert_equal_cell_styles(cell1, cell2): assert cell1.protection.__dict__ == cell2.protection.__dict__ +def test_styler_default_values(): + # GH 54154 + openpyxl = pytest.importorskip("openpyxl") + df = DataFrame([{"A": 1, "B": 2, "C": 3}, {"A": 1, "B": 2, "C": 3}]) + + with tm.ensure_clean(".xlsx") as path: + with ExcelWriter(path, engine="openpyxl") as writer: + df.to_excel(writer, sheet_name="custom") + + with contextlib.closing(openpyxl.load_workbook(path)) as wb: + # Check font, spacing, indentation + assert wb["custom"].cell(1, 1).font.bold is False + assert wb["custom"].cell(1, 1).alignment.horizontal is None + assert wb["custom"].cell(1, 1).alignment.vertical is None + + # Check border + assert wb["custom"].cell(1, 1).border.bottom.color is None + assert wb["custom"].cell(1, 1).border.top.color is None + assert wb["custom"].cell(1, 1).border.left.color is None + assert wb["custom"].cell(1, 1).border.right.color is None + + @pytest.mark.parametrize( "engine", ["xlsxwriter", "openpyxl"], @@ -123,6 +145,36 @@ def test_styler_to_excel_unstyled(engine): ] +def test_styler_custom_style(): + # GH 54154 + css_style = "background-color: #111222" + openpyxl = pytest.importorskip("openpyxl") + df = DataFrame([{"A": 1, "B": 2}, {"A": 1, "B": 2}]) + + with tm.ensure_clean(".xlsx") as path: + with ExcelWriter(path, engine="openpyxl") as writer: + styler = df.style.map(lambda x: css_style) + styler.to_excel(writer, sheet_name="custom", index=False) + + with contextlib.closing(openpyxl.load_workbook(path)) as wb: + # Check font, spacing, indentation + assert wb["custom"].cell(1, 1).font.bold is False + assert wb["custom"].cell(1, 1).alignment.horizontal is None + assert wb["custom"].cell(1, 1).alignment.vertical is None + + # Check border + assert wb["custom"].cell(1, 1).border.bottom.color is None + assert wb["custom"].cell(1, 1).border.top.color is None + assert wb["custom"].cell(1, 1).border.left.color is None + assert wb["custom"].cell(1, 1).border.right.color is None + + # Check background color + assert wb["custom"].cell(2, 1).fill.fgColor.index == "00111222" + assert wb["custom"].cell(3, 1).fill.fgColor.index == "00111222" + assert wb["custom"].cell(2, 2).fill.fgColor.index == "00111222" + assert wb["custom"].cell(3, 2).fill.fgColor.index == "00111222" + + @pytest.mark.parametrize( "engine", ["xlsxwriter", "openpyxl"],