@@ -116,8 +116,8 @@ class CSSToExcelConverter:
116116 focusing on font styling, backgrounds, borders and alignment.
117117
118118 Operates by first computing CSS styles in a fairly generic
119- way (see :meth:`compute_css`) then determining Excel style
120- properties from CSS properties (see :meth:`build_xlstyle`).
119+ way (see :meth: `compute_css`) then determining Excel style
120+ properties from CSS properties (see :meth: `build_xlstyle`).
121121
122122 Parameters
123123 ----------
@@ -587,14 +587,15 @@ def __init__(
587587
588588 def _format_value (self , val ):
589589 if is_scalar (val ) and missing .isna (val ):
590- val = self .na_rep
590+ return self .na_rep
591591 elif is_float (val ):
592592 if missing .isposinf_scalar (val ):
593- val = self .inf_rep
593+ return self .inf_rep
594594 elif missing .isneginf_scalar (val ):
595- val = f"-{ self .inf_rep } "
595+ return f"-{ self .inf_rep } "
596596 elif self .float_format is not None :
597- val = float (self .float_format % val )
597+ return float (self .float_format % val )
598+
598599 if getattr (val , "tzinfo" , None ) is not None :
599600 raise ValueError (
600601 "Excel does not support datetimes with "
@@ -616,15 +617,25 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
616617
617618 columns = self .columns
618619 merge_columns = self .merge_cells in {True , "columns" }
619- level_strs = columns ._format_multi (sparsify = merge_columns , include_names = False )
620+ NBSP = "\u00a0 "
621+
622+ fixed_levels = []
623+ for lvl in range (columns .nlevels ):
624+ vals = columns .get_level_values (lvl )
625+ fixed_levels .append (vals .fillna (NBSP ))
626+ fixed_columns = MultiIndex .from_arrays (fixed_levels , names = columns .names )
627+
628+ level_strs = fixed_columns ._format_multi (
629+ sparsify = merge_columns , include_names = False
630+ )
620631 level_lengths = get_level_lengths (level_strs )
621632 coloffset = 0
622633 lnum = 0
623634
624635 if self .index and isinstance (self .df .index , MultiIndex ):
625636 coloffset = self .df .index .nlevels - 1
626637
627- for lnum , name in enumerate (columns .names ):
638+ for lnum , name in enumerate (fixed_columns .names ):
628639 yield ExcelCell (
629640 row = lnum ,
630641 col = coloffset ,
@@ -633,7 +644,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
633644 )
634645
635646 for lnum , (spans , levels , level_codes ) in enumerate (
636- zip (level_lengths , columns .levels , columns .codes , strict = True )
647+ zip (level_lengths , fixed_columns .levels , fixed_columns .codes , strict = True )
637648 ):
638649 values = levels .take (level_codes )
639650 for i , span_val in spans .items ():
@@ -657,7 +668,6 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
657668 def _format_header_regular (self ) -> Iterable [ExcelCell ]:
658669 if self ._has_aliases or self .header :
659670 coloffset = 0
660-
661671 if self .index :
662672 coloffset = 1
663673 if isinstance (self .df .index , MultiIndex ):
@@ -673,7 +683,10 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
673683 )
674684 colnames = self .header
675685
676- for colindex , colname in enumerate (colnames ):
686+ NBSP = "\u00a0 "
687+ output_colnames = colnames .fillna (NBSP )
688+
689+ for colindex , colname in enumerate (output_colnames ):
677690 yield CssExcelCell (
678691 row = self .rowcounter ,
679692 col = colindex + coloffset ,
@@ -687,15 +700,14 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
687700
688701 def _format_header (self ) -> Iterable [ExcelCell ]:
689702 gen : Iterable [ExcelCell ]
690-
691703 if isinstance (self .columns , MultiIndex ):
692704 gen = self ._format_header_mi ()
693705 else :
694706 gen = self ._format_header_regular ()
695707
696708 gen2 : Iterable [ExcelCell ] = ()
697709
698- if self .df .index .names :
710+ if self .df .index .names and self . header is not False :
699711 row = [x if x is not None else "" for x in self .df .index .names ] + [
700712 ""
701713 ] * len (self .columns )
@@ -762,12 +774,11 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]:
762774 def _format_hierarchical_rows (self ) -> Iterable [ExcelCell ]:
763775 if self ._has_aliases or self .header :
764776 self .rowcounter += 1
765-
766777 gcolidx = 0
767778
768779 if self .index :
769- index_labels = self .df .index .names
770780 # check for aliases
781+ index_labels = self .df .index .names
771782 if self .index_label and isinstance (
772783 self .index_label , (list , tuple , np .ndarray , Index )
773784 ):
@@ -802,10 +813,8 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
802813 allow_fill = levels ._can_hold_na ,
803814 fill_value = levels ._na_value ,
804815 )
805- # GH#60099
806- if isinstance (values [0 ], Period ):
816+ if values .size > 0 and isinstance (values [0 ], Period ):
807817 values = values .to_timestamp ()
808-
809818 for i , span_val in spans .items ():
810819 mergestart , mergeend = None , None
811820 if span_val > 1 :
@@ -901,9 +910,7 @@ def write(
901910 write engine to use if writer is a path - you can also set this
902911 via the options ``io.excel.xlsx.writer``,
903912 or ``io.excel.xlsm.writer``.
904-
905913 {storage_options}
906-
907914 engine_kwargs: dict, optional
908915 Arbitrary keyword arguments passed to excel engine.
909916 """
0 commit comments