Skip to content

Commit b45f14a

Browse files
ENH: Write excel comments, via styler.to_excel() tooltips (pandas-dev#58070)
Co-Authored-By: diogomsmiranda <[email protected]>
1 parent 1165859 commit b45f14a

File tree

9 files changed

+252
-3
lines changed

9 files changed

+252
-3
lines changed

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Other enhancements
4040
- :meth:`Styler.format_index_names` can now be used to format the index and column names (:issue:`48936` and :issue:`47489`)
4141
- :class:`.errors.DtypeWarning` improved to include column names when mixed data types are detected (:issue:`58174`)
4242
- :func:`DataFrame.to_excel` argument ``merge_cells`` now accepts a value of ``"columns"`` to only merge :class:`MultiIndex` column header header cells (:issue:`35384`)
43+
- :func:`DataFrame.to_excel` now supports writing notes to an excel files via :meth:`Styler.set_tooltips` (:issue:`58070`)
4344
- :meth:`DataFrame.corrwith` now accepts ``min_periods`` as optional arguments, as in :meth:`DataFrame.corr` and :meth:`Series.corr` (:issue:`9490`)
4445
- :meth:`DataFrame.cummin`, :meth:`DataFrame.cummax`, :meth:`DataFrame.cumprod` and :meth:`DataFrame.cumsum` methods now have a ``numeric_only`` parameter (:issue:`53072`)
4546
- :meth:`DataFrame.ewm` now allows ``adjust=False`` when ``times`` is provided (:issue:`54328`)

pandas/core/generic.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -2126,10 +2126,11 @@ def _repr_data_resource_(self):
21262126
storage_options_versionadded="1.2.0",
21272127
extra_parameters=textwrap.dedent(
21282128
"""\
2129-
engine_kwargs : dict, optional
2130-
Arbitrary keyword arguments passed to excel engine.
2131-
"""
2129+
engine_kwargs : dict, optional
2130+
Arbitrary keyword arguments passed to excel engine.
2131+
"""
21322132
),
2133+
extra_examples="",
21332134
)
21342135
def to_excel(
21352136
self,
@@ -2261,6 +2262,8 @@ def to_excel(
22612262
automatically chosen depending on the file extension):
22622263
22632264
>>> df1.to_excel("output1.xlsx", engine="xlsxwriter") # doctest: +SKIP
2265+
{extra_examples}
2266+
End of examples.
22642267
"""
22652268
if engine_kwargs is None:
22662269
engine_kwargs = {}

pandas/io/excel/_base.py

+3
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,7 @@ def _write_cells(
12121212
startrow: int = 0,
12131213
startcol: int = 0,
12141214
freeze_panes: tuple[int, int] | None = None,
1215+
notes: DataFrame | None = None,
12151216
) -> None:
12161217
"""
12171218
Write given formatted cells into Excel an excel sheet
@@ -1220,6 +1221,8 @@ def _write_cells(
12201221
----------
12211222
cells : generator
12221223
cell of formatted data to save to Excel sheet
1224+
notes: DataFrame
1225+
DataFrame containing notes to be written to the Excel sheet
12231226
sheet_name : str, default None
12241227
Name of Excel sheet, if None, then use self.cur_sheet
12251228
startrow : upper left cell row to dump data frame

pandas/io/excel/_odswriter.py

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
WriteExcelBuffer,
2828
)
2929

30+
from pandas.core.frame import DataFrame
31+
3032
from pandas.io.formats.excel import ExcelCell
3133

3234

@@ -99,6 +101,7 @@ def _write_cells(
99101
startrow: int = 0,
100102
startcol: int = 0,
101103
freeze_panes: tuple[int, int] | None = None,
104+
notes: DataFrame | None = None,
102105
) -> None:
103106
"""
104107
Write the frame cells using odf
@@ -110,6 +113,14 @@ def _write_cells(
110113
)
111114
from odf.text import P
112115

116+
if notes is not None:
117+
raise NotImplementedError(
118+
"""
119+
Notes are not supported by the odswriter engine,
120+
see https://github.com/eea/odfpy
121+
"""
122+
)
123+
113124
sheet_name = self._get_sheet_name(sheet_name)
114125
assert sheet_name is not None
115126

pandas/io/excel/_openpyxl.py

+23
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
WriteExcelBuffer,
3838
)
3939

40+
from pandas.core.frame import DataFrame
41+
4042

4143
class OpenpyxlWriter(ExcelWriter):
4244
_engine = "openpyxl"
@@ -447,7 +449,10 @@ def _write_cells(
447449
startrow: int = 0,
448450
startcol: int = 0,
449451
freeze_panes: tuple[int, int] | None = None,
452+
notes: DataFrame | None = None,
450453
) -> None:
454+
from openpyxl.comments import Comment
455+
451456
# Write the frame cells using openpyxl.
452457
sheet_name = self._get_sheet_name(sheet_name)
453458

@@ -484,6 +489,10 @@ def _write_cells(
484489
row=freeze_panes[0] + 1, column=freeze_panes[1] + 1
485490
)
486491

492+
notes_col = None
493+
if notes is not None and cells is not None:
494+
notes_col = startcol + next(cells).col + 1
495+
487496
for cell in cells:
488497
xcell = wks.cell(
489498
row=startrow + cell.row + 1, column=startcol + cell.col + 1
@@ -530,6 +539,20 @@ def _write_cells(
530539
for k, v in style_kwargs.items():
531540
setattr(xcell, k, v)
532541

542+
if notes is None or notes_col is None:
543+
return
544+
545+
for row_idx, val in enumerate(notes.itertuples(index=False)):
546+
for col_idx, note in enumerate(val):
547+
xcell = wks.cell(
548+
# first row has columns and openpyxl starts counting at 1, not 0
549+
row=row_idx + 2,
550+
column=col_idx + notes_col, # n columns with indexes
551+
)
552+
if note:
553+
comment = Comment(str(note), "")
554+
xcell.comment = comment
555+
533556

534557
class OpenpyxlReader(BaseExcelReader["Workbook"]):
535558
@doc(storage_options=_shared_docs["storage_options"])

pandas/io/excel/_xlsxwriter.py

+20
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
WriteExcelBuffer,
2121
)
2222

23+
from pandas.core.frame import DataFrame
24+
2325

2426
class _XlsxStyler:
2527
# Map from openpyxl-oriented styles to flatter xlsxwriter representation
@@ -245,6 +247,7 @@ def _write_cells(
245247
startrow: int = 0,
246248
startcol: int = 0,
247249
freeze_panes: tuple[int, int] | None = None,
250+
notes: DataFrame | None = None,
248251
) -> None:
249252
# Write the frame cells using xlsxwriter.
250253
sheet_name = self._get_sheet_name(sheet_name)
@@ -258,6 +261,10 @@ def _write_cells(
258261
if validate_freeze_panes(freeze_panes):
259262
wks.freeze_panes(*(freeze_panes))
260263

264+
notes_col = None
265+
if notes is not None and cells is not None:
266+
notes_col = startcol + next(cells).col
267+
261268
for cell in cells:
262269
val, fmt = self._value_with_fmt(cell.val)
263270

@@ -282,3 +289,16 @@ def _write_cells(
282289
)
283290
else:
284291
wks.write(startrow + cell.row, startcol + cell.col, val, style)
292+
293+
if notes is None or notes_col is None:
294+
return
295+
296+
for row_idx, row in enumerate(notes.itertuples(index=False)):
297+
for col_idx, note in enumerate(row):
298+
if note == "":
299+
continue
300+
wks.write_comment(
301+
row_idx + 1, # first row has columns
302+
col_idx + notes_col, # n columns with indexes
303+
str(note),
304+
)

pandas/io/formats/excel.py

+4
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,10 @@ def __init__(
557557
) -> None:
558558
self.rowcounter = 0
559559
self.na_rep = na_rep
560+
self.notes = None
560561
if not isinstance(df, DataFrame):
562+
if df.tooltips is not None:
563+
self.notes = df.tooltips.tt_data
561564
self.styler = df
562565
self.styler._compute() # calculate applied styles
563566
df = df.data
@@ -954,6 +957,7 @@ def write(
954957
startrow=startrow,
955958
startcol=startcol,
956959
freeze_panes=freeze_panes,
960+
notes=self.notes,
957961
)
958962
finally:
959963
# make sure to close opened file handles

pandas/io/formats/style.py

+14
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383

8484
from pandas import ExcelWriter
8585

86+
import textwrap
8687

8788
####
8889
# Shared Doc Strings
@@ -538,6 +539,19 @@ def set_tooltips(
538539
storage_options=_shared_docs["storage_options"],
539540
storage_options_versionadded="1.5.0",
540541
extra_parameters="",
542+
extra_examples=textwrap.dedent(
543+
"""\
544+
If you wish to write excel notes to the workbook, you can do so by
545+
passing a DataFrame to ``set_tooltips``. This process is independent
546+
from writing data to the workbook, therefore both DataFrames can have
547+
different dimensions.
548+
549+
>>> notes = pd.DataFrame(
550+
... [["cell 1", "cell 2"], ["cell 3", "cell 4"]],
551+
... ) # doctest: +SKIP
552+
>>> df1.style.set_tooltips(notes).to_excel("output.xlsx") # doctest: +SKIP
553+
"""
554+
),
541555
)
542556
def to_excel(
543557
self,

0 commit comments

Comments
 (0)