Skip to content
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Other enhancements
- Added :meth:`~DataFrame.set_flags` for setting table-wide flags on a ``Series`` or ``DataFrame`` (:issue:`28394`)
- :class:`Index` with object dtype supports division and multiplication (:issue:`34160`)
- :meth:`DataFrame.explode` and :meth:`Series.explode` now support exploding of sets (:issue:`35614`)
-
- `Styler` now allows direct CSS class name addition to individual data cells (:issue:`36159`)

.. _whatsnew_120.api_breaking.python:

Expand Down
48 changes: 47 additions & 1 deletion pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def __init__(
self.cell_ids = cell_ids
self.na_rep = na_rep

self.cell_context: Dict = {}

# display_funcs maps (row, col) -> formatting function

def default_display_func(x):
Expand Down Expand Up @@ -262,7 +264,7 @@ def format_attr(pair):
idx_lengths = _get_level_lengths(self.index)
col_lengths = _get_level_lengths(self.columns, hidden_columns)

cell_context = dict()
cell_context = self.cell_context

n_rlvls = self.data.index.nlevels
n_clvls = self.data.columns.nlevels
Expand Down Expand Up @@ -499,6 +501,49 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Style
self._display_funcs[(i, j)] = formatter
return self

def set_data_classes(self, classes: DataFrame) -> "Styler":
"""
Add string based CSS class names to data cells that will appear in the
`Styler` HTML result.

Parameters
----------
classes : DataFrame
DataFrame containing strings that will be translated to CSS classes. Empty
strings, None, or NaN values will be ignored. DataFrame must have
identical rows and columns to the underlying `Styler` data.

Returns
-------
self : Styler

Examples
--------
>>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=['A', 'B', 'C'])
>>> classes = pd.DataFrame([
... ['min-num red', '', 'blue'],
... ['red', None, 'blue max-num']
... ], index=df.index, columns=df.columns)
>>> df.style.set_data_classes(classes)
"""
if not (
self.columns.equals(classes.columns) and self.index.equals(classes.index)
):
raise ValueError(
"CSS classes DataFrame must have identical column and index labelling "
"to underlying."
)

mask = (classes.isna()) | (classes.eq(""))
self.cell_context["data"] = {
r: {c: [classes.iloc[r, c]]}
for r, rn in enumerate(classes.index)
for c, cn in enumerate(classes.columns)
if not mask.iloc[r, c]
}

return self

def render(self, **kwargs) -> str:
"""
Render the built up styles to HTML.
Expand Down Expand Up @@ -609,6 +654,7 @@ def clear(self) -> None:
Returns None.
"""
self.ctx.clear()
self.cell_context = {}
self._todo = []

def _compute(self):
Expand Down
14 changes: 14 additions & 0 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,20 @@ def test_no_cell_ids(self):
s = styler.render() # render twice to ensure ctx is not updated
assert s.find('<td class="data row0 col0" >') != -1

def test_set_data_classes(self):
# GH 36159
df = pd.DataFrame(data=[[0, 1], [2, 3]])
classes = pd.DataFrame(
data=[["test-class", ""], [np.nan, None]],
columns=df.columns,
index=df.index,
)
s = Styler(df, uuid="_", cell_ids=False).set_data_classes(classes).render()
assert '<td class="data row0 col0 test-class" >0</td>' in s
assert '<td class="data row0 col1" >1</td>' in s
assert '<td class="data row1 col0" >2</td>' in s
assert '<td class="data row1 col1" >3</td>' in s


@td.skip_if_no_mpl
class TestStylerMatplotlibDep:
Expand Down