Skip to content

Commit aa58447

Browse files
authored
CLN: Styler Types for CSS variables on ctx object. (#39660)
1 parent fea76c4 commit aa58447

File tree

5 files changed

+382
-532
lines changed

5 files changed

+382
-532
lines changed

doc/source/whatsnew/v0.20.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ For example, after running the following, ``styled.xlsx`` renders as below:
374374
df.iloc[0, 2] = np.nan
375375
df
376376
styled = (df.style
377-
.applymap(lambda val: 'color: %s' % 'red' if val < 0 else 'black')
377+
.applymap(lambda val: 'color:red;' if val < 0 else 'color:black;')
378378
.highlight_max())
379379
styled.to_excel('styled.xlsx', engine='openpyxl')
380380

doc/source/whatsnew/v1.3.0.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ Other enhancements
6565
- :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`)
6666
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`, :issue:`21266`, :issue:`39317`)
6767
- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`)
68-
- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None``. (:issue:`39359`)
68+
- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`)
69+
- :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`)
6970
- :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`)
7071
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
7172
- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)

pandas/io/formats/excel.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,8 @@ def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]:
773773
series = self.df.iloc[:, colidx]
774774
for i, val in enumerate(series):
775775
if styles is not None:
776-
xlstyle = self.style_converter(";".join(styles[i, colidx]))
776+
css = ";".join([a + ":" + str(v) for (a, v) in styles[i, colidx]])
777+
xlstyle = self.style_converter(css)
777778
yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle)
778779

779780
def get_formatted_cells(self) -> Iterable[ExcelCell]:

pandas/io/formats/style.py

+19-32
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@
5353
)
5454

5555
jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
56-
CSSSequence = Sequence[Tuple[str, Union[str, int, float]]]
57-
CSSProperties = Union[str, CSSSequence]
56+
57+
CSSPair = Tuple[str, Union[str, int, float]]
58+
CSSList = List[CSSPair]
59+
CSSProperties = Union[str, CSSList]
5860
CSSStyles = List[Dict[str, CSSProperties]]
5961

6062
try:
@@ -194,7 +196,7 @@ def __init__(
194196
# assign additional default vars
195197
self.hidden_index: bool = False
196198
self.hidden_columns: Sequence[int] = []
197-
self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list)
199+
self.ctx: DefaultDict[Tuple[int, int], CSSList] = defaultdict(list)
198200
self.cell_context: Dict[str, Any] = {}
199201
self._todo: List[Tuple[Callable, Tuple, Dict]] = []
200202
self.tooltips: Optional[_Tooltips] = None
@@ -414,7 +416,8 @@ def _translate(self):
414416
clabels = [[x] for x in clabels]
415417
clabels = list(zip(*clabels))
416418

417-
cellstyle_map = defaultdict(list)
419+
cellstyle_map: DefaultDict[Tuple[CSSPair, ...], List[str]] = defaultdict(list)
420+
418421
head = []
419422

420423
for r in range(n_clvls):
@@ -531,25 +534,21 @@ def _translate(self):
531534
}
532535

533536
# only add an id if the cell has a style
534-
props = []
537+
props: CSSList = []
535538
if self.cell_ids or (r, c) in ctx:
536539
row_dict["id"] = "_".join(cs[1:])
537-
for x in ctx[r, c]:
538-
# have to handle empty styles like ['']
539-
if x.count(":"):
540-
props.append(tuple(x.split(":")))
541-
else:
542-
props.append(("", ""))
540+
props.extend(ctx[r, c])
543541

544542
# add custom classes from cell context
545543
cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
546544
row_dict["class"] = " ".join(cs)
547545

548546
row_es.append(row_dict)
549-
cellstyle_map[tuple(props)].append(f"row{r}_col{c}")
547+
if props: # (), [] won't be in cellstyle_map, cellstyle respectively
548+
cellstyle_map[tuple(props)].append(f"row{r}_col{c}")
550549
body.append(row_es)
551550

552-
cellstyle = [
551+
cellstyle: List[Dict[str, Union[CSSList, List[str]]]] = [
553552
{"props": list(props), "selectors": selectors}
554553
for props, selectors in cellstyle_map.items()
555554
]
@@ -755,19 +754,14 @@ def render(self, **kwargs) -> str:
755754
self._compute()
756755
# TODO: namespace all the pandas keys
757756
d = self._translate()
758-
# filter out empty styles, every cell will have a class
759-
# but the list of props may just be [['', '']].
760-
# so we have the nested anys below
761-
trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])]
762-
d["cellstyle"] = trimmed
763757
d.update(kwargs)
764758
return self.template.render(**d)
765759

766760
def _update_ctx(self, attrs: DataFrame) -> None:
767761
"""
768-
Update the state of the Styler.
762+
Update the state of the Styler for data cells.
769763
770-
Collects a mapping of {index_label: ['<property>: <value>']}.
764+
Collects a mapping of {index_label: [('<property>', '<value>'), ..]}.
771765
772766
Parameters
773767
----------
@@ -776,20 +770,13 @@ def _update_ctx(self, attrs: DataFrame) -> None:
776770
Whitespace shouldn't matter and the final trailing ';' shouldn't
777771
matter.
778772
"""
779-
coli = {k: i for i, k in enumerate(self.columns)}
780-
rowi = {k: i for i, k in enumerate(self.index)}
781-
for jj in range(len(attrs.columns)):
782-
cn = attrs.columns[jj]
783-
j = coli[cn]
773+
for cn in attrs.columns:
784774
for rn, c in attrs[[cn]].itertuples():
785775
if not c:
786776
continue
787-
c = c.rstrip(";")
788-
if not c:
789-
continue
790-
i = rowi[rn]
791-
for pair in c.split(";"):
792-
self.ctx[(i, j)].append(pair)
777+
css_list = _maybe_convert_css_to_tuples(c)
778+
i, j = self.index.get_loc(rn), self.columns.get_loc(cn)
779+
self.ctx[(i, j)].extend(css_list)
793780

794781
def _copy(self, deepcopy: bool = False) -> Styler:
795782
styler = Styler(
@@ -2068,7 +2055,7 @@ def _maybe_wrap_formatter(
20682055
raise TypeError(msg)
20692056

20702057

2071-
def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence:
2058+
def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList:
20722059
"""
20732060
Convert css-string to sequence of tuples format if needed.
20742061
'color:red; border:1px solid black;' -> [('color', 'red'),

0 commit comments

Comments
 (0)