66 Any ,
77)
88
9- from pandas .io .excel ._base import ExcelWriter
10- from pandas .io .excel ._util import (
11- combine_kwargs ,
12- validate_freeze_panes ,
13- )
14-
159if TYPE_CHECKING :
16- from pandas ._typing import (
17- ExcelWriterIfSheetExists ,
18- FilePath ,
19- StorageOptions ,
20- WriteExcelBuffer ,
21- )
10+ from pandas ._typing import FilePath , StorageOptions , WriteExcelBuffer
11+
12+ from xlsxwriter import Workbook
13+
14+ from pandas .compat ._optional import import_optional_dependency
15+
16+ from pandas .io .excel ._base import ExcelWriter
17+ from pandas .io .excel ._util import validate_freeze_panes
2218
2319
2420class _XlsxStyler :
@@ -93,28 +89,44 @@ class _XlsxStyler:
9389 }
9490
9591 @classmethod
96- def convert (cls , style_dict , num_format_str = None ) -> dict [str , Any ]:
97- """
98- converts a style_dict to an xlsxwriter format dict
99-
100- Parameters
101- ----------
102- style_dict : style dictionary to convert
103- num_format_str : optional number format string
104- """
105- # Create a XlsxWriter format object.
106- props = {}
107-
108- if num_format_str is not None :
109- props ["num_format" ] = num_format_str
110-
111- if style_dict is None :
112- return props
113-
92+ def convert (
93+ cls ,
94+ style_dict : dict ,
95+ num_format_str : str | None = None ,
96+ ) -> dict [str , Any ]:
97+ """Convert a style_dict to an xlsxwriter format dict."""
98+ # Create a copy to avoid modifying the input
99+ style_dict = style_dict .copy ()
100+
101+ # Map CSS font-weight to xlsxwriter font-weight (bold)
102+ if style_dict .get ("font-weight" ) in ("bold" , "bolder" , 700 , "700" ) or (
103+ isinstance (style_dict .get ("font" ), dict )
104+ and style_dict ["font" ].get ("weight" ) in ("bold" , "bolder" , 700 , "700" )
105+ ):
106+ # For XLSXWriter, we need to set the font with bold=True
107+ style_dict = {"font" : {"bold" : True , "name" : "Calibri" , "size" : 11 }}
108+ # Also set the b property directly as it might be needed
109+ style_dict ["b" ] = True
110+
111+ # Handle font styles
112+ if "font-style" in style_dict and style_dict ["font-style" ] == "italic" :
113+ style_dict ["italic" ] = True
114+ del style_dict ["font-style" ]
115+
116+ # Convert CSS border styles to xlsxwriter format
117+ # border_map = {
118+ # "border-top": "top",
119+ # "border-right": "right",
120+ # "border-bottom": "bottom",
121+ # "border-left": "left",
122+ # }
114123 if "borders" in style_dict :
115124 style_dict = style_dict .copy ()
116125 style_dict ["border" ] = style_dict .pop ("borders" )
117126
127+ # Initialize props to track which properties we've processed
128+ props = {}
129+
118130 for style_group_key , style_group in style_dict .items ():
119131 for src , dst in cls .STYLE_MAPPING .get (style_group_key , []):
120132 # src is a sequence of keys into a nested dict
@@ -189,36 +201,29 @@ def __init__( # pyright: ignore[reportInconsistentConstructor]
189201 datetime_format : str | None = None ,
190202 mode : str = "w" ,
191203 storage_options : StorageOptions | None = None ,
192- if_sheet_exists : ExcelWriterIfSheetExists | None = None ,
193- engine_kwargs : dict [ str , Any ] | None = None ,
194- ** kwargs ,
204+ if_sheet_exists : str | None = None ,
205+ engine_kwargs : dict | None = None ,
206+ autofilter : bool = False ,
195207 ) -> None :
196208 # Use the xlsxwriter module as the Excel writer.
197- from xlsxwriter import Workbook
198-
199- engine_kwargs = combine_kwargs (engine_kwargs , kwargs )
200-
201- if mode == "a" :
202- raise ValueError ("Append mode is not supported with xlsxwriter!" )
203-
209+ import_optional_dependency ("xlsxwriter" )
204210 super ().__init__ (
205211 path ,
206- engine = engine ,
207- date_format = date_format ,
208- datetime_format = datetime_format ,
209212 mode = mode ,
210213 storage_options = storage_options ,
211214 if_sheet_exists = if_sheet_exists ,
212215 engine_kwargs = engine_kwargs ,
213216 )
214217
215- self ._engine_kwargs = engine_kwargs
218+ self ._engine_kwargs = engine_kwargs or {}
219+ self .autofilter = autofilter
220+ self ._book = None
216221
217222 try :
218- self ._book = Workbook (self ._handles .handle , ** engine_kwargs )
219- except TypeError :
223+ self ._book = Workbook (self ._handles .handle , ** self . _engine_kwargs ) # type: ignore[arg-type]
224+ except TypeError as e :
220225 self ._handles .handle .close ()
221- raise
226+ raise RuntimeError ( "Failed to create XlsxWriter workbook" ) from e
222227
223228 @property
224229 def book (self ):
@@ -260,14 +265,32 @@ def _write_cells(
260265 if validate_freeze_panes (freeze_panes ):
261266 wks .freeze_panes (* (freeze_panes ))
262267
263- min_row = None
264- min_col = None
265- max_row = None
266- max_col = None
268+ # Initialize bounds with first cell
269+ first_cell = next (cells , None )
270+ if first_cell is None :
271+ return
272+
273+ # Initialize with first cell's position
274+ min_row = startrow + first_cell .row
275+ min_col = startcol + first_cell .col
276+ max_row = min_row
277+ max_col = min_col
267278
268- header_bold = bool (self ._engine_kwargs .get ("header_bold" , False )) if hasattr (self , "_engine_kwargs" ) else False
269- bold_format = self .book .add_format ({"bold" : True }) if header_bold else None
279+ # Process first cell
280+ val , fmt = self ._value_with_fmt (first_cell .val )
281+ stylekey = json .dumps (first_cell .style )
282+ if fmt :
283+ stylekey += fmt
270284
285+ if stylekey in style_dict :
286+ style = style_dict [stylekey ]
287+ else :
288+ style = self .book .add_format (_XlsxStyler .convert (first_cell .style , fmt ))
289+ style_dict [stylekey ] = style
290+
291+ wks .write (startrow + first_cell .row , startcol + first_cell .col , val , style )
292+
293+ # Process remaining cells
271294 for cell in cells :
272295 val , fmt = self ._value_with_fmt (cell .val )
273296
@@ -281,34 +304,43 @@ def _write_cells(
281304 style = self .book .add_format (_XlsxStyler .convert (cell .style , fmt ))
282305 style_dict [stylekey ] = style
283306
284- crow = startrow + cell .row
285- ccol = startcol + cell .col
286- if min_row is None or crow < min_row :
287- min_row = crow
288- if min_col is None or ccol < min_col :
289- min_col = ccol
290- if max_row is None or crow > max_row :
291- max_row = crow
292- if max_col is None or ccol > max_col :
293- max_col = ccol
307+ row = startrow + cell .row
308+ col = startcol + cell .col
309+
310+ # Write the cell
311+ wks .write (row , col , val , style )
312+
313+ # Update bounds
314+ min_row = min (min_row , row ) if min_row is not None else row
315+ min_col = min (min_col , col ) if min_col is not None else col
316+ max_row = max (max_row , row ) if max_row is not None else row
317+ max_col = max (max_col , col ) if max_col is not None else col
294318
295319 if cell .mergestart is not None and cell .mergeend is not None :
296320 wks .merge_range (
297- crow ,
298- ccol ,
321+ row ,
322+ col ,
299323 startrow + cell .mergestart ,
300324 startcol + cell .mergeend ,
301325 val ,
302326 style ,
303327 )
304328 else :
305- if bold_format is not None and (startrow == crow ):
306- wks .write (crow , ccol , val , bold_format )
307- else :
308- wks .write (crow , ccol , val , style )
309-
310- if hasattr (self , "_engine_kwargs" ) and bool (self ._engine_kwargs .get ("autofilter_header" , False )):
311- if min_row is not None and min_col is not None and max_row is not None and max_col is not None :
329+ wks .write (row , col , val , style )
330+
331+ # Apply autofilter if requested
332+ if getattr (self , "autofilter" , False ):
333+ wks .autofilter (min_row , min_col , max_row , max_col )
334+
335+ if hasattr (self , "_engine_kwargs" ) and bool (
336+ self ._engine_kwargs .get ("autofilter_header" , False )
337+ ):
338+ if (
339+ min_row is not None
340+ and min_col is not None
341+ and max_row is not None
342+ and max_col is not None
343+ ):
312344 try :
313345 wks .autofilter (min_row , min_col , max_row , max_col )
314346 except Exception :
0 commit comments