Skip to content

Commit

Permalink
ENH: Styler.apply(map)_index made compatible with Styler.to_excel (
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 authored Oct 19, 2021
1 parent c5ccc18 commit f2abbd9
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 173 deletions.
4 changes: 2 additions & 2 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ Styler

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

- 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`).
- :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`).
- 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`).
- :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`).
- :meth:`.Styler.to_latex` introduces keyword argument ``environment``, which also allows a specific "longtable" entry through a separate jinja2 template (:issue:`41866`).
- :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`).
- 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`)
Expand Down
122 changes: 89 additions & 33 deletions pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import itertools
import re
from typing import (
Any,
Callable,
Hashable,
Iterable,
Expand Down Expand Up @@ -70,6 +71,26 @@ def __init__(
self.mergeend = mergeend


class CssExcelCell(ExcelCell):
def __init__(
self,
row: int,
col: int,
val,
style: dict | None,
css_styles: dict[tuple[int, int], list[tuple[str, Any]]] | None,
css_row: int,
css_col: int,
css_converter: Callable | None,
**kwargs,
):
if css_styles and css_converter:
css = ";".join(a + ":" + str(v) for (a, v) in css_styles[css_row, css_col])
style = css_converter(css)

return super().__init__(row=row, col=col, val=val, style=style, **kwargs)


class CSSToExcelConverter:
"""
A callable for converting CSS declarations to ExcelWriter styles
Expand Down Expand Up @@ -472,12 +493,14 @@ def __init__(
self.na_rep = na_rep
if not isinstance(df, DataFrame):
self.styler = df
self.styler._compute() # calculate applied styles
df = df.data
if style_converter is None:
style_converter = CSSToExcelConverter()
self.style_converter = style_converter
self.style_converter: Callable | None = style_converter
else:
self.styler = None
self.style_converter = None
self.df = df
if cols is not None:

Expand Down Expand Up @@ -567,22 +590,35 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
):
values = levels.take(level_codes)
for i, span_val in spans.items():
spans_multiple_cells = span_val > 1
yield ExcelCell(
mergestart, mergeend = None, None
if span_val > 1:
mergestart, mergeend = lnum, coloffset + i + span_val
yield CssExcelCell(
row=lnum,
col=coloffset + i + 1,
val=values[i],
style=self.header_style,
mergestart=lnum if spans_multiple_cells else None,
mergeend=(
coloffset + i + span_val if spans_multiple_cells else None
),
css_styles=getattr(self.styler, "ctx_columns", None),
css_row=lnum,
css_col=i,
css_converter=self.style_converter,
mergestart=mergestart,
mergeend=mergeend,
)
else:
# Format in legacy format with dots to indicate levels.
for i, values in enumerate(zip(*level_strs)):
v = ".".join(map(pprint_thing, values))
yield ExcelCell(lnum, coloffset + i + 1, v, self.header_style)
yield CssExcelCell(
row=lnum,
col=coloffset + i + 1,
val=v,
style=self.header_style,
css_styles=getattr(self.styler, "ctx_columns", None),
css_row=lnum,
css_col=i,
css_converter=self.style_converter,
)

self.rowcounter = lnum

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

for colindex, colname in enumerate(colnames):
yield ExcelCell(
self.rowcounter, colindex + coloffset, colname, self.header_style
yield CssExcelCell(
row=self.rowcounter,
col=colindex + coloffset,
val=colname,
style=self.header_style,
css_styles=getattr(self.styler, "ctx_columns", None),
css_row=0,
css_col=colindex,
css_converter=self.style_converter,
)

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

for idx, idxval in enumerate(index_values):
yield ExcelCell(self.rowcounter + idx, 0, idxval, self.header_style)

yield CssExcelCell(
row=self.rowcounter + idx,
col=0,
val=idxval,
style=self.header_style,
css_styles=getattr(self.styler, "ctx_index", None),
css_row=idx,
css_col=0,
css_converter=self.style_converter,
)
coloffset = 1
else:
coloffset = 0
Expand Down Expand Up @@ -721,30 +772,37 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
)

for i, span_val in spans.items():
spans_multiple_cells = span_val > 1
yield ExcelCell(
mergestart, mergeend = None, None
if span_val > 1:
mergestart = self.rowcounter + i + span_val - 1
mergeend = gcolidx
yield CssExcelCell(
row=self.rowcounter + i,
col=gcolidx,
val=values[i],
style=self.header_style,
mergestart=(
self.rowcounter + i + span_val - 1
if spans_multiple_cells
else None
),
mergeend=gcolidx if spans_multiple_cells else None,
css_styles=getattr(self.styler, "ctx_index", None),
css_row=i,
css_col=gcolidx,
css_converter=self.style_converter,
mergestart=mergestart,
mergeend=mergeend,
)
gcolidx += 1

else:
# Format hierarchical rows with non-merged values.
for indexcolvals in zip(*self.df.index):
for idx, indexcolval in enumerate(indexcolvals):
yield ExcelCell(
yield CssExcelCell(
row=self.rowcounter + idx,
col=gcolidx,
val=indexcolval,
style=self.header_style,
css_styles=getattr(self.styler, "ctx_index", None),
css_row=idx,
css_col=gcolidx,
css_converter=self.style_converter,
)
gcolidx += 1

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

def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]:
if self.styler is None:
styles = None
else:
styles = self.styler._compute().ctx
if not styles:
styles = None
xlstyle = None

# Write the body of the frame data series by series.
for colidx in range(len(self.columns)):
series = self.df.iloc[:, colidx]
for i, val in enumerate(series):
if styles is not None:
css = ";".join([a + ":" + str(v) for (a, v) in styles[i, colidx]])
xlstyle = self.style_converter(css)
yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle)
yield CssExcelCell(
row=self.rowcounter + i,
col=colidx + coloffset,
val=val,
style=None,
css_styles=getattr(self.styler, "ctx", None),
css_row=i,
css_col=colidx,
css_converter=self.style_converter,
)

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

0 comments on commit f2abbd9

Please sign in to comment.