Skip to content

Commit f2abbd9

Browse files
authored
ENH: Styler.apply(map)_index made compatible with Styler.to_excel (#41995)
1 parent c5ccc18 commit f2abbd9

File tree

3 files changed

+227
-173
lines changed

3 files changed

+227
-173
lines changed

doc/source/whatsnew/v1.4.0.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ Styler
7272

7373
:class:`.Styler` has been further developed in 1.4.0. The following enhancements have been made:
7474

75-
- Styling and formatting of indexes has been added, with :meth:`.Styler.apply_index`, :meth:`.Styler.applymap_index` and :meth:`.Styler.format_index`. These mirror the signature of the methods already used to style and format data values, and work with both HTML and LaTeX format (:issue:`41893`, :issue:`43101`).
76-
- :meth:`.Styler.bar` introduces additional arguments to control alignment, display and colors (:issue:`26070`, :issue:`36419`, :issue:`43662`), and it also validates the input arguments ``width`` and ``height`` (:issue:`42511`).
75+
- Styling and formatting of indexes has been added, with :meth:`.Styler.apply_index`, :meth:`.Styler.applymap_index` and :meth:`.Styler.format_index`. These mirror the signature of the methods already used to style and format data values, and work with both HTML, LaTeX and Excel format (:issue:`41893`, :issue:`43101`, :issue:`41993`, :issue:`41995`).
76+
- :meth:`.Styler.bar` introduces additional arguments to control alignment and display (:issue:`26070`, :issue:`36419`), and it also validates the input arguments ``width`` and ``height`` (:issue:`42511`).
7777
- :meth:`.Styler.to_latex` introduces keyword argument ``environment``, which also allows a specific "longtable" entry through a separate jinja2 template (:issue:`41866`).
7878
- :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index``, ``sparse_columns``, ``bold_headers``, ``caption``, ``max_rows`` and ``max_columns`` (:issue:`41946`, :issue:`43149`, :issue:`42972`).
7979
- Keyword arguments ``level`` and ``names`` added to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for additional control of visibility of MultiIndexes and index names (:issue:`25475`, :issue:`43404`, :issue:`43346`)

pandas/io/formats/excel.py

+89-33
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import itertools
88
import re
99
from typing import (
10+
Any,
1011
Callable,
1112
Hashable,
1213
Iterable,
@@ -70,6 +71,26 @@ def __init__(
7071
self.mergeend = mergeend
7172

7273

74+
class CssExcelCell(ExcelCell):
75+
def __init__(
76+
self,
77+
row: int,
78+
col: int,
79+
val,
80+
style: dict | None,
81+
css_styles: dict[tuple[int, int], list[tuple[str, Any]]] | None,
82+
css_row: int,
83+
css_col: int,
84+
css_converter: Callable | None,
85+
**kwargs,
86+
):
87+
if css_styles and css_converter:
88+
css = ";".join(a + ":" + str(v) for (a, v) in css_styles[css_row, css_col])
89+
style = css_converter(css)
90+
91+
return super().__init__(row=row, col=col, val=val, style=style, **kwargs)
92+
93+
7394
class CSSToExcelConverter:
7495
"""
7596
A callable for converting CSS declarations to ExcelWriter styles
@@ -472,12 +493,14 @@ def __init__(
472493
self.na_rep = na_rep
473494
if not isinstance(df, DataFrame):
474495
self.styler = df
496+
self.styler._compute() # calculate applied styles
475497
df = df.data
476498
if style_converter is None:
477499
style_converter = CSSToExcelConverter()
478-
self.style_converter = style_converter
500+
self.style_converter: Callable | None = style_converter
479501
else:
480502
self.styler = None
503+
self.style_converter = None
481504
self.df = df
482505
if cols is not None:
483506

@@ -567,22 +590,35 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
567590
):
568591
values = levels.take(level_codes)
569592
for i, span_val in spans.items():
570-
spans_multiple_cells = span_val > 1
571-
yield ExcelCell(
593+
mergestart, mergeend = None, None
594+
if span_val > 1:
595+
mergestart, mergeend = lnum, coloffset + i + span_val
596+
yield CssExcelCell(
572597
row=lnum,
573598
col=coloffset + i + 1,
574599
val=values[i],
575600
style=self.header_style,
576-
mergestart=lnum if spans_multiple_cells else None,
577-
mergeend=(
578-
coloffset + i + span_val if spans_multiple_cells else None
579-
),
601+
css_styles=getattr(self.styler, "ctx_columns", None),
602+
css_row=lnum,
603+
css_col=i,
604+
css_converter=self.style_converter,
605+
mergestart=mergestart,
606+
mergeend=mergeend,
580607
)
581608
else:
582609
# Format in legacy format with dots to indicate levels.
583610
for i, values in enumerate(zip(*level_strs)):
584611
v = ".".join(map(pprint_thing, values))
585-
yield ExcelCell(lnum, coloffset + i + 1, v, self.header_style)
612+
yield CssExcelCell(
613+
row=lnum,
614+
col=coloffset + i + 1,
615+
val=v,
616+
style=self.header_style,
617+
css_styles=getattr(self.styler, "ctx_columns", None),
618+
css_row=lnum,
619+
css_col=i,
620+
css_converter=self.style_converter,
621+
)
586622

587623
self.rowcounter = lnum
588624

@@ -607,8 +643,15 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
607643
colnames = self.header
608644

609645
for colindex, colname in enumerate(colnames):
610-
yield ExcelCell(
611-
self.rowcounter, colindex + coloffset, colname, self.header_style
646+
yield CssExcelCell(
647+
row=self.rowcounter,
648+
col=colindex + coloffset,
649+
val=colname,
650+
style=self.header_style,
651+
css_styles=getattr(self.styler, "ctx_columns", None),
652+
css_row=0,
653+
css_col=colindex,
654+
css_converter=self.style_converter,
612655
)
613656

614657
def _format_header(self) -> Iterable[ExcelCell]:
@@ -668,8 +711,16 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]:
668711
index_values = self.df.index.to_timestamp()
669712

670713
for idx, idxval in enumerate(index_values):
671-
yield ExcelCell(self.rowcounter + idx, 0, idxval, self.header_style)
672-
714+
yield CssExcelCell(
715+
row=self.rowcounter + idx,
716+
col=0,
717+
val=idxval,
718+
style=self.header_style,
719+
css_styles=getattr(self.styler, "ctx_index", None),
720+
css_row=idx,
721+
css_col=0,
722+
css_converter=self.style_converter,
723+
)
673724
coloffset = 1
674725
else:
675726
coloffset = 0
@@ -721,30 +772,37 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
721772
)
722773

723774
for i, span_val in spans.items():
724-
spans_multiple_cells = span_val > 1
725-
yield ExcelCell(
775+
mergestart, mergeend = None, None
776+
if span_val > 1:
777+
mergestart = self.rowcounter + i + span_val - 1
778+
mergeend = gcolidx
779+
yield CssExcelCell(
726780
row=self.rowcounter + i,
727781
col=gcolidx,
728782
val=values[i],
729783
style=self.header_style,
730-
mergestart=(
731-
self.rowcounter + i + span_val - 1
732-
if spans_multiple_cells
733-
else None
734-
),
735-
mergeend=gcolidx if spans_multiple_cells else None,
784+
css_styles=getattr(self.styler, "ctx_index", None),
785+
css_row=i,
786+
css_col=gcolidx,
787+
css_converter=self.style_converter,
788+
mergestart=mergestart,
789+
mergeend=mergeend,
736790
)
737791
gcolidx += 1
738792

739793
else:
740794
# Format hierarchical rows with non-merged values.
741795
for indexcolvals in zip(*self.df.index):
742796
for idx, indexcolval in enumerate(indexcolvals):
743-
yield ExcelCell(
797+
yield CssExcelCell(
744798
row=self.rowcounter + idx,
745799
col=gcolidx,
746800
val=indexcolval,
747801
style=self.header_style,
802+
css_styles=getattr(self.styler, "ctx_index", None),
803+
css_row=idx,
804+
css_col=gcolidx,
805+
css_converter=self.style_converter,
748806
)
749807
gcolidx += 1
750808

@@ -756,22 +814,20 @@ def _has_aliases(self) -> bool:
756814
return is_list_like(self.header)
757815

758816
def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]:
759-
if self.styler is None:
760-
styles = None
761-
else:
762-
styles = self.styler._compute().ctx
763-
if not styles:
764-
styles = None
765-
xlstyle = None
766-
767817
# Write the body of the frame data series by series.
768818
for colidx in range(len(self.columns)):
769819
series = self.df.iloc[:, colidx]
770820
for i, val in enumerate(series):
771-
if styles is not None:
772-
css = ";".join([a + ":" + str(v) for (a, v) in styles[i, colidx]])
773-
xlstyle = self.style_converter(css)
774-
yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle)
821+
yield CssExcelCell(
822+
row=self.rowcounter + i,
823+
col=colidx + coloffset,
824+
val=val,
825+
style=None,
826+
css_styles=getattr(self.styler, "ctx", None),
827+
css_row=i,
828+
css_col=colidx,
829+
css_converter=self.style_converter,
830+
)
775831

776832
def get_formatted_cells(self) -> Iterable[ExcelCell]:
777833
for cell in itertools.chain(self._format_header(), self._format_body()):

0 commit comments

Comments
 (0)