Skip to content

Commit 70d0dd0

Browse files
authored
ENH: add set_td_classes method for CSS class addition to data cells (#36159)
1 parent 73cdfc4 commit 70d0dd0

File tree

3 files changed

+90
-2
lines changed

3 files changed

+90
-2
lines changed

doc/source/whatsnew/v1.2.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Other enhancements
104104
- :meth:`DataFrame.applymap` now supports ``na_action`` (:issue:`23803`)
105105
- :class:`Index` with object dtype supports division and multiplication (:issue:`34160`)
106106
- :meth:`DataFrame.explode` and :meth:`Series.explode` now support exploding of sets (:issue:`35614`)
107-
-
107+
- `Styler` now allows direct CSS class name addition to individual data cells (:issue:`36159`)
108108

109109
.. _whatsnew_120.api_breaking.python:
110110

pandas/io/formats/style.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def __init__(
171171
self.cell_ids = cell_ids
172172
self.na_rep = na_rep
173173

174+
self.cell_context: Dict[str, Any] = {}
175+
174176
# display_funcs maps (row, col) -> formatting function
175177

176178
def default_display_func(x):
@@ -262,7 +264,7 @@ def format_attr(pair):
262264
idx_lengths = _get_level_lengths(self.index)
263265
col_lengths = _get_level_lengths(self.columns, hidden_columns)
264266

265-
cell_context = dict()
267+
cell_context = self.cell_context
266268

267269
n_rlvls = self.data.index.nlevels
268270
n_clvls = self.data.columns.nlevels
@@ -499,6 +501,70 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style
499501
self._display_funcs[(i, j)] = formatter
500502
return self
501503

504+
def set_td_classes(self, classes: DataFrame) -> "Styler":
505+
"""
506+
Add string based CSS class names to data cells that will appear within the
507+
`Styler` HTML result. These classes are added within specified `<td>` elements.
508+
509+
Parameters
510+
----------
511+
classes : DataFrame
512+
DataFrame containing strings that will be translated to CSS classes,
513+
mapped by identical column and index values that must exist on the
514+
underlying `Styler` data. None, NaN values, and empty strings will
515+
be ignored and not affect the rendered HTML.
516+
517+
Returns
518+
-------
519+
self : Styler
520+
521+
Examples
522+
--------
523+
>>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"])
524+
>>> classes = pd.DataFrame([
525+
... ["min-val red", "", "blue"],
526+
... ["red", None, "blue max-val"]
527+
... ], index=df.index, columns=df.columns)
528+
>>> df.style.set_td_classes(classes)
529+
530+
Using `MultiIndex` columns and a `classes` `DataFrame` as a subset of the
531+
underlying,
532+
533+
>>> df = pd.DataFrame([[1,2],[3,4]], index=["a", "b"],
534+
... columns=[["level0", "level0"], ["level1a", "level1b"]])
535+
>>> classes = pd.DataFrame(["min-val"], index=["a"],
536+
... columns=[["level0"],["level1a"]])
537+
>>> df.style.set_td_classes(classes)
538+
539+
Form of the output with new additional css classes,
540+
541+
>>> df = pd.DataFrame([[1]])
542+
>>> css = pd.DataFrame(["other-class"])
543+
>>> s = Styler(df, uuid="_", cell_ids=False).set_td_classes(css)
544+
>>> s.hide_index().render()
545+
'<style type="text/css" ></style>'
546+
'<table id="T__" >'
547+
' <thead>'
548+
' <tr><th class="col_heading level0 col0" >0</th></tr>'
549+
' </thead>'
550+
' <tbody>'
551+
' <tr><td class="data row0 col0 other-class" >1</td></tr>'
552+
' </tbody>'
553+
'</table>'
554+
555+
"""
556+
classes = classes.reindex_like(self.data)
557+
558+
mask = (classes.isna()) | (classes.eq(""))
559+
self.cell_context["data"] = {
560+
r: {c: [str(classes.iloc[r, c])]}
561+
for r, rn in enumerate(classes.index)
562+
for c, cn in enumerate(classes.columns)
563+
if not mask.iloc[r, c]
564+
}
565+
566+
return self
567+
502568
def render(self, **kwargs) -> str:
503569
"""
504570
Render the built up styles to HTML.
@@ -609,6 +675,7 @@ def clear(self) -> None:
609675
Returns None.
610676
"""
611677
self.ctx.clear()
678+
self.cell_context = {}
612679
self._todo = []
613680

614681
def _compute(self):

pandas/tests/io/formats/test_style.py

+21
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,27 @@ def test_no_cell_ids(self):
16911691
s = styler.render() # render twice to ensure ctx is not updated
16921692
assert s.find('<td class="data row0 col0" >') != -1
16931693

1694+
@pytest.mark.parametrize(
1695+
"classes",
1696+
[
1697+
DataFrame(
1698+
data=[["", "test-class"], [np.nan, None]],
1699+
columns=["A", "B"],
1700+
index=["a", "b"],
1701+
),
1702+
DataFrame(data=[["test-class"]], columns=["B"], index=["a"]),
1703+
DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]),
1704+
],
1705+
)
1706+
def test_set_data_classes(self, classes):
1707+
# GH 36159
1708+
df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"])
1709+
s = Styler(df, uuid="_", cell_ids=False).set_td_classes(classes).render()
1710+
assert '<td class="data row0 col0" >0</td>' in s
1711+
assert '<td class="data row0 col1 test-class" >1</td>' in s
1712+
assert '<td class="data row1 col0" >2</td>' in s
1713+
assert '<td class="data row1 col1" >3</td>' in s
1714+
16941715
def test_colspan_w3(self):
16951716
# GH 36223
16961717
df = pd.DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]])

0 commit comments

Comments
 (0)