From 777ce63333cd054586fc047ba28727166beafbab Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Mon, 18 Mar 2024 19:51:36 +0700 Subject: [PATCH 01/16] add new method to styler --- pandas/io/formats/style.py | 2 + pandas/io/formats/style_render.py | 155 +++++++++++++++++++- pandas/tests/io/formats/style/test_style.py | 2 + 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 7247e11be874e..ab5f1c039b7ca 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1683,6 +1683,8 @@ def _copy(self, deepcopy: bool = False) -> Styler: "_display_funcs", "_display_funcs_index", "_display_funcs_columns", + "_display_funcs_index_names", + "_display_funcs_column_names", "hidden_rows", "hidden_columns", "ctx", diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 2c93dbe74eace..c4174c949aac2 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -140,9 +140,15 @@ def __init__( self._display_funcs_index: DefaultDict[ # maps (row, level) -> format func tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) + self._display_funcs_index_names: DefaultDict[ # maps index level -> format func + tuple[int, int], Callable[[Any], str] + ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) self._display_funcs_columns: DefaultDict[ # maps (level, col) -> format func tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) + self._display_funcs_column_names: DefaultDict[ # maps col level -> format func + tuple[int, int], Callable[[Any], str] + ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) def _render( self, @@ -460,6 +466,12 @@ def _generate_col_header_row( ] * (self.index.nlevels - sum(self.hide_index_) - 1) name = self.data.columns.names[r] + + is_display = name is not None and not self.hide_column_names + value = name if is_display else self.css["blank_value"] + display_value = ( + self._display_funcs_column_names[r](value) if is_display else None + ) column_name = [ _element( "th", @@ -468,10 +480,9 @@ def _generate_col_header_row( if name is None else f"{self.css['index_name']} {self.css['level']}{r}" ), - name - if (name is not None and not self.hide_column_names) - else self.css["blank_value"], + value, not all(self.hide_index_), + display_value=display_value, ) ] @@ -553,6 +564,9 @@ def _generate_index_names_row( f"{self.css['index_name']} {self.css['level']}{c}", self.css["blank_value"] if name is None else name, not self.hide_index_[c], + display_value=( + None if name is None else self._display_funcs_index_names[c](name) + ), ) for c, name in enumerate(self.data.index.names) ] @@ -1560,6 +1574,139 @@ def alias_(x, value): return self + def format_names( + self, + formatter: ExtFormatter | None = None, + axis: Axis = 0, + level: Level | list[Level] | None = None, + na_rep: str | None = None, + precision: int | None = None, + decimal: str = ".", + thousands: str | None = None, + escape: str | None = None, + hyperlinks: str | None = None, + ) -> StylerRenderer: + r""" + Format the text display value of index names or column names. + + .. versionadded:: TODO + + Parameters + ---------- + formatter : str, callable, dict or None + Object to define how values are displayed. See notes. + axis : {0, "index", 1, "columns"} + Whether to apply the formatter to the index or column headers. + level : int, str, list + The level(s) over which to apply the generic formatter. + na_rep : str, optional + Representation for missing values. + If ``na_rep`` is None, no special formatting is applied. + precision : int, optional + Floating point precision to use for display purposes, if not determined by + the specified ``formatter``. + decimal : str, default "." + Character used as decimal separator for floats, complex and integers. + thousands : str, optional, default None + Character used as thousands separator for floats, complex and integers. + escape : str, optional + Use 'html' to replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` + in cell display string with HTML-safe sequences. + Use 'latex' to replace the characters ``&``, ``%``, ``$``, ``#``, ``_``, + ``{``, ``}``, ``~``, ``^``, and ``\`` in the cell display string with + LaTeX-safe sequences. + Escaping is done before ``formatter``. + hyperlinks : {"html", "latex"}, optional + Convert string patterns containing https://, http://, ftp:// or www. to + HTML tags as clickable URL hyperlinks if "html", or LaTeX \href + commands if "latex". + + Returns + ------- + Styler + + See Also + -------- + Styler.format_index: Format the text display value of index labels + or column headers. + + Notes + ----- + This method assigns a formatting function, ``formatter``, to each level label + in the DataFrame's index or column headers. If ``formatter`` is ``None``, + then the default formatter is used. + If a callable then that function should take a label value as input and return + a displayable representation, such as a string. If ``formatter`` is + given as a string this is assumed to be a valid Python format specification + and is wrapped to a callable as ``string.format(x)``. If a ``dict`` is given, + keys should correspond to MultiIndex level numbers or names, and values should + be string or callable, as above. + + The default formatter currently expresses floats and complex numbers with the + pandas display precision unless using the ``precision`` argument here. The + default formatter does not adjust the representation of missing values unless + the ``na_rep`` argument is used. + + The ``level`` argument defines which levels of a MultiIndex to apply the + method to. If the ``formatter`` argument is given in dict form but does + not include all levels within the level argument then these unspecified levels + will have the default formatter applied. Any levels in the formatter dict + specifically excluded from the level argument will be ignored. + + When using a ``formatter`` string the dtypes must be compatible, otherwise a + `ValueError` will be raised. + + .. warning:: + `Styler.format_index` is ignored when using the output format + `Styler.to_excel`, since Excel and Python have inherrently different + formatting structures. + However, it is possible to use the `number-format` pseudo CSS attribute + to force Excel permissible formatting. See documentation for `Styler.format`. + """ + axis = self.data._get_axis_number(axis) + if axis == 0: + display_funcs_, obj = self._display_funcs_index_names, self.index + else: + display_funcs_, obj = self._display_funcs_column_names, self.columns + levels_ = refactor_levels(level, obj) + + if all( + ( + formatter is None, + level is None, + precision is None, + decimal == ".", + thousands is None, + na_rep is None, + escape is None, + hyperlinks is None, + ) + ): + display_funcs_.clear() + return self # clear the formatter / revert to default and avoid looping + + if not isinstance(formatter, dict): + formatter = {level: formatter for level in levels_} + else: + formatter = { + obj._get_level_number(level): formatter_ + for level, formatter_ in formatter.items() + } + + for lvl in levels_: + format_func = _maybe_wrap_formatter( + formatter.get(lvl), + na_rep=na_rep, + precision=precision, + decimal=decimal, + thousands=thousands, + escape=escape, + hyperlinks=hyperlinks, + ) + display_funcs_[lvl] = format_func + + return self + def _element( html_element: str, @@ -1571,7 +1718,7 @@ def _element( """ Template to return container with information for a or element. """ - if "display_value" not in kwargs: + if "display_value" not in kwargs or kwargs["display_value"] is None: kwargs["display_value"] = value return { "type": html_element, diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 6fa72bd48031c..e90d4b6e7da18 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -77,6 +77,8 @@ def mi_styler_comp(mi_styler): columns=mi_styler.columns, ) ) + mi_styler.format_names(escape="html", axis=0) + mi_styler.format_names(escape="html", axis=1) return mi_styler From 3cbe658e782d823915459ddd98a4a504f6123006 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 19 Mar 2024 10:29:21 +0700 Subject: [PATCH 02/16] add html test --- pandas/tests/io/formats/style/test_html.py | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 8cb06e3b7619d..1554a8042f9d2 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -9,6 +9,7 @@ from pandas import ( DataFrame, MultiIndex, + Series, option_context, ) @@ -1003,3 +1004,55 @@ def test_to_html_na_rep_non_scalar_data(datapath): """ assert result == expected + + +@pytest.mark.parametrize("escape_axis_0", [True, False]) +@pytest.mark.parametrize("escape_axis_1", [True, False]) +def test_format_names(escape_axis_0, escape_axis_1): + index = Series(["a", "b"], name=">c_name") + columns = Series(["A"], name="col_name>") + df = DataFrame([[2.61], [2.69]], index=index, columns=columns) + styler = Styler(df) + + if escape_axis_0: + styler.format_names(axis=0, escape="html") + expected_index = ">c_name" + else: + expected_index = ">c_name" + + if escape_axis_1: + styler.format_names(axis=1, escape="html") + expected_columns = "col_name>" + else: + expected_columns = "col_name>" + + result = styler.to_html(table_uuid="test") + expected = dedent( + f"""\ + + + + + + + + + + + + + + + + + + + + + + +
{expected_columns}A
{expected_index} 
a2.610000
b2.690000
+ """ + ) + assert result == expected From d214e02978940fa8c5857df4a5112ac8a08558f0 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 19 Mar 2024 11:00:37 +0700 Subject: [PATCH 03/16] fix type --- pandas/io/formats/style_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index c4174c949aac2..63d24250ddf1b 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -141,13 +141,13 @@ def __init__( tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) self._display_funcs_index_names: DefaultDict[ # maps index level -> format func - tuple[int, int], Callable[[Any], str] + int, Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) self._display_funcs_columns: DefaultDict[ # maps (level, col) -> format func tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) self._display_funcs_column_names: DefaultDict[ # maps col level -> format func - tuple[int, int], Callable[[Any], str] + int, Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) def _render( From 1e3661fd327d1eda7444974c290d309f9ded407f Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 19 Mar 2024 15:47:55 +0700 Subject: [PATCH 04/16] rename to format_index_names --- pandas/io/formats/style_render.py | 2 +- pandas/tests/io/formats/style/test_html.py | 6 +++--- pandas/tests/io/formats/style/test_style.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 63d24250ddf1b..164c5de9f04b6 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1574,7 +1574,7 @@ def alias_(x, value): return self - def format_names( + def format_index_names( self, formatter: ExtFormatter | None = None, axis: Axis = 0, diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 1554a8042f9d2..4eff2f5e5519e 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -1008,20 +1008,20 @@ def test_to_html_na_rep_non_scalar_data(datapath): @pytest.mark.parametrize("escape_axis_0", [True, False]) @pytest.mark.parametrize("escape_axis_1", [True, False]) -def test_format_names(escape_axis_0, escape_axis_1): +def test_format_index_names(escape_axis_0, escape_axis_1): index = Series(["a", "b"], name=">c_name") columns = Series(["A"], name="col_name>") df = DataFrame([[2.61], [2.69]], index=index, columns=columns) styler = Styler(df) if escape_axis_0: - styler.format_names(axis=0, escape="html") + styler.format_index_names(axis=0, escape="html") expected_index = ">c_name" else: expected_index = ">c_name" if escape_axis_1: - styler.format_names(axis=1, escape="html") + styler.format_index_names(axis=1, escape="html") expected_columns = "col_name>" else: expected_columns = "col_name>" diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index e90d4b6e7da18..89addbbbc1ded 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -77,8 +77,8 @@ def mi_styler_comp(mi_styler): columns=mi_styler.columns, ) ) - mi_styler.format_names(escape="html", axis=0) - mi_styler.format_names(escape="html", axis=1) + mi_styler.format_index_names(escape="html", axis=0) + mi_styler.format_index_names(escape="html", axis=1) return mi_styler From 9241a1b1cbe7dcfdde8fce41b19ae1a89501fe1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Nguy=E1=BB=85n?= <30631476+quangngd@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:22:28 +0700 Subject: [PATCH 05/16] Update pandas/io/formats/style_render.py Co-authored-by: JHM Darbyshire <24256554+attack68@users.noreply.github.com> --- pandas/io/formats/style_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 63d24250ddf1b..1ae92f57050e7 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1657,11 +1657,9 @@ def format_names( `ValueError` will be raised. .. warning:: - `Styler.format_index` is ignored when using the output format + `Styler.format_index_names` is ignored when using the output format `Styler.to_excel`, since Excel and Python have inherrently different formatting structures. - However, it is possible to use the `number-format` pseudo CSS attribute - to force Excel permissible formatting. See documentation for `Styler.format`. """ axis = self.data._get_axis_number(axis) if axis == 0: From d96eac4c722c2220776cf4eec83d2fa6b1386a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Nguy=E1=BB=85n?= <30631476+quangngd@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:22:41 +0700 Subject: [PATCH 06/16] Update pandas/io/formats/style_render.py Co-authored-by: JHM Darbyshire <24256554+attack68@users.noreply.github.com> --- pandas/io/formats/style_render.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 1ae92f57050e7..ba4dd7d105602 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1632,26 +1632,9 @@ def format_names( Notes ----- - This method assigns a formatting function, ``formatter``, to each level label - in the DataFrame's index or column headers. If ``formatter`` is ``None``, - then the default formatter is used. - If a callable then that function should take a label value as input and return - a displayable representation, such as a string. If ``formatter`` is - given as a string this is assumed to be a valid Python format specification - and is wrapped to a callable as ``string.format(x)``. If a ``dict`` is given, - keys should correspond to MultiIndex level numbers or names, and values should - be string or callable, as above. - - The default formatter currently expresses floats and complex numbers with the - pandas display precision unless using the ``precision`` argument here. The - default formatter does not adjust the representation of missing values unless - the ``na_rep`` argument is used. - - The ``level`` argument defines which levels of a MultiIndex to apply the - method to. If the ``formatter`` argument is given in dict form but does - not include all levels within the level argument then these unspecified levels - will have the default formatter applied. Any levels in the formatter dict - specifically excluded from the level argument will be ignored. + This method has a similar signature to :meth:`Styler.format_index`. Since `names` are generally label + based, and often not numeric, the typical features expected to be more frequently used here are + ``escape`` and ``hyperlinks``. When using a ``formatter`` string the dtypes must be compatible, otherwise a `ValueError` will be raised. From ec2d2b3cb029870737e777a548160a65006f4e25 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 19 Mar 2024 23:20:26 +0700 Subject: [PATCH 07/16] add tests --- pandas/tests/io/formats/style/test_format.py | 87 ++++++++++++- pandas/tests/io/formats/style/test_html.py | 122 ++++++++++++------- 2 files changed, 163 insertions(+), 46 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 1c84816ead140..5d019813c9d04 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -32,10 +32,14 @@ def styler(df): @pytest.fixture def df_multi(): - return DataFrame( - data=np.arange(16).reshape(4, 4), - columns=MultiIndex.from_product([["A", "B"], ["a", "b"]]), - index=MultiIndex.from_product([["X", "Y"], ["x", "y"]]), + return ( + DataFrame( + data=np.arange(16).reshape(4, 4), + columns=MultiIndex.from_product([["A", "B"], ["a", "b"]]), + index=MultiIndex.from_product([["X", "Y"], ["x", "y"]]), + ) + .rename_axis(["0_0", "0_1"], axis=0) + .rename_axis(["1_0", "1_1"], axis=1) ) @@ -560,3 +564,78 @@ def test_relabel_roundtrip(styler): ctx = styler._translate(True, True) assert {"value": "x", "display_value": "x"}.items() <= ctx["body"][0][0].items() assert {"value": "y", "display_value": "y"}.items() <= ctx["body"][1][0].items() + + +@pytest.mark.parametrize("axis", [0, 1]) +@pytest.mark.parametrize( + "level, expected", + [ + (0, ["X", "one"]), # level int + ("zero", ["X", "one"]), # level name + (1, ["zero", "X"]), # other level int + ("one", ["zero", "X"]), # other level name + ([0, 1], ["X", "X"]), # both levels + ([0, "zero"], ["X", "one"]), # level int and name simultaneous + ([0, "one"], ["X", "X"]), # both levels as int and name + (["one", "zero"], ["X", "X"]), # both level names, reversed + ], +) +def test_format_index_names_level(axis, level, expected): + midx = MultiIndex.from_arrays([["_", "_"], ["_", "_"]], names=["zero", "one"]) + df = DataFrame([[1, 2], [3, 4]]) + if axis == 0: + df.index = midx + else: + df.columns = midx + + styler = df.style.format_index_names(lambda v: "X", level=level, axis=axis) + ctx = styler._translate(True, True) + + if axis == 0: # compare index + result = [ctx["head"][1][s]["display_value"] for s in range(2)] + else: # compare columns + result = [ctx["head"][s][0]["display_value"] for s in range(2)] + assert expected == result + + +@pytest.mark.parametrize( + "attr, kwargs", + [ + ("_display_funcs_index_names", {"axis": 0}), + ("_display_funcs_column_names", {"axis": 1}), + ], +) +def test_format_index_names_clear(styler, attr, kwargs): + assert 0 not in getattr(styler, attr) # using default + styler.format_index_names("{:.2f}", **kwargs) + assert 0 in getattr(styler, attr) # formatter is specified + styler.format_index_names(**kwargs) + assert 0 not in getattr(styler, attr) # formatter cleared to default + + +@pytest.mark.parametrize("axis", [0, 1]) +def test_format_index_names_callable(styler_multi, axis): + ctx = styler_multi.format_index_names( + lambda v: v.replace("_", "A"), axis=axis + )._translate(True, True) + result = [ + ctx["head"][2][0]["display_value"], + ctx["head"][2][1]["display_value"], + ctx["head"][0][1]["display_value"], + ctx["head"][1][1]["display_value"], + ] + if axis == 0: + expected = ["0A0", "0A1", "1_0", "1_1"] + else: + expected = ["0_0", "0_1", "1A0", "1A1"] + assert result == expected + + +def test_format_index_names_dict(styler_multi): + ctx = ( + styler_multi.format_index_names({"0_0": "{:<<5}"}) + .format_index_names({"1_1": "{:>>4}"}, axis=1) + ._translate(True, True) + ) + assert ctx["head"][2][0]["display_value"] == "0_0<<" + assert ctx["head"][1][1]["display_value"] == ">1_1" diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 4eff2f5e5519e..1f9bff3b0b200 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -9,7 +9,6 @@ from pandas import ( DataFrame, MultiIndex, - Series, option_context, ) @@ -35,6 +34,16 @@ def styler_mi(): return Styler(DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=midx)) +@pytest.fixture +def styler_multi(): + df = DataFrame( + data=np.arange(16).reshape(4, 4), + columns=MultiIndex.from_product([["A", "B"], ["a", "b"]], names=["A&", "b&"]), + index=MultiIndex.from_product([["X", "Y"], ["x", "y"]], names=["X>", "y_"]), + ) + return Styler(df) + + @pytest.fixture def tpl_style(env): return env.get_template("html_style.tpl") @@ -1008,51 +1017,80 @@ def test_to_html_na_rep_non_scalar_data(datapath): @pytest.mark.parametrize("escape_axis_0", [True, False]) @pytest.mark.parametrize("escape_axis_1", [True, False]) -def test_format_index_names(escape_axis_0, escape_axis_1): - index = Series(["a", "b"], name=">c_name") - columns = Series(["A"], name="col_name>") - df = DataFrame([[2.61], [2.69]], index=index, columns=columns) - styler = Styler(df) - +def test_format_index_names(styler_multi, escape_axis_0, escape_axis_1): if escape_axis_0: - styler.format_index_names(axis=0, escape="html") - expected_index = ">c_name" + styler_multi.format_index_names(axis=0, escape="html") + expected_index = ["X>", "y_"] else: - expected_index = ">c_name" + expected_index = ["X>", "y_"] if escape_axis_1: - styler.format_index_names(axis=1, escape="html") - expected_columns = "col_name>" + styler_multi.format_index_names(axis=1, escape="html") + expected_columns = ["A&", "b&"] else: - expected_columns = "col_name>" + expected_columns = ["A&", "b&"] - result = styler.to_html(table_uuid="test") - expected = dedent( - f"""\ - - - - - - - - - - - - - - - - - - - - - - -
{expected_columns}A
{expected_index} 
a2.610000
b2.690000
- """ - ) + result = styler_multi.to_html(table_uuid="test") + expected = f"""\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 {expected_columns[0]}AB
 {expected_columns[1]}abab
{expected_index[0]}{expected_index[1]}    
Xx0123
y4567
Yx891011
y12131415
+""" assert result == expected From 87ffa4d8f6a65c154122d2b107e7330f49706d14 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 19 Mar 2024 23:40:46 +0700 Subject: [PATCH 08/16] add test --- pandas/tests/io/formats/style/test_format.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 5d019813c9d04..ae68fcf9ef1fc 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -639,3 +639,23 @@ def test_format_index_names_dict(styler_multi): ) assert ctx["head"][2][0]["display_value"] == "0_0<<" assert ctx["head"][1][1]["display_value"] == ">1_1" + + +def test_format_index_names_with_hidden_levels(styler_multi): + ctx = styler_multi._translate(True, True) + full_head_height = len(ctx["head"]) + full_head_width = len(ctx["head"][0]) + assert full_head_height == 3 + assert full_head_width == 6 + + ctx = ( + styler_multi.hide(axis=0, level=1) + .hide(axis=1, level=1) + .format_index_names("{:>>4}", axis=1) + .format_index_names("{:!<5}") + ._translate(True, True) + ) + assert len(ctx["head"]) == full_head_height - 1 + assert len(ctx["head"][0]) == full_head_width - 1 + assert ctx["head"][0][0]["display_value"] == ">1_0" + assert ctx["head"][1][0]["display_value"] == "0_0!!" From 783269e88b00085a46383dfeb580787aa919e726 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Wed, 20 Mar 2024 09:55:52 +0700 Subject: [PATCH 09/16] more doc --- doc/source/reference/style.rst | 1 + pandas/io/formats/style_render.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 2256876c93e01..0e1d93841d52f 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -41,6 +41,7 @@ Style application Styler.map_index Styler.format Styler.format_index + Styler.format_index_names Styler.relabel_index Styler.hide Styler.concat diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index b26dc0d12b17b..68b808c604d9e 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1632,17 +1632,17 @@ def format_index_names( Notes ----- - This method has a similar signature to :meth:`Styler.format_index`. Since `names` are generally label - based, and often not numeric, the typical features expected to be more frequently used here are - ``escape`` and ``hyperlinks``. + This method has a similar signature to :meth:`Styler.format_index`. Since + `names` are generally label based, and often not numeric, the typical features + expected to be more frequently used here are ``escape`` and ``hyperlinks``. When using a ``formatter`` string the dtypes must be compatible, otherwise a `ValueError` will be raised. .. warning:: - `Styler.format_index_names` is ignored when using the output format - `Styler.to_excel`, since Excel and Python have inherrently different - formatting structures. + `Styler.format_index_names` is ignored when using the output format + `Styler.to_excel`, since Excel and Python have inherrently different + formatting structures. """ axis = self.data._get_axis_number(axis) if axis == 0: From a644ac1553dc05471ef0c959797d16baf49d01a0 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Wed, 20 Mar 2024 11:01:35 +0700 Subject: [PATCH 10/16] doc --- ci/code_checks.sh | 3 --- pandas/io/formats/style_render.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 4b8e632f3246c..09f24d67b0ad5 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -814,8 +814,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then --ignore_errors pandas.io.formats.style.Styler.clear SA01\ --ignore_errors pandas.io.formats.style.Styler.concat RT03,SA01\ --ignore_errors pandas.io.formats.style.Styler.export RT03\ - --ignore_errors pandas.io.formats.style.Styler.format RT03\ - --ignore_errors pandas.io.formats.style.Styler.format_index RT03\ --ignore_errors pandas.io.formats.style.Styler.from_custom_template SA01\ --ignore_errors pandas.io.formats.style.Styler.hide RT03,SA01\ --ignore_errors pandas.io.formats.style.Styler.highlight_between RT03\ @@ -825,7 +823,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then --ignore_errors pandas.io.formats.style.Styler.highlight_quantile RT03\ --ignore_errors pandas.io.formats.style.Styler.map RT03\ --ignore_errors pandas.io.formats.style.Styler.map_index RT03\ - --ignore_errors pandas.io.formats.style.Styler.relabel_index RT03\ --ignore_errors pandas.io.formats.style.Styler.set_caption RT03,SA01\ --ignore_errors pandas.io.formats.style.Styler.set_properties RT03,SA01\ --ignore_errors pandas.io.formats.style.Styler.set_sticky RT03,SA01\ diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 68b808c604d9e..8d83e0d7ec529 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1019,6 +1019,7 @@ def format( Returns ------- Styler + Returns itself for chaining. See Also -------- @@ -1275,6 +1276,7 @@ def format_index( Returns ------- Styler + Returns itself for chaining. See Also -------- @@ -1439,6 +1441,7 @@ def relabel_index( Returns ------- Styler + Returns itself for chaining. See Also -------- @@ -1624,6 +1627,7 @@ def format_index_names( Returns ------- Styler + Returns itself for chaining. See Also -------- From 7d396cc17f682c756e98ef0c5a5329bdac512d8e Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Wed, 20 Mar 2024 11:09:40 +0700 Subject: [PATCH 11/16] update code_checks --- ci/code_checks.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index a9967dcb8efe6..63e5d20160dd2 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -797,8 +797,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.io.formats.style.Styler.clear SA01" \ -i "pandas.io.formats.style.Styler.concat RT03,SA01" \ -i "pandas.io.formats.style.Styler.export RT03" \ - -i "pandas.io.formats.style.Styler.format RT03" \ - -i "pandas.io.formats.style.Styler.format_index RT03" \ -i "pandas.io.formats.style.Styler.from_custom_template SA01" \ -i "pandas.io.formats.style.Styler.hide RT03,SA01" \ -i "pandas.io.formats.style.Styler.highlight_between RT03" \ @@ -808,7 +806,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.io.formats.style.Styler.highlight_quantile RT03" \ -i "pandas.io.formats.style.Styler.map RT03" \ -i "pandas.io.formats.style.Styler.map_index RT03" \ - -i "pandas.io.formats.style.Styler.relabel_index RT03" \ -i "pandas.io.formats.style.Styler.set_caption RT03,SA01" \ -i "pandas.io.formats.style.Styler.set_properties RT03,SA01" \ -i "pandas.io.formats.style.Styler.set_sticky RT03,SA01" \ From bcfb5f4a161ec02064ba69335bf3aeea4d6c458f Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Wed, 20 Mar 2024 21:00:02 +0700 Subject: [PATCH 12/16] add example --- pandas/io/formats/style_render.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8d83e0d7ec529..8d33dc84c562c 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1647,6 +1647,25 @@ def format_index_names( `Styler.format_index_names` is ignored when using the output format `Styler.to_excel`, since Excel and Python have inherrently different formatting structures. + + Examples + -------- + Basic use + + >>> df = pd.DataFrame( + ... [[1, 2], [3, 4]], + ... index=pd.Index(["a", "b"], name="idx"), + ... ) + >>> df # doctest: +SKIP + 0 1 + idx + a 1 2 + b 3 4 + >>> df.style.format_index_names(lambda x: x.upper(), axis=0) # doctest: +SKIP + 0 1 + IDX + a 1 2 + b 3 4 """ axis = self.data._get_axis_number(axis) if axis == 0: From 1396cfdda437d5059832ea2d2fc4ad35db940505 Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Thu, 21 Mar 2024 15:23:25 +0700 Subject: [PATCH 13/16] update test --- pandas/tests/io/formats/style/test_html.py | 65 +--------------------- 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 1f9bff3b0b200..2306324efb974 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -1031,66 +1031,5 @@ def test_format_index_names(styler_multi, escape_axis_0, escape_axis_1): expected_columns = ["A&", "b&"] result = styler_multi.to_html(table_uuid="test") - expected = f"""\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 {expected_columns[0]}AB
 {expected_columns[1]}abab
{expected_index[0]}{expected_index[1]}    
Xx0123
y4567
Yx891011
y12131415
-""" - assert result == expected + for expected_str in expected_index + expected_columns: + assert f"{expected_str}" in result From f4806f759f48cdbc2539de617f58ac4e50564158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Nguy=E1=BB=85n?= <30631476+quangngd@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:58:31 +0700 Subject: [PATCH 14/16] Update pandas/io/formats/style_render.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/io/formats/style_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8d33dc84c562c..9e0803c822fe5 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1592,7 +1592,7 @@ def format_index_names( r""" Format the text display value of index names or column names. - .. versionadded:: TODO + .. versionadded:: 3.0 Parameters ---------- From f7d3461e8485eca1c0e85a6ced75b974b8cc5a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Nguy=E1=BB=85n?= <30631476+quangngd@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:58:36 +0700 Subject: [PATCH 15/16] Update pandas/io/formats/style_render.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/io/formats/style_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 9e0803c822fe5..464697d32bda4 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1650,8 +1650,6 @@ def format_index_names( Examples -------- - Basic use - >>> df = pd.DataFrame( ... [[1, 2], [3, 4]], ... index=pd.Index(["a", "b"], name="idx"), From 60e6ef2781b88c9e4a2b97a337ae69efa58e32ac Mon Sep 17 00:00:00 2001 From: Quang Nguyen Date: Tue, 26 Mar 2024 10:07:08 +0700 Subject: [PATCH 16/16] update doc --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/io/formats/style_render.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index f3fcdcdb79ed6..92138c3a77b1e 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -34,6 +34,7 @@ Other enhancements - Allow dictionaries to be passed to :meth:`pandas.Series.str.replace` via ``pat`` parameter (:issue:`51748`) - Support passing a :class:`Series` input to :func:`json_normalize` that retains the :class:`Series` :class:`Index` (:issue:`51452`) - Users can globally disable any ``PerformanceWarning`` by setting the option ``mode.performance_warnings`` to ``False`` (:issue:`56920`) +- :meth:`Styler.format_index_names` can now be used to format the index and column names (:issue:`48936` and :issue:`47489`) - .. --------------------------------------------------------------------------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8d33dc84c562c..65ae4ece9bc53 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1629,6 +1629,11 @@ def format_index_names( Styler Returns itself for chaining. + Raises + ------ + ValueError + If the `formatter` is a string and the dtypes are incompatible. + See Also -------- Styler.format_index: Format the text display value of index labels @@ -1640,9 +1645,6 @@ def format_index_names( `names` are generally label based, and often not numeric, the typical features expected to be more frequently used here are ``escape`` and ``hyperlinks``. - When using a ``formatter`` string the dtypes must be compatible, otherwise a - `ValueError` will be raised. - .. warning:: `Styler.format_index_names` is ignored when using the output format `Styler.to_excel`, since Excel and Python have inherrently different