Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,14 @@
"\n",
"The precise structure of the CSS `class` attached to each cell is as follows.\n",
"\n",
"- Cells with Index and Column names include `index_name` and `level<k>` where `k` is its level in a MultiIndex\n",
"- Cells with Index names include `index_name` and `level<k>` where `k` is its level in a MultiIndex\n",
" \n",
" :::{versionchanged} 2.0.0\n",
" Column name cells are no longer attached to the `index_name` class</div>\n",
" :::\n",
"- :::{versionadded} 2.0.0\n",
" Cells with Column names include `col_name` and `level<k>` where `k` is its level in a MultiIndex\n",
" :::\n",
"- Index label cells include\n",
" + `row_heading`\n",
" + `level<k>` where `k` is the level in a MultiIndex\n",
Expand Down Expand Up @@ -2026,7 +2033,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "Python 3.10.4 ('pandas-testing')",
"language": "python",
"name": "python3"
},
Expand All @@ -2040,7 +2047,12 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.5"
"version": "3.10.4"
},
"vscode": {
"interpreter": {
"hash": "99ba7404bdbf380b6668594445346cc49dc5e126b0bc53947f71689e253b4235"
}
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Other enhancements
- :func:`timedelta_range` now supports a ``unit`` keyword ("s", "ms", "us", or "ns") to specify the desired resolution of the output index (:issue:`49824`)
- :meth:`DataFrame.to_json` now supports a ``mode`` keyword with supported inputs 'w' and 'a'. Defaulting to 'w', 'a' can be used when lines=True and orient='records' to append record oriented json lines to an existing json file. (:issue:`35849`)
- Added ``name`` parameter to :meth:`IntervalIndex.from_breaks`, :meth:`IntervalIndex.from_arrays` and :meth:`IntervalIndex.from_tuples` (:issue:`48911`)
-
- :meth:`.Styler.apply_index` and :meth:`.Styler.applymap_index` now supports a `names` keyword to specify styles for axis name cells (:issue:`48936`)

.. ---------------------------------------------------------------------------
.. _whatsnew_200.notable_bug_fixes:
Expand Down
89 changes: 79 additions & 10 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ class Styler(StylerRenderer):

CSS classes are attached to the generated HTML

* Index and Column names include ``index_name`` and ``level<k>``
* Index names include ``index_name`` and ``level<k>``
where `k` is its level in a MultiIndex
* Column names include ``col_name`` and ``level<k>``
where `k` is its level in a MultiIndex
* Index label cells include

Expand Down Expand Up @@ -1522,7 +1524,7 @@ def _update_ctx_header(self, attrs: DataFrame, axis: AxisInt) -> None:

Parameters
----------
attrs : Series
attrs : DataFrame
Should contain strings of '<property>: <value>;<prop2>: <val2>', and an
integer index.
Whitespace shouldn't matter and the final trailing ';' shouldn't
Expand All @@ -1541,6 +1543,31 @@ def _update_ctx_header(self, attrs: DataFrame, axis: AxisInt) -> None:
else:
self.ctx_columns[(j, i)].extend(css_list)

def _update_ctx_header_names(self, attrs: Series, axis: AxisInt) -> None:
"""
Update the state of the ``Styler`` for header level name cells.

Collects a mapping of {index_label: [('<property>', '<value>'), ..]}.

Parameters
----------
attrs : Series
Should contain strings of '<property>: <value>;<prop2>: <val2>', and an
integer index.
Whitespace shouldn't matter and the final trailing ';' shouldn't
matter.
axis : int
Identifies whether the ctx object being updated is the index or columns
"""
for i, c in attrs.items(): # type: int, str
if not c:
continue
css_list = maybe_convert_css_to_tuples(c)
if axis == 0:
self.ctx_index_names[(0, i)].extend(css_list)
else:
self.ctx_col_names[(i, 0)].extend(css_list)

def _copy(self, deepcopy: bool = False) -> Styler:
"""
Copies a Styler, allowing for deepcopy or shallow copy
Expand Down Expand Up @@ -1595,6 +1622,8 @@ def _copy(self, deepcopy: bool = False) -> Styler:
"ctx",
"ctx_index",
"ctx_columns",
"ctx_index_names",
"ctx_col_names",
"cell_context",
"_todo",
"table_styles",
Expand Down Expand Up @@ -1788,21 +1817,34 @@ def _apply_index(
func: Callable,
axis: Axis = 0,
level: Level | list[Level] | None = None,
names: bool = False,
method: str = "apply",
**kwargs,
) -> Styler:
axis = self.data._get_axis_number(axis)
obj = self.index if axis == 0 else self.columns

levels_ = refactor_levels(level, obj)
data = DataFrame(obj.to_list()).loc[:, levels_]
if names:
level_names = obj.names
data = Series(level_names).loc[levels_]

if method == "apply":
result = data.apply(func, axis=0, **kwargs)
elif method == "applymap":
result = data.applymap(func, **kwargs)
if method == "apply":
result = data.pipe(func, **kwargs)
elif method == "applymap":
result = data.apply(func, **kwargs)

self._update_ctx_header_names(result, axis)
else:
data = DataFrame(obj.to_list()).loc[:, levels_]

if method == "apply":
result = data.apply(func, axis=0, **kwargs)
elif method == "applymap":
result = data.applymap(func, **kwargs)

self._update_ctx_header(result, axis)

self._update_ctx_header(result, axis)
return self

@doc(
Expand All @@ -1822,6 +1864,7 @@ def apply_index(
func: Callable,
axis: AxisInt | str = 0,
level: Level | list[Level] | None = None,
names: bool = False,
**kwargs,
) -> Styler:
"""
Expand All @@ -1839,6 +1882,11 @@ def apply_index(
The headers over which to apply the function.
level : int, str, list, optional
If index is MultiIndex the level(s) over which to apply the function.
names : bool, optional
Whether to apply the styles to the index/column level names (if True) or
to the index/column values (if False, default).

.. versionadded:: 2.0.0
**kwargs : dict
Pass along to ``func``.

Expand Down Expand Up @@ -1879,11 +1927,30 @@ def apply_index(
... # doctest: +SKIP

.. figure:: ../../_static/style/appmaphead2.png

Applying styles to specific axis level names using a list

>>> midx = pd.MultiIndex.from_product(
... [['ix', 'jy'], [0, 1], ['x3', 'z4']],
... names=["l0", "l1", "l2"]
... )
>>> df = pd.DataFrame([np.arange(8)], columns=midx)
>>> df.style.{this}_index(lambda x: [
... "background-color: yellow",
... "",
... "background-color: red",
... ],
... axis="columns",
... names=True
... )
... # doctest: +SKIP

.. figure:: ../../_static/style/appmaphead3.png
"""
self._todo.append(
(
lambda instance: getattr(instance, "_apply_index"),
(func, axis, level, "apply"),
(func, axis, level, names, "apply"),
kwargs,
)
)
Expand All @@ -1907,12 +1974,13 @@ def applymap_index(
func: Callable,
axis: AxisInt | str = 0,
level: Level | list[Level] | None = None,
names: bool = False,
**kwargs,
) -> Styler:
self._todo.append(
(
lambda instance: getattr(instance, "_apply_index"),
(func, axis, level, "applymap"),
(func, axis, level, names, "applymap"),
kwargs,
)
)
Expand Down Expand Up @@ -2397,6 +2465,7 @@ def set_table_styles(
css_class_names = {"row_heading": "row_heading",
"col_heading": "col_heading",
"index_name": "index_name",
"col_name": "col_name",
"col": "col",
"row": "row",
"col_trim": "col_trim",
Expand Down
60 changes: 43 additions & 17 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class CSSDict(TypedDict):

CSSStyles = List[CSSDict]
Subset = Union[slice, Sequence, Index]
# Maps styles to list of cell element id's that have that style
CellMapStyles = DefaultDict[Tuple[CSSPair, ...], List[str]]
# Maps cell positions to CSS styles
CSSCellMap = DefaultDict[Tuple[int, int], CSSList]


class StylerRenderer:
Expand Down Expand Up @@ -110,6 +114,7 @@ def __init__(
"row_heading": "row_heading",
"col_heading": "col_heading",
"index_name": "index_name",
"col_name": "col_name",
"col": "col",
"row": "row",
"col_trim": "col_trim",
Expand All @@ -127,9 +132,11 @@ def __init__(
self.hide_columns_: list = [False] * self.columns.nlevels
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
self.hidden_columns: Sequence[int] = []
self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
self.ctx: CSSCellMap = defaultdict(list)
self.ctx_index: CSSCellMap = defaultdict(list)
self.ctx_columns: CSSCellMap = defaultdict(list)
self.ctx_index_names: CSSCellMap = defaultdict(list)
self.ctx_col_names: CSSCellMap = defaultdict(list)
self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str)
self._todo: list[tuple[Callable, tuple, dict]] = []
self.tooltips: Tooltips | None = None
Expand Down Expand Up @@ -307,9 +314,9 @@ def _translate(
max_cols,
)

self.cellstyle_map_columns: DefaultDict[
tuple[CSSPair, ...], list[str]
] = defaultdict(list)
self.cellstyle_map_columns: CellMapStyles = defaultdict(list)
self.cellstyle_map_col_names: CellMapStyles = defaultdict(list)
self.cellstyle_map_index_names: CellMapStyles = defaultdict(list)
head = self._translate_header(sparse_cols, max_cols)
d.update({"head": head})

Expand All @@ -319,19 +326,17 @@ def _translate(
)
d.update({"index_lengths": idx_lengths})

self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
list
)
self.cellstyle_map_index: DefaultDict[
tuple[CSSPair, ...], list[str]
] = defaultdict(list)
self.cellstyle_map: CellMapStyles = defaultdict(list)
self.cellstyle_map_index: CellMapStyles = defaultdict(list)
body: list = self._translate_body(idx_lengths, max_rows, max_cols)
d.update({"body": body})

ctx_maps = {
"cellstyle": "cellstyle_map",
"cellstyle_index": "cellstyle_map_index",
"cellstyle_columns": "cellstyle_map_columns",
"cellstyle_index_names": "cellstyle_map_index_names",
"cellstyle_col_names": "cellstyle_map_col_names",
} # add the cell_ids styles map to the render dictionary in right format
for k, attr in ctx_maps.items():
map = [
Expand Down Expand Up @@ -455,7 +460,7 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict
(
f"{self.css['blank']} {self.css['level']}{r}"
if name is None
else f"{self.css['index_name']} {self.css['level']}{r}"
else f"{self.css['col_name']} {self.css['level']}{r}"
),
name
if (name is not None and not self.hide_column_names)
Expand All @@ -464,6 +469,16 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict
)
]
Copy link
Contributor Author

@tehunter tehunter Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 column_name = [ _element( "th", ( f"{self.css['blank']} {self.css['level']}{r}" if name is None else f"{self.css['columns_name']} {self.css['level']}{r}" ), name if (name is not None and not self.hide_column_names) else self.css["blank_value"], not all(self.hide_index_), ) ]

Is this a breaking change? For column level names, it removes the index_name class and adds in the columns_names class in it's place. If users were purposefully doing styles with .index_name selectors with the intention of applying those to column level names, it will break that behavior.

Copy link
Contributor

@attack68 attack68 Oct 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it will break the example in the user guide because that styles class index_name.

However, this should be an easy fix:

index_names = { 'selector': '.index_name, .col_name', 'props': 'font-style: italic; color: darkgrey; font-weight:normal;' } 

As long as this break is documented I think including the additional functionality is worth it in long run.


if (
not all(self.hide_index_)
and (r, 0) in self.ctx_col_names
and self.ctx_col_names[r, 0]
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this following the same pattern as above?

column_name[0]["id"] = f"{self.css['col_name']}_{self.css['level']}{r}"
self.cellstyle_map_col_names[
tuple(self.ctx_col_names[r, 0])
].append(f"{self.css['col_name']}_{self.css['level']}{r}")

column_headers: list = []
visible_col_count: int = 0
for c, value in enumerate(clabels[r]):
Expand Down Expand Up @@ -534,15 +549,26 @@ def _generate_index_names_row(self, iter: tuple, max_cols: int, col_lengths: dic

clabels = iter

index_names = [
_element(
index_names = []
for c, name in enumerate(self.data.index.names):
index_name_element = _element(
"th",
f"{self.css['index_name']} {self.css['level']}{c}",
self.css["blank_value"] if name is None else name,
not self.hide_index_[c],
)
for c, name in enumerate(self.data.index.names)
]

if (
not self.hide_index_[c]
and (0, c) in self.ctx_index_names
and self.ctx_index_names[0, c]
):
element_id = f"{self.css['index_name']}_{self.css['level']}{c}"
index_name_element["id"] = element_id
self.cellstyle_map_index_names[
tuple(self.ctx_index_names[0, c])
].append(element_id)
index_names.append(index_name_element)

column_blanks: list = []
visible_col_count: int = 0
Expand Down
2 changes: 1 addition & 1 deletion pandas/io/formats/templates/html_style.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{% endblock table_styles %}
{% block before_cellstyle %}{% endblock before_cellstyle %}
{% block cellstyle %}
{% for cs in [cellstyle, cellstyle_index, cellstyle_columns] %}
{% for cs in [cellstyle, cellstyle_index, cellstyle_columns, cellstyle_index_names, cellstyle_col_names] %}
{% for s in cs %}
{% for selector in s.selectors %}{% if not loop.first %}, {% endif %}#T_{{uuid}}_{{selector}}{% endfor %} {
{% for p,val in s.props %}
Expand Down
Loading