Skip to content

Commit 7d3558c

Browse files
Dacopsdiogomsmiranda
authored andcommitted
ENH: Write excel comments, via styler.to_excel() tooltips (pandas-dev#58070)
Co-Authored-By: Dacops <[email protected]>
1 parent 81a44fa commit 7d3558c

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
@@ -39,6 +39,7 @@ Other enhancements
3939
- Users can globally disable any ``PerformanceWarning`` by setting the option ``mode.performance_warnings`` to ``False`` (:issue:`56920`)
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`)
42+
- :func:`DataFrame.to_excel` now supports writing notes to an excel files via :meth:`Styler.set_tooltips` (:issue:`58070`)
4243
- :meth:`DataFrame.corrwith` now accepts ``min_periods`` as optional arguments, as in :meth:`DataFrame.corr` and :meth:`Series.corr` (:issue:`9490`)
4344
- :meth:`DataFrame.cummin`, :meth:`DataFrame.cummax`, :meth:`DataFrame.cumprod` and :meth:`DataFrame.cumsum` methods now have a ``numeric_only`` parameter (:issue:`53072`)
4445
- :meth:`DataFrame.fillna` and :meth:`Series.fillna` can now accept ``value=None``; for non-object dtype the corresponding NA value will be used (:issue:`57723`)

pandas/core/generic.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -2127,10 +2127,11 @@ def _repr_data_resource_(self):
21272127
storage_options_versionadded="1.2.0",
21282128
extra_parameters=textwrap.dedent(
21292129
"""\
2130-
engine_kwargs : dict, optional
2131-
Arbitrary keyword arguments passed to excel engine.
2132-
"""
2130+
engine_kwargs : dict, optional
2131+
Arbitrary keyword arguments passed to excel engine.
2132+
"""
21332133
),
2134+
extra_examples="",
21342135
)
21352136
def to_excel(
21362137
self,
@@ -2262,6 +2263,8 @@ def to_excel(
22622263
automatically chosen depending on the file extension):
22632264
22642265
>>> df1.to_excel("output1.xlsx", engine="xlsxwriter") # doctest: +SKIP
2266+
{extra_examples}
2267+
End of examples.
22652268
"""
22662269
if engine_kwargs is None:
22672270
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,7 +489,11 @@ def _write_cells(
484489
row=freeze_panes[0] + 1, column=freeze_panes[1] + 1
485490
)
486491

492+
notes_col = None
493+
487494
for cell in cells:
495+
if notes_col is None:
496+
notes_col = startcol + cell.col + 1
488497
xcell = wks.cell(
489498
row=startrow + cell.row + 1, column=startcol + cell.col + 1
490499
)
@@ -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,8 @@ def _write_cells(
258261
if validate_freeze_panes(freeze_panes):
259262
wks.freeze_panes(*(freeze_panes))
260263

264+
notes_col = None
265+
261266
for cell in cells:
262267
val, fmt = self._value_with_fmt(cell.val)
263268

@@ -281,4 +286,19 @@ def _write_cells(
281286
style,
282287
)
283288
else:
289+
if notes_col is None:
290+
notes_col = startcol + cell.col
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
@@ -553,7 +553,10 @@ def __init__(
553553
) -> None:
554554
self.rowcounter = 0
555555
self.na_rep = na_rep
556+
self.notes = None
556557
if not isinstance(df, DataFrame):
558+
if df.tooltips is not None:
559+
self.notes = df.tooltips.tt_data
557560
self.styler = df
558561
self.styler._compute() # calculate applied styles
559562
df = df.data
@@ -945,6 +948,7 @@ def write(
945948
startrow=startrow,
946949
startcol=startcol,
947950
freeze_panes=freeze_panes,
951+
notes=self.notes,
948952
)
949953
finally:
950954
# make sure to close opened file handles

pandas/io/formats/style.py

+14
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282

8383
from pandas import ExcelWriter
8484

85+
import textwrap
8586

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

0 commit comments

Comments
 (0)