From e03e01476a32a08f5e466572642abf5ab918a0e4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 16 Aug 2021 19:30:12 +0200 Subject: [PATCH 01/44] build format_index mechanics --- pandas/io/formats/style.py | 44 ++------- pandas/io/formats/style_render.py | 103 +++++++++++++++++++- pandas/io/formats/templates/html_table.tpl | 4 +- pandas/tests/io/formats/style/test_style.py | 2 + 4 files changed, 112 insertions(+), 41 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a72de753d6a8a..9481e2898c1e8 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -32,7 +32,6 @@ import pandas as pd from pandas import ( - Index, IndexSlice, RangeIndex, ) @@ -57,6 +56,7 @@ Tooltips, maybe_convert_css_to_tuples, non_reducing_slice, + refactor_levels, ) try: @@ -1074,6 +1074,8 @@ def _copy(self, deepcopy: bool = False) -> Styler: ] deep = [ # nested lists or dicts "_display_funcs", + "_display_funcs_index", + "_display_funcs_columns", "hidden_rows", "hidden_columns", "ctx", @@ -1262,7 +1264,7 @@ def _apply_index( f"`axis` must be one of 0, 1, 'index', 'columns', got {axis}" ) - levels_ = _refactor_levels(level, obj) + levels_ = refactor_levels(level, obj) data = DataFrame(obj.to_list()).loc[:, levels_] if method == "apply": @@ -2052,7 +2054,7 @@ def hide_index( raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: - levels_ = _refactor_levels(level, self.index) + levels_ = refactor_levels(level, self.index) self.hide_index_ = [ True if lev in levels_ else False for lev in range(self.index.nlevels) ] @@ -2164,7 +2166,7 @@ def hide_columns( raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: - levels_ = _refactor_levels(level, self.columns) + levels_ = refactor_levels(level, self.columns) self.hide_columns_ = [ True if lev in levels_ else False for lev in range(self.columns.nlevels) ] @@ -3358,37 +3360,3 @@ def css_calc(x, left: float, right: float, align: str): index=data.index, columns=data.columns, ) - - -def _refactor_levels( - level: Level | list[Level] | None, - obj: Index, -) -> list[Level]: - """ - Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``. - - Parameters - ---------- - level : int, str, list - Original ``level`` arg supplied to above methods. - obj: - Either ``self.index`` or ``self.columns`` - - Returns - ------- - list : refactored arg with a list of levels to hide - """ - if level is None: - levels_: list[Level] = list(range(obj.nlevels)) - elif isinstance(level, int): - levels_ = [level] - elif isinstance(level, str): - levels_ = [obj._get_level_number(level)] - elif isinstance(level, list): - levels_ = [ - obj._get_level_number(lev) if not isinstance(lev, int) else lev - for lev in level - ] - else: - raise ValueError("`level` must be of type `int`, `str` or list of such") - return levels_ diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index aa58b3abbd06c..b381e52ab7b82 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -22,6 +22,7 @@ from pandas._config import get_option from pandas._libs import lib +from pandas._typing import Level from pandas.compat._optional import import_optional_dependency from pandas.core.dtypes.generic import ABCSeries @@ -108,7 +109,13 @@ def __init__( self._todo: list[tuple[Callable, tuple, dict]] = [] self.tooltips: Tooltips | None = None def_precision = get_option("display.precision") - self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function + self._display_funcs: DefaultDict[ # maps (row, col) -> format func + tuple[int, int], Callable[[Any], str] + ] = defaultdict(lambda: partial(_default_formatter, precision=def_precision)) + self._display_funcs_index: DefaultDict[ # maps (row, level) -> format func + tuple[int, int], Callable[[Any], str] + ] = defaultdict(lambda: partial(_default_formatter, precision=def_precision)) + self._display_funcs_columns: DefaultDict[ # maps (level, col) -> format func tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=def_precision)) @@ -346,6 +353,7 @@ def _translate_header( f"{col_heading_class} level{r} col{c}", value, _is_visible(c, r, col_lengths), + display_value=self._display_funcs_columns[(r, c)](value), attributes=( f'colspan="{col_lengths.get((r, c), 0)}"' if col_lengths.get((r, c), 0) > 1 @@ -502,6 +510,7 @@ def _translate_body( f"{row_heading_class} level{c} row{r}", value, (_is_visible(r, c, idx_lengths) and not self.hide_index_[c]), + display_value=self._display_funcs_index[(r, c)](value), attributes=( f'rowspan="{idx_lengths.get((c, r), 0)}"' if idx_lengths.get((c, r), 0) > 1 @@ -791,6 +800,64 @@ def format( return self + def format_index( + self, + formatter: ExtFormatter | None = None, + axis: int | str = 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, + ) -> StylerRenderer: + r""" """ + if axis == 0: + display_funcs_, obj = self._display_funcs_index, self.index + elif axis == 1: + display_funcs_, obj = self._display_funcs_columns, self.columns + + levels_ = refactor_levels(level, obj) + + if all( + ( + formatter is None, + precision is None, + decimal == ".", + thousands is None, + na_rep is None, + escape 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 level in set(formatter.keys()).union(levels_): + format_func = _maybe_wrap_formatter( + formatter.get(level), + na_rep=na_rep, + precision=precision, + decimal=decimal, + thousands=thousands, + escape=escape, + ) + + for i in range(len(obj)): + if axis == 0: + display_funcs_[(i, level)] = format_func + else: + display_funcs_[(level, i)] = format_func + + return self + def _element( html_element: str, @@ -1113,6 +1180,40 @@ def maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: return style +def refactor_levels( + level: Level | list[Level] | None, + obj: Index, +) -> list[Level]: + """ + Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``. + + Parameters + ---------- + level : int, str, list + Original ``level`` arg supplied to above methods. + obj: + Either ``self.index`` or ``self.columns`` + + Returns + ------- + list : refactored arg with a list of levels to hide + """ + if level is None: + levels_: list[Level] = list(range(obj.nlevels)) + elif isinstance(level, int): + levels_ = [level] + elif isinstance(level, str): + levels_ = [obj._get_level_number(level)] + elif isinstance(level, list): + levels_ = [ + obj._get_level_number(lev) if not isinstance(lev, int) else lev + for lev in level + ] + else: + raise ValueError("`level` must be of type `int`, `str` or list of such") + return levels_ + + class Tooltips: """ An extension to ``Styler`` that allows for and manipulates tooltips on hover diff --git a/pandas/io/formats/templates/html_table.tpl b/pandas/io/formats/templates/html_table.tpl index 3e3a40b9fdaa6..8cf3ed00fc991 100644 --- a/pandas/io/formats/templates/html_table.tpl +++ b/pandas/io/formats/templates/html_table.tpl @@ -21,13 +21,13 @@ {% if exclude_styles %} {% for c in r %} {% if c.is_visible != False %} - <{{c.type}} {{c.attributes}}>{{c.value}} + <{{c.type}} {{c.attributes}}>{{c.display_value}} {% endif %} {% endfor %} {% else %} {% for c in r %} {% if c.is_visible != False %} - <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.value}} + <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} {% endif %} {% endfor %} {% endif %} diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 6cc4b889d369a..0276afe4fbc3e 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -51,6 +51,8 @@ def mi_styler_comp(mi_styler): mi_styler.hide_index([("i0", "i1_a")]) mi_styler.set_table_attributes('class="box"') mi_styler.format(na_rep="MISSING", precision=3) + mi_styler.format_index(precision=2, axis=0) + mi_styler.format_index(precision=4, axis=1) mi_styler.highlight_max(axis=None) mi_styler.applymap_index(lambda x: "color: white;", axis=0) mi_styler.applymap_index(lambda x: "color: black;", axis=1) From fb862791288c69e833ce0ba4465dd511c7ee9764 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 17 Aug 2021 18:02:19 +0200 Subject: [PATCH 02/44] test index formatter display_value, and clearing --- pandas/tests/io/formats/style/test_format.py | 42 +++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 299643028c141..eda0e2eeecd34 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -34,6 +34,28 @@ def test_display_format(styler): assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 +@pytest.mark.parametrize("index", [True, False]) +@pytest.mark.parametrize("columns", [True, False]) +def test_display_format_index(styler, index, columns): + exp_index = ["x", "y"] + if index: + styler.format_index(lambda v: v.upper(), axis=0) + exp_index = ["X", "Y"] + + exp_columns = ["A", "B"] + if columns: + styler.format_index(lambda v: v.lower(), axis=1) + exp_columns = ["a", "b"] + + ctx = styler._translate(True, True) + + for r, row in enumerate(ctx["body"]): + assert row[0]["display_value"] == exp_index[r] + + for c, col in enumerate(ctx["head"][1:]): + assert col["display_value"] == exp_columns[c] + + def test_format_dict(styler): ctx = styler.format({"A": "{:0.1f}", "B": "{0:.2%}"})._translate(True, True) assert ctx["body"][0][1]["display_value"] == "0.0" @@ -90,12 +112,20 @@ def test_format_non_numeric_na(): assert ctx["body"][1][2]["display_value"] == "-" -def test_format_clear(styler): - assert (0, 0) not in styler._display_funcs # using default - styler.format("{:.2f") - assert (0, 0) in styler._display_funcs # formatter is specified - styler.format() - assert (0, 0) not in styler._display_funcs # formatter cleared to default +@pytest.mark.parametrize( + "func, attr, kwargs", + [ + ("format", "_display_funcs", {}), + ("format_index", "_display_funcs_index", {"axis": 0}), + ("format_index", "_display_funcs_columns", {"axis": 1}), + ], +) +def test_format_clear(styler, func, attr, kwargs): + assert (0, 0) not in getattr(styler, attr) # using default + getattr(styler, func)("{:.2f}", **kwargs) + assert (0, 0) in getattr(styler, attr) # formatter is specified + getattr(styler, func)(**kwargs) + assert (0, 0) not in getattr(styler, attr) # formatter cleared to default @pytest.mark.parametrize( From 846e5a21e885f6bfb5a304a461d475338d0f3a35 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 17 Aug 2021 19:16:52 +0200 Subject: [PATCH 03/44] prelim doc string --- pandas/io/formats/style_render.py | 130 +++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index a1d3dd8d30848..79bb6f8853fcb 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -811,7 +811,135 @@ def format_index( thousands: str | None = None, escape: str | None = None, ) -> StylerRenderer: - r""" """ + r""" + Format the text display value of index labels or column headers. + + .. versionadded:: 1.4.0 + + Parameters + ---------- + formatter : str, callable, dict or None + Object to define how values are displayed. See notes. + axis : int, str + 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``. + + Returns + ------- + self : Styler + + 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 raise a ``KeyError``. + + When using a ``formatter`` string the dtypes must be compatible, otherwise a + `ValueError` will be raised. + + Examples + -------- + Using ``na_rep`` and ``precision`` with the default ``formatter`` + + >>> df = pd.DataFrame([[np.nan, 1.0, 'A'], [2.0, np.nan, 3.0]]) + >>> df.style.format(na_rep='MISS', precision=3) # doctest: +SKIP + 0 1 2 + 0 MISS 1.000 A + 1 2.000 MISS 3.000 + + Using a ``formatter`` specification on consistent column dtypes + + >>> df.style.format('{:.2f}', na_rep='MISS', subset=[0,1]) # doctest: +SKIP + 0 1 2 + 0 MISS 1.00 A + 1 2.00 MISS 3.000000 + + Using the default ``formatter`` for unspecified columns + + >>> df.style.format({0: '{:.2f}', 1: '£ {:.1f}'}, na_rep='MISS', precision=1) + ... # doctest: +SKIP + 0 1 2 + 0 MISS £ 1.0 A + 1 2.00 MISS 3.0 + + Multiple ``na_rep`` or ``precision`` specifications under the default + ``formatter``. + + >>> df.style.format(na_rep='MISS', precision=1, subset=[0]) + ... .format(na_rep='PASS', precision=2, subset=[1, 2]) # doctest: +SKIP + 0 1 2 + 0 MISS 1.00 A + 1 2.0 PASS 3.00 + + Using a callable ``formatter`` function. + + >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT' + >>> df.style.format({0: '{:.1f}', 2: func}, precision=4, na_rep='MISS') + ... # doctest: +SKIP + 0 1 2 + 0 MISS 1.0000 STRING + 1 2.0 MISS FLOAT + + Using a ``formatter`` with HTML ``escape`` and ``na_rep``. + + >>> df = pd.DataFrame([['
', '"A&B"', None]]) + >>> s = df.style.format( + ... '{0}', escape="html", na_rep="NA" + ... ) + >>> s.to_html() # doctest: +SKIP + ... + <div></div> + "A&B" + NA + ... + + Using a ``formatter`` with LaTeX ``escape``. + + >>> df = pd.DataFrame([["123"], ["~ ^"], ["$%#"]]) + >>> df.style.format("\\textbf{{{}}}", escape="latex").to_latex() + ... # doctest: +SKIP + \begin{tabular}{ll} + {} & {0} \\ + 0 & \textbf{123} \\ + 1 & \textbf{\textasciitilde \space \textasciicircum } \\ + 2 & \textbf{\$\%\#} \\ + \end{tabular} + """ if axis == 0: display_funcs_, obj = self._display_funcs_index, self.index elif axis == 1: From 7e9400ad594cdcac895c0f0ad18b8aa62a737d1f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 18 Aug 2021 18:14:55 +0200 Subject: [PATCH 04/44] format_index docs --- pandas/io/formats/style_render.py | 82 +++++++++++++------------------ 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 79bb6f8853fcb..6f52b5801b614 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -867,7 +867,7 @@ def format_index( 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 raise a ``KeyError``. + 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. @@ -876,80 +876,66 @@ def format_index( -------- Using ``na_rep`` and ``precision`` with the default ``formatter`` - >>> df = pd.DataFrame([[np.nan, 1.0, 'A'], [2.0, np.nan, 3.0]]) - >>> df.style.format(na_rep='MISS', precision=3) # doctest: +SKIP - 0 1 2 - 0 MISS 1.000 A - 1 2.000 MISS 3.000 + >>> df = pd.DataFrame([[1, 2, 3]], columns=[2.0, np.nan, 4.0]]) + >>> df.style.format_index(axis=1, na_rep='MISS', precision=3) # doctest: +SKIP + 2.000 MISS 4.000 + 0 1 2 3 - Using a ``formatter`` specification on consistent column dtypes + Using a ``formatter`` specification on consistent dtypes in a level - >>> df.style.format('{:.2f}', na_rep='MISS', subset=[0,1]) # doctest: +SKIP - 0 1 2 - 0 MISS 1.00 A - 1 2.00 MISS 3.000000 + >>> df.style.format_index('{:.2f}', axis=1, na_rep='MISS') # doctest: +SKIP + 2.00 MISS 4.00 + 0 1 2 3 - Using the default ``formatter`` for unspecified columns + Using the default ``formatter`` for unspecified levels - >>> df.style.format({0: '{:.2f}', 1: '£ {:.1f}'}, na_rep='MISS', precision=1) + >>> df = pd.DataFrame([[1, 2, 3]], + ... columns=pd.MultiIndex.from_arrays([["a", "a", "b"],[2, np.nan, 4]])) + >>> df.style.format_index({0: lambda v: upper(v)}, axis=1, precision=1) ... # doctest: +SKIP - 0 1 2 - 0 MISS £ 1.0 A - 1 2.00 MISS 3.0 - - Multiple ``na_rep`` or ``precision`` specifications under the default - ``formatter``. - - >>> df.style.format(na_rep='MISS', precision=1, subset=[0]) - ... .format(na_rep='PASS', precision=2, subset=[1, 2]) # doctest: +SKIP - 0 1 2 - 0 MISS 1.00 A - 1 2.0 PASS 3.00 + A B + 2.0 nan 4.0 + 0 1 2 3 Using a callable ``formatter`` function. >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT' - >>> df.style.format({0: '{:.1f}', 2: func}, precision=4, na_rep='MISS') + >>> df.style.format_index(func, axis=1, na_rep='MISS') ... # doctest: +SKIP - 0 1 2 - 0 MISS 1.0000 STRING - 1 2.0 MISS FLOAT + STRING STRING + FLOAT MISS FLOAT + 0 1 2 3 Using a ``formatter`` with HTML ``escape`` and ``na_rep``. - >>> df = pd.DataFrame([['
', '"A&B"', None]]) - >>> s = df.style.format( - ... '{0}', escape="html", na_rep="NA" - ... ) - >>> s.to_html() # doctest: +SKIP - ... - <div></div> - "A&B" - NA + >>> df = pd.DataFrame([[1, 2, 3]], columns=['"A"', 'A&B', None]) + >>> s = df.style.format_index('$ {0}', axis=1, escape="html", na_rep="NA") + $ "A" + $ A&B + NA ... Using a ``formatter`` with LaTeX ``escape``. - >>> df = pd.DataFrame([["123"], ["~ ^"], ["$%#"]]) - >>> df.style.format("\\textbf{{{}}}", escape="latex").to_latex() + >>> df = pd.DataFrame([[1, 2, 3]], columns=["123", "~", "$%#"]) + >>> df.style.format_index("\\textbf{{{}}}", escape="latex", axis=1).to_latex() ... # doctest: +SKIP - \begin{tabular}{ll} - {} & {0} \\ - 0 & \textbf{123} \\ - 1 & \textbf{\textasciitilde \space \textasciicircum } \\ - 2 & \textbf{\$\%\#} \\ + \begin{tabular}{lrrr} + {} & {\textbf{123}} & {\textbf{\textasciitilde }} & {\textbf{\$\%\#}} \\ + 0 & 1 & 2 & 3 \\ \end{tabular} """ + axis = self.data._get_axis_number(axis) if axis == 0: display_funcs_, obj = self._display_funcs_index, self.index - elif axis == 1: + else: display_funcs_, obj = self._display_funcs_columns, self.columns - levels_ = refactor_levels(level, obj) if all( ( formatter is None, + level is None, precision is None, decimal == ".", thousands is None, @@ -968,7 +954,7 @@ def format_index( for level, formatter_ in formatter.items() } - for level in set(formatter.keys()).union(levels_): + for level in levels_: format_func = _maybe_wrap_formatter( formatter.get(level), na_rep=na_rep, From 87a6c88b2feccdb67b3f18b553ba713a62e43283 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 19 Aug 2021 21:15:03 +0200 Subject: [PATCH 05/44] refactor for perf --- pandas/io/formats/style_render.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 62dfcfd5662b9..5873903ec302f 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -954,9 +954,9 @@ def format_index( for level, formatter_ in formatter.items() } - for level in levels_: + for lvl in levels_: format_func = _maybe_wrap_formatter( - formatter.get(level), + formatter.get(lvl), na_rep=na_rep, precision=precision, decimal=decimal, @@ -964,11 +964,8 @@ def format_index( escape=escape, ) - for i in range(len(obj)): - if axis == 0: - display_funcs_[(i, level)] = format_func - else: - display_funcs_[(level, i)] = format_func + for idx in [(i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj))]: + display_funcs_[idx] = format_func return self From 9c969adeacd2f55a404bc18605f589b519ab415d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 20 Aug 2021 07:25:37 +0200 Subject: [PATCH 06/44] add test --- pandas/tests/io/formats/style/test_format.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index eda0e2eeecd34..590b8ec700661 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -62,6 +62,12 @@ def test_format_dict(styler): assert ctx["body"][0][2]["display_value"] == "-60.90%" +def test_format_index_dict(styler): + ctx = styler.format_index({0: lambda v: v.upper()})._translate(True, True) + for i, val in enumerate(["X", "Y"]): + assert ctx["body"][i][0]["display_value"] == val + + def test_format_string(styler): ctx = styler.format("{:.2f}")._translate(True, True) assert ctx["body"][0][1]["display_value"] == "0.00" From 26f390647ce79d3ebfefddaeb91001a181e4fd4c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 21 Aug 2021 11:03:15 +0200 Subject: [PATCH 07/44] add tests: escape --- pandas/tests/io/formats/style/test_format.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 590b8ec700661..13d1fb2c72cb8 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -2,6 +2,7 @@ import pytest from pandas import ( + NA, DataFrame, IndexSlice, NaT, @@ -39,13 +40,13 @@ def test_display_format(styler): def test_display_format_index(styler, index, columns): exp_index = ["x", "y"] if index: - styler.format_index(lambda v: v.upper(), axis=0) + styler.format_index(lambda v: v.upper(), axis=0) # test callable exp_index = ["X", "Y"] exp_columns = ["A", "B"] if columns: - styler.format_index(lambda v: v.lower(), axis=1) - exp_columns = ["a", "b"] + styler.format_index("*{}*", axis=1) # test string + exp_columns = ["*A*", "*B*"] ctx = styler._translate(True, True) @@ -103,6 +104,14 @@ def test_format_with_na_rep(): assert ctx["body"][1][2]["display_value"] == "120.00%" +def test_format_index_with_na_rep(): + df = DataFrame([[1, 2, 3, 4, 5]], columns=["A", None, np.nan, NaT, NA]) + ctx = df.style.format_index(None, na_rep="--", axis=1)._translate(True, True) + assert ctx["head"][0][1]["display_value"] == "A" + for i in [2, 3, 4, 5]: + assert ctx["head"][0][i]["display_value"] == "--" + + def test_format_non_numeric_na(): # GH 21527 28358 df = DataFrame( @@ -159,6 +168,13 @@ def test_format_escape_html(escape, exp): expected = f'&{exp}&' assert expected in s.to_html() + # also test format_index() + styler = Styler(DataFrame(columns=[chars]), uuid_len=0) + styler.format_index("&{0}&", escape=None, axis=1) + assert styler._translate(True, True)["head"][0][1]["display_value"] == f"&{chars}&" + styler.format_index("&{0}&", escape=escape, axis=1) + assert styler._translate(True, True)["head"][0][1]["display_value"] == f"&{exp}&" + def test_format_escape_na_rep(): # tests the na_rep is not escaped From ec404189cf08f2219a5462bb927d5a8bc0c196da Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 21 Aug 2021 11:09:35 +0200 Subject: [PATCH 08/44] add tests: escape na_rep --- pandas/tests/io/formats/style/test_format.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 13d1fb2c72cb8..f5ae822b475c4 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -185,6 +185,14 @@ def test_format_escape_na_rep(): assert ex in s.to_html() assert expected2 in s.to_html() + # also test for format_index() + df = DataFrame(columns=['<>&"', None]) + styler = Styler(df, uuid_len=0) + styler.format_index("X&{0}>X", escape="html", na_rep="&", axis=1) + ctx = styler._translate(True, True) + assert ctx["head"][0][1]["display_value"] == "X&<>&">X" + assert ctx["head"][0][2]["display_value"] == "&" + def test_format_escape_floats(styler): # test given formatter for number format is not impacted by escape From 0de5397741c26ff72448bd74a902c31170b91b35 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 21 Aug 2021 11:16:56 +0200 Subject: [PATCH 09/44] add tests: raises --- pandas/tests/io/formats/style/test_format.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index f5ae822b475c4..ba6c364482ad1 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -206,9 +206,10 @@ def test_format_escape_floats(styler): @pytest.mark.parametrize("formatter", [5, True, [2.0]]) -def test_format_raises(styler, formatter): +@pytest.mark.parametrize("func", ["format", "format_index"]) +def test_format_raises(styler, formatter, func): with pytest.raises(TypeError, match="expected str or callable"): - styler.format(formatter) + getattr(styler, func)(formatter) def test_format_with_precision(): From 6fe8285993a48e6cda1032e80630a17af3f4b4d5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 21 Aug 2021 13:00:15 +0200 Subject: [PATCH 10/44] test decimal and thousands --- pandas/tests/io/formats/style/test_format.py | 36 +++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index ba6c364482ad1..ee13322f1253a 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -271,41 +271,43 @@ def test_format_subset(): @pytest.mark.parametrize("formatter", [None, "{:,.1f}"]) @pytest.mark.parametrize("decimal", [".", "*"]) @pytest.mark.parametrize("precision", [None, 2]) -def test_format_thousands(formatter, decimal, precision): - s = DataFrame([[1000000.123456789]]).style # test float - result = s.format( +@pytest.mark.parametrize("func, col", [("format", 1), ("format_index", 0)]) +def test_format_thousands(formatter, decimal, precision, func, col): + styler = DataFrame([[1000000.123456789]], index=[1000000.123456789]).style + result = getattr(styler, func)( # testing float thousands="_", formatter=formatter, decimal=decimal, precision=precision )._translate(True, True) - assert "1_000_000" in result["body"][0][1]["display_value"] + assert "1_000_000" in result["body"][0][col]["display_value"] - s = DataFrame([[1000000]]).style # test int - result = s.format( + styler = DataFrame([[1000000]], index=[1000000]).style + result = getattr(styler, func)( # testing int thousands="_", formatter=formatter, decimal=decimal, precision=precision )._translate(True, True) - assert "1_000_000" in result["body"][0][1]["display_value"] + assert "1_000_000" in result["body"][0][col]["display_value"] - s = DataFrame([[1 + 1000000.123456789j]]).style # test complex - result = s.format( + styler = DataFrame([[1 + 1000000.123456789j]], index=[1 + 1000000.123456789j]).style + result = getattr(styler, func)( # testing complex thousands="_", formatter=formatter, decimal=decimal, precision=precision )._translate(True, True) - assert "1_000_000" in result["body"][0][1]["display_value"] + assert "1_000_000" in result["body"][0][col]["display_value"] @pytest.mark.parametrize("formatter", [None, "{:,.4f}"]) @pytest.mark.parametrize("thousands", [None, ",", "*"]) @pytest.mark.parametrize("precision", [None, 4]) -def test_format_decimal(formatter, thousands, precision): - s = DataFrame([[1000000.123456789]]).style # test float - result = s.format( +@pytest.mark.parametrize("func, col", [("format", 1), ("format_index", 0)]) +def test_format_decimal(formatter, thousands, precision, func, col): + styler = DataFrame([[1000000.123456789]], index=[1000000.123456789]).style + result = getattr(styler, func)( # testing float decimal="_", formatter=formatter, thousands=thousands, precision=precision )._translate(True, True) - assert "000_123" in result["body"][0][1]["display_value"] + assert "000_123" in result["body"][0][col]["display_value"] - s = DataFrame([[1 + 1000000.123456789j]]).style # test complex - result = s.format( + styler = DataFrame([[1 + 1000000.123456789j]], index=[1 + 1000000.123456789j]).style + result = getattr(styler, func)( # testing complex decimal="_", formatter=formatter, thousands=thousands, precision=precision )._translate(True, True) - assert "000_123" in result["body"][0][1]["display_value"] + assert "000_123" in result["body"][0][col]["display_value"] def test_str_escape_error(): From 6b61e31dab66b6cbd6003cce8638bc6bf13221a2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 21 Aug 2021 17:15:50 +0200 Subject: [PATCH 11/44] test precision --- pandas/tests/io/formats/style/test_format.py | 44 ++++++++++---------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index ee13322f1253a..bcfbd0e12051e 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -212,28 +212,30 @@ def test_format_raises(styler, formatter, func): getattr(styler, func)(formatter) -def test_format_with_precision(): +@pytest.mark.parametrize( + "precision, expected", + [ + (1, ["1.0", "2.0", "3.2", "4.6"]), + (2, ["1.00", "2.01", "3.21", "4.57"]), + (3, ["1.000", "2.009", "3.212", "4.566"]), + ], +) +def test_format_with_precision(precision, expected): # Issue #13257 - df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) - s = Styler(df) - - ctx = s.format(precision=1)._translate(True, True) - assert ctx["body"][0][1]["display_value"] == "1.0" - assert ctx["body"][0][2]["display_value"] == "2.0" - assert ctx["body"][1][1]["display_value"] == "3.2" - assert ctx["body"][1][2]["display_value"] == "4.6" - - ctx = s.format(precision=2)._translate(True, True) - assert ctx["body"][0][1]["display_value"] == "1.00" - assert ctx["body"][0][2]["display_value"] == "2.01" - assert ctx["body"][1][1]["display_value"] == "3.21" - assert ctx["body"][1][2]["display_value"] == "4.57" - - ctx = s.format(precision=3)._translate(True, True) - assert ctx["body"][0][1]["display_value"] == "1.000" - assert ctx["body"][0][2]["display_value"] == "2.009" - assert ctx["body"][1][1]["display_value"] == "3.212" - assert ctx["body"][1][2]["display_value"] == "4.566" + df = DataFrame([[1.0, 2.0090, 3.2121, 4.566]], columns=[1.0, 2.0090, 3.2121, 4.566]) + styler = Styler(df) + styler.format(precision=precision) + styler.format_index(precision=precision, axis=1) + + ctx = styler._translate(True, True) + for col, exp in enumerate(expected): + assert ctx["body"][0][col + 1]["display_value"] == exp # format test + assert ctx["head"][0][col + 1]["display_value"] == exp # format_index test + + +def test_format_index_level(): + # TODO + pass def test_format_subset(): From 666e460e3f2c8fa4e22b4a926af82f8e694d17e1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 30 Aug 2021 18:53:41 +0200 Subject: [PATCH 12/44] whats new --- doc/source/whatsnew/v1.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index be647e344f270..37822fc8d3dcd 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -70,7 +70,7 @@ Styler :class:`.Styler` has been further developed in 1.4.0. The following enhancements have been made: - - Styling of indexing has been added, with :meth:`.Styler.apply_index` and :meth:`.Styler.applymap_index`. These mirror the signature of the methods already used to style data values, and work with both HTML and LaTeX format (:issue:`41893`). + - Styling and formatting of indexes has been added, with :meth:`.Styler.apply_index`, :meth:`.Styler.applymap_index` and :meth:`.Styler.format_index`. These mirror the signature of the methods already used to style and format data values, and work with both HTML and LaTeX format (:issue:`41893`, :issue:`43101`). - :meth:`.Styler.bar` introduces additional arguments to control alignment and display (:issue:`26070`, :issue:`36419`), and it also validates the input arguments ``width`` and ``height`` (:issue:`42511`). - :meth:`.Styler.to_latex` introduces keyword argument ``environment``, which also allows a specific "longtable" entry through a separate jinja2 template (:issue:`41866`). - :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index`` and ``sparse_columns`` (:issue:`41946`) From 49bb7316346486b64c5f1893525fa1bc404547a1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 30 Aug 2021 19:22:19 +0200 Subject: [PATCH 13/44] level tests --- pandas/tests/io/formats/style/test_format.py | 37 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index bcfbd0e12051e..ddb9e8dc9d7f3 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -5,6 +5,7 @@ NA, DataFrame, IndexSlice, + MultiIndex, NaT, Timestamp, ) @@ -233,9 +234,39 @@ def test_format_with_precision(precision, expected): assert ctx["head"][0][col + 1]["display_value"] == exp # format_index test -def test_format_index_level(): - # TODO - pass +@pytest.mark.parametrize("axis", [0, 1]) +@pytest.mark.parametrize( + "level, expected", + [ + (0, ["X", "X", "_", "_"]), # level int + ("zero", ["X", "X", "_", "_"]), # level name + (1, ["_", "_", "X", "X"]), # other level int + ("one", ["_", "_", "X", "X"]), # other level name + ([0, 1], ["X", "X", "X", "X"]), # both levels + ([0, "zero"], ["X", "X", "_", "_"]), # level int and name simultaneous + ([0, "one"], ["X", "X", "X", "X"]), # both levels as int and name + (["one", "zero"], ["X", "X", "X", "X"]), # both level names, reversed + ], +) +def test_format_index_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(lambda v: "X", level=level, axis=axis) + ctx = styler._translate(True, True) + + if axis == 0: # compare index + result = [ctx["body"][s][0]["display_value"] for s in range(2)] + result += [ctx["body"][s][1]["display_value"] for s in range(2)] + else: # compare columns + result = [ctx["head"][0][s + 1]["display_value"] for s in range(2)] + result += [ctx["head"][1][s + 1]["display_value"] for s in range(2)] + + assert expected == result def test_format_subset(): From 044cd052093723b4a9a9d22c4f52806d7f8983a3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 30 Aug 2021 20:46:42 +0200 Subject: [PATCH 14/44] user guide --- doc/source/user_guide/style.ipynb | 54 +++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 4de54c5d9471c..75d86e67466db 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -150,15 +150,14 @@ "\n", "### Formatting Values\n", "\n", - "Before adding styles it is useful to show that the [Styler][styler] can distinguish the *display* value from the *actual* value. To control the display value, the text is printed in each cell, and we can use the [.format()][formatfunc] method to manipulate this according to a [format spec string][format] or a callable that takes a single value and returns a string. It is possible to define this for the whole table or for individual columns. \n", + "Before adding styles it is useful to show that the [Styler][styler] can distinguish the *display* value from the *actual* value, in both datavlaues and index or columns headers. To control the display value, the text is printed in each cell as string, and we can use the [.format()][formatfunc] and [.format_index()][formatfuncindex] methods to manipulate this according to a [format spec string][format] or a callable that takes a single value and returns a string. It is possible to define this for the whole table, or index, or for individual columns, or MultiIndex levels. \n", "\n", - "Additionally, the format function has a **precision** argument to specifically help formatting floats, as well as **decimal** and **thousands** separators to support other locales, an **na_rep** argument to display missing data, and an **escape** argument to help displaying safe-HTML or safe-LaTeX. The default formatter is configured to adopt pandas' regular `display.precision` option, controllable using `with pd.option_context('display.precision', 2):`\n", - "\n", - "Here is an example of using the multiple options to control the formatting generally and with specific column formatters.\n", + "Additionally, the format function has a **precision** argument to specifically help formatting floats, as well as **decimal** and **thousands** separators to support other locales, an **na_rep** argument to display missing data, and an **escape** argument to help displaying safe-HTML or safe-LaTeX. The default formatter is configured to adopt pandas' regular `display.precision` option, controllable using `with pd.option_context('display.precision', 2):` \n", "\n", "[styler]: ../reference/api/pandas.io.formats.style.Styler.rst\n", "[format]: https://docs.python.org/3/library/string.html#format-specification-mini-language\n", - "[formatfunc]: ../reference/api/pandas.io.formats.style.Styler.format.rst" + "[formatfunc]: ../reference/api/pandas.io.formats.style.Styler.format.rst\n", + "[formatfuncindex]: ../reference/api/pandas.io.formats.style.Styler.format_index.rst" ] }, { @@ -173,6 +172,49 @@ " })" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using Styler to manipulate the display is a useful feature because maintaining the indexing and datavalues for other purposes gives greater control. You do not have to overwrite your DataFrame to display it how you like. Here is an example of using the formatting functions whilst still relying on the underlying data for indexing and calculations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "weather_df = pd.DataFrame(np.random.rand(10,2)*5, \n", + " index=pd.date_range(start=\"2021-01-01\", periods=10),\n", + " columns=[\"Tokyo\", \"Beijing\"])\n", + "\n", + "def rain_condition(v): \n", + " if v < 1.5:\n", + " return \"dry\"\n", + " elif v < 2.75:\n", + " return \"wet\"\n", + " return \"very wet\"\n", + "\n", + "def make_pretty(styler):\n", + " styler.set_caption(\"Rainfall Level\")\n", + " styler.format(rain_condition)\n", + " styler.format_index(lambda v: v.strftime(\"%A\"))\n", + " styler.background_gradient(axis=None, vmin=1, vmax=5)\n", + " return styler\n", + "\n", + "weather_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "weather_df.loc[\"2021-01-04\":\"2021-01-08\"].style.pipe(make_pretty)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -187,7 +229,7 @@ "\n", "Hiding does not change the integer arrangement of CSS classes, e.g. hiding the first two columns of a DataFrame means the column class indexing will start at `col2`, since `col0` and `col1` are simply ignored.\n", "\n", - "We can update our `Styler` object to hide some data and format the values.\n", + "We can update our `Styler` object from before to hide some data and format the values.\n", "\n", "[hideidx]: ../reference/api/pandas.io.formats.style.Styler.hide_index.rst\n", "[hidecols]: ../reference/api/pandas.io.formats.style.Styler.hide_columns.rst" From 8fc497da77a10416d65b8ef868d2906ab56bc64e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 31 Aug 2021 12:52:18 +0200 Subject: [PATCH 15/44] typing fix --- 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 5873903ec302f..92dbaf9c47ab2 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1294,7 +1294,7 @@ def maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: def refactor_levels( level: Level | list[Level] | None, obj: Index, -) -> list[Level]: +) -> list[int]: """ Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``. @@ -1310,7 +1310,7 @@ def refactor_levels( list : refactored arg with a list of levels to hide """ if level is None: - levels_: list[Level] = list(range(obj.nlevels)) + levels_: list[int] = list(range(obj.nlevels)) elif isinstance(level, int): levels_ = [level] elif isinstance(level, str): From 8fb9519bcc97bdd77a7be97120577381534de6fb Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 31 Aug 2021 13:04:03 +0200 Subject: [PATCH 16/44] user guide refactor --- doc/source/user_guide/style.ipynb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 75d86e67466db..7b7df83274620 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -190,17 +190,17 @@ " columns=[\"Tokyo\", \"Beijing\"])\n", "\n", "def rain_condition(v): \n", - " if v < 1.5:\n", - " return \"dry\"\n", + " if v < 1.75:\n", + " return \"Dry\"\n", " elif v < 2.75:\n", - " return \"wet\"\n", - " return \"very wet\"\n", + " return \"Rain\"\n", + " return \"Heavy Rain\"\n", "\n", "def make_pretty(styler):\n", - " styler.set_caption(\"Rainfall Level\")\n", + " styler.set_caption(\"Weather Conditions\")\n", " styler.format(rain_condition)\n", " styler.format_index(lambda v: v.strftime(\"%A\"))\n", - " styler.background_gradient(axis=None, vmin=1, vmax=5)\n", + " styler.background_gradient(axis=None, vmin=1, vmax=5, cmap=\"YlGnBu\")\n", " return styler\n", "\n", "weather_df" @@ -2016,7 +2016,6 @@ } ], "metadata": { - "celltoolbar": "Edit Metadata", "kernelspec": { "display_name": "Python 3", "language": "python", From df7548c4c2039b1563cc703680e06e1baa27ce59 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 1 Sep 2021 08:53:34 +0200 Subject: [PATCH 17/44] input to axis --- 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 f1b0d6922cd5a..f252d4cb0a130 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -833,7 +833,7 @@ def format_index( ---------- formatter : str, callable, dict or None Object to define how values are displayed. See notes. - axis : int, str + 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. From e36f198263fe4af9fc9a9cccdd05702126db4c1a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 2 Sep 2021 18:19:44 +0200 Subject: [PATCH 18/44] fix tests --- pandas/tests/io/formats/style/test_style.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 9eeb4621f5360..65e46133598ea 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1025,7 +1025,7 @@ def test_mi_sparse_column_names(self): }, { "class": "col_heading level1 col0", - "display_value": 1, + "display_value": "1", "is_visible": True, "type": "th", "value": 1, @@ -1033,7 +1033,7 @@ def test_mi_sparse_column_names(self): }, { "class": "col_heading level1 col1", - "display_value": 0, + "display_value": "0", "is_visible": True, "type": "th", "value": 0, @@ -1041,7 +1041,7 @@ def test_mi_sparse_column_names(self): }, { "class": "col_heading level1 col2", - "display_value": 1, + "display_value": "1", "is_visible": True, "type": "th", "value": 1, @@ -1049,7 +1049,7 @@ def test_mi_sparse_column_names(self): }, { "class": "col_heading level1 col3", - "display_value": 0, + "display_value": "0", "is_visible": True, "type": "th", "value": 0, @@ -1147,7 +1147,7 @@ def test_hide_columns_index_mult_levels(self): # column headers assert ctx["head"][0][2]["is_visible"] assert ctx["head"][1][2]["is_visible"] - assert ctx["head"][1][3]["display_value"] == 1 + assert ctx["head"][1][3]["display_value"] == "1" # indices assert ctx["body"][0][0]["is_visible"] # data From ecd01dd298378e872e63f9d25bd9565562318d65 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 5 Sep 2021 10:38:47 +0200 Subject: [PATCH 19/44] fix recent merged tests --- pandas/tests/io/formats/style/test_format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 86b9c90ba4b79..1d2285b6d402a 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -408,7 +408,7 @@ def test_1level_multiindex(): midx = MultiIndex.from_product([[1, 2]], names=[""]) df = DataFrame(-1, index=midx, columns=[0, 1]) ctx = df.style._translate(True, True) - assert ctx["body"][0][0]["display_value"] == 1 + assert ctx["body"][0][0]["display_value"] == "1" assert ctx["body"][0][0]["is_visible"] is True - assert ctx["body"][1][0]["display_value"] == 2 + assert ctx["body"][1][0]["display_value"] == "2" assert ctx["body"][1][0]["is_visible"] is True From 4ccf831639ec6dba15a5f0c46304293c489816dd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 7 Sep 2021 23:52:32 +0200 Subject: [PATCH 20/44] pandas to_string --- pandas/io/formats/style.py | 17 +++++++++++++++++ pandas/io/formats/style_render.py | 9 +++++++++ pandas/io/formats/templates/string.tpl | 12 ++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 pandas/io/formats/templates/string.tpl diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 92bd4bcc7ced1..a0852098e054e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -988,6 +988,23 @@ def to_html( html, buf=buf, encoding=(encoding if buf is not None else None) ) + def to_string( + self, + buf=None, + encoding=None, + sparse_index: bool | None = None, + sparse_columns: bool | None = None, + ): + obj = self._copy(deepcopy=True) + + text = obj._render_string( + sparse_columns=sparse_columns, + sparse_index=sparse_index, + ) + return save_to_buffer( + text, buf=buf, encoding=(encoding if buf is not None else None) + ) + def set_td_classes(self, classes: DataFrame) -> Styler: """ Set the DataFrame of strings added to the ``class`` attribute of ```` diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 6be75262a64ca..b1f1d5a631a9a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -67,6 +67,7 @@ class StylerRenderer: template_html_table = env.get_template("html_table.tpl") template_html_style = env.get_template("html_style.tpl") template_latex = env.get_template("latex.tpl") + template_string = env.get_template("string.tpl") def __init__( self, @@ -149,6 +150,14 @@ def _render_latex(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> s d.update(kwargs) return self.template_latex.render(**d) + def _render_string(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str: + self._compute() + + d = self._translate(sparse_index, sparse_columns, blank="") + + d.update(kwargs) + return self.template_string.render(**d) + def _compute(self): """ Execute the style functions built up in `self._todo`. diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl new file mode 100644 index 0000000000000..6b984d6b2570b --- /dev/null +++ b/pandas/io/formats/templates/string.tpl @@ -0,0 +1,12 @@ +{% for r in head %} +{% for c in r %} +{% if c.is_visible != False %}{{c.display_value}}{% endif %}{% if not loop.last %} {% endif %} +{% endfor %} + +{% endfor %} +{% for r in body %} +{% for c in r %} +{% if c.is_visible != False %}{{c.display_value}}{% endif %}{% if not loop.last %} {% endif %} +{% endfor %} + +{% endfor %} From 06ce6bfe47d9d1d982975ba4ff3403a747c611aa Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 19:35:15 +0200 Subject: [PATCH 21/44] add to style.rst --- doc/source/reference/style.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 11d57e66c4773..e67813084e389 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -39,6 +39,7 @@ Style application Styler.apply_index Styler.applymap_index Styler.format + Styler.format_index Styler.hide_index Styler.hide_columns Styler.set_td_classes From 8312e304e60bcd369413e4e5d9faa54eda9cd3e9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 20:38:21 +0200 Subject: [PATCH 22/44] add justification of strings --- pandas/io/formats/style_render.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index f904ad7c11a63..89105155d6726 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -161,6 +161,7 @@ def _render_string(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> self._compute() d = self._translate(sparse_index, sparse_columns, blank="") + self._translate_string(d) d.update(kwargs) return self.template_string.render(**d) @@ -630,6 +631,21 @@ def _translate_latex(self, d: dict) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body + def _translate_string(self, d: dict) -> None: + """ + Manipulate the render dict for string output + """ + d["col_max_char"] = [0] * len(d["body"][0]) + for row in d["head"] + d["body"]: + for cn, col in enumerate(row): + chars = len(col["display_value"]) + if chars > d["col_max_char"][cn]: + d["col_max_char"][cn] = chars + + for row in d["head"] + d["body"]: + for cn, col in enumerate(row): + col["display_value"] = col["display_value"].rjust(d["col_max_char"][cn]) + def format( self, formatter: ExtFormatter | None = None, From 5924dcacaf062b2cc7eb00c60e9294e5e473c286 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 20:55:38 +0200 Subject: [PATCH 23/44] bug fix --- pandas/io/formats/style_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8c5af730a5fc7..aa91c872f7e97 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -351,7 +351,8 @@ def _translate_header( "th", f"{col_heading_class} level{r} col{c}", value, - _is_visible(c, r, col_lengths), + _is_visible(c, r, col_lengths) + and c not in self.hidden_columns, attributes=( f'colspan="{col_lengths.get((r, c), 0)}"' if col_lengths.get((r, c), 0) > 1 From 739a4a06fb22e38e1cd203ecfbc5cf401a0590ef Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 21:08:24 +0200 Subject: [PATCH 24/44] test and whats new --- doc/source/whatsnew/v1.4.0.rst | 1 + pandas/tests/io/formats/style/test_style.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index cac22fc06b89b..7107e3eecb2f1 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -440,6 +440,7 @@ Styler - Bug in :meth:`Styler.apply` where functions which returned Series objects were not correctly handled in terms of aligning their index labels (:issue:`13657`, :issue:`42014`) - Bug when rendering an empty DataFrame with a named index (:issue:`43305`). - Bug when rendering a single level MultiIndex (:issue:`43383`). +- Bug when combining non-sparse rendering and :meth:`.Styler.hide_columns` (:issue:`43464`) Other ^^^^^ diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index e5c3d9ae14bdd..1cfcbb5232515 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1447,3 +1447,14 @@ def test_caption_raises(mi_styler, caption): msg = "`caption` must be either a string or 2-tuple of strings." with pytest.raises(ValueError, match=msg): mi_styler.set_caption(caption) + + +def test_no_sparse_hiding_columns(): + # GH 43464 + midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]]) + df = DataFrame(9, index=[0], columns=midx) + styler = df.style.hide_columns((1, "a")) + ctx = styler._translate(False, False) + + for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]: + assert ctx["head"][ix[0]][ix[1]]["is_visible"] is False From 9121aa3e863429ec6b5d0e9bae956471943dbd68 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 21:16:40 +0200 Subject: [PATCH 25/44] no space for hidden cols --- pandas/io/formats/templates/string.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 6b984d6b2570b..7a7d791a02771 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,12 +1,12 @@ {% for r in head %} {% for c in r %} -{% if c.is_visible != False %}{{c.display_value}}{% endif %}{% if not loop.last %} {% endif %} +{% if c.is_visible != False %}{{c.display_value}}{% if not loop.last %} {% endif %}{% endif %} {% endfor %} {% endfor %} {% for r in body %} {% for c in r %} -{% if c.is_visible != False %}{{c.display_value}}{% endif %}{% if not loop.last %} {% endif %} +{% if c.is_visible != False %}{{c.display_value}}{% if not loop.last %} {% endif %}{% endif %} {% endfor %} {% endfor %} From 563754cd2b49be595240d59170b274eccc936147 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 8 Sep 2021 22:39:14 +0200 Subject: [PATCH 26/44] max_colwidth and align options --- pandas/io/formats/style.py | 8 ++++++ pandas/io/formats/style_render.py | 43 ++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 80ce54ec8b1bc..12058350b4217 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -997,12 +997,20 @@ def to_string( encoding=None, sparse_index: bool | None = None, sparse_columns: bool | None = None, + max_colwidth: int | None = None, + align: str | None = None, ): obj = self._copy(deepcopy=True) + max_colwidth = 1e9 if max_colwidth is None else max_colwidth + align_options = {"r": "rjust", "c": "center", "l": "ljust"} + align = "rjust" if align is None else align_options[align] + text = obj._render_string( sparse_columns=sparse_columns, sparse_index=sparse_index, + align=align, + max_colwidth=max_colwidth, ) return save_to_buffer( text, buf=buf, encoding=(encoding if buf is not None else None) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index d1965f865205e..0c1701a712933 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -157,11 +157,20 @@ def _render_latex(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> s d.update(kwargs) return self.template_latex.render(**d) - def _render_string(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str: + def _render_string( + self, + sparse_index: bool, + sparse_columns: bool, + align: str, + max_colwidth: int, + **kwargs, + ) -> str: self._compute() d = self._translate(sparse_index, sparse_columns, blank="") - self._translate_string(d) + + precision = (get_option("styler.format.precision"),) + self._translate_string(d, max_colwidth, precision, align) d.update(kwargs) return self.template_string.render(**d) @@ -632,10 +641,18 @@ def _translate_latex(self, d: dict) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body - def _translate_string(self, d: dict) -> None: + def _translate_string( + self, + d: dict, + max_colwidth: int, + precision: int, + align: str, + ) -> None: """ Manipulate the render dict for string output """ + + # find the maximum length of items in each column d["col_max_char"] = [0] * len(d["body"][0]) for row in d["head"] + d["body"]: for cn, col in enumerate(row): @@ -643,9 +660,27 @@ def _translate_string(self, d: dict) -> None: if chars > d["col_max_char"][cn]: d["col_max_char"][cn] = chars + def char_refactor(value, display, max_cw, precision, align): + if len(display) <= max_cw: + return getattr(display, align)(max_cw) + elif len(display) > max_cw: + if isinstance(value, (float, complex)): + return getattr(f"{{:.{precision}E}}".format(value), align)(max_cw) + elif isinstance(value, int): + return getattr(f"{value:.0E}", align)(max_cw) + else: + return display[: max_cw - 3] + "..." + + # refactor display values to satisfy consistent column width for row in d["head"] + d["body"]: for cn, col in enumerate(row): - col["display_value"] = col["display_value"].rjust(d["col_max_char"][cn]) + col["display_value"] = char_refactor( + col["value"], + col["display_value"], + min(max_colwidth, d["col_max_char"][cn]), + precision, + align, + ) def format( self, From 718a15d7291297d423ee9d24a9708237d941331b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 12 Sep 2021 21:38:03 +0200 Subject: [PATCH 27/44] add docs --- pandas/io/formats/style_render.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 7827e87f4df92..6739516727ec0 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -671,10 +671,14 @@ def _translate_string( precision: int, align: str, ) -> None: - """ - Manipulate the render dict for string output - """ + r""" + Post-process the default render dict for the String template format. + Processing items included are: + - Measuring total column character length and justifying or trimming. + - Remove hidden indexes or reinsert missing th elements if part of multiindex + or multirow sparsification (so that \multirow and \multicol work correctly). + """ # find the maximum length of items in each column d["col_max_char"] = [0] * len(d["body"][0]) for row in d["head"] + d["body"]: From 7f6aa70a4aa4cfce42802e6c0b08f3f733cd41c2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 15 Sep 2021 22:22:37 +0200 Subject: [PATCH 28/44] add line_width and other args --- pandas/io/formats/style.py | 13 ++++++++-- pandas/io/formats/style_render.py | 33 +++++++++++++++++++++++--- pandas/io/formats/templates/string.tpl | 14 ++--------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e0001d13254cf..0fe5e70b006bd 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1016,19 +1016,28 @@ def to_string( sparse_index: bool | None = None, sparse_columns: bool | None = None, max_colwidth: int | None = None, + line_width: int | None = None, align: str | None = None, + delimiter: str = " ", ): obj = self._copy(deepcopy=True) + if sparse_index is None: + sparse_index = get_option("styler.sparse.index") + if sparse_columns is None: + sparse_columns = get_option("styler.sparse.columns") + max_colwidth = 1e9 if max_colwidth is None else max_colwidth - align_options = {"r": "rjust", "c": "center", "l": "ljust"} - align = "rjust" if align is None else align_options[align] + line_width = 1e9 if line_width is None else line_width + align = "none" if align is None else align text = obj._render_string( sparse_columns=sparse_columns, sparse_index=sparse_index, align=align, max_colwidth=max_colwidth, + line_width=line_width, + delimiter=delimiter, ) return save_to_buffer( text, buf=buf, encoding=(encoding if buf is not None else None) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 6739516727ec0..dafb7f9aadebf 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -179,6 +179,8 @@ def _render_string( precision = (get_option("styler.format.precision"),) self._translate_string(d, max_colwidth, precision, align) + self.template_string.globals["parse_row"] = _parse_string_row + d.update(kwargs) return self.template_string.render(**d) @@ -679,6 +681,8 @@ def _translate_string( - Remove hidden indexes or reinsert missing th elements if part of multiindex or multirow sparsification (so that \multirow and \multicol work correctly). """ + self._translate_latex(d) # post process for hidden elements akin to latex + # find the maximum length of items in each column d["col_max_char"] = [0] * len(d["body"][0]) for row in d["head"] + d["body"]: @@ -688,13 +692,21 @@ def _translate_string( d["col_max_char"][cn] = chars def char_refactor(value, display, max_cw, precision, align): + align_options = { + "r": "rjust", + "c": "center", + "l": "ljust", + "none": "__str__", + } + args = (max_cw,) if align != "none" else () + func = align_options[align] if len(display) <= max_cw: - return getattr(display, align)(max_cw) + return getattr(display, func)(*args) elif len(display) > max_cw: if isinstance(value, (float, complex)): - return getattr(f"{{:.{precision}E}}".format(value), align)(max_cw) + return getattr(f"{{:.{precision}E}}".format(value), func)(*args) elif isinstance(value, int): - return getattr(f"{value:.0E}", align)(max_cw) + return getattr(f"{value:.0E}", func)(*args) else: return display[: max_cw - 3] + "..." @@ -1867,3 +1879,18 @@ def _escape_latex(s): .replace("^", "\\textasciicircum ") .replace("ab2§=§8yz", "\\textbackslash ") ) + + +def _parse_string_row(row: list, spacer: str, max_line: int) -> str: + """ + Parse a row for jinja2 string template + """ + res = "" + len_res = 0 + for c in row: + if (len_res + len(c["display_value"])) > max_line: + res += "\n" + len_res = 0 + res += c["display_value"] + spacer + len_res += len(c["display_value"] + spacer) + return res + "\n" diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 7a7d791a02771..8c6f01d9c9220 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,12 +1,2 @@ -{% for r in head %} -{% for c in r %} -{% if c.is_visible != False %}{{c.display_value}}{% if not loop.last %} {% endif %}{% endif %} -{% endfor %} - -{% endfor %} -{% for r in body %} -{% for c in r %} -{% if c.is_visible != False %}{{c.display_value}}{% if not loop.last %} {% endif %}{% endif %} -{% endfor %} - -{% endfor %} +{% for r in head %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} +{% for r in body %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} From 48a15c905d01e7b1fc0c4f630cbd7baddd82c6dd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 17 Sep 2021 00:18:12 +0200 Subject: [PATCH 29/44] line max arg --- pandas/io/formats/style_render.py | 6 +++++- pandas/io/formats/templates/string.tpl | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index dafb7f9aadebf..a524ce3762022 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -181,7 +181,7 @@ def _render_string( self.template_string.globals["parse_row"] = _parse_string_row - d.update(kwargs) + d.update({**kwargs, "max_colwidth": max_colwidth}) return self.template_string.render(**d) def _compute(self): @@ -690,6 +690,10 @@ def _translate_string( chars = len(col["display_value"]) if chars > d["col_max_char"][cn]: d["col_max_char"][cn] = chars + d["line_max_char"] = sum( + (chars if chars < max_colwidth else max_colwidth) + for chars in d["col_max_char"] + ) def char_refactor(value, display, max_cw, precision, align): align_options = { diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 8c6f01d9c9220..dd6f28c60d92e 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,2 +1,6 @@ {% for r in head %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} +{% for val in col_max_char %}{{ "-" * (val if val < max_colwidth else max_colwidth + delimiter|length) }}{% endfor %} + {% for r in body %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} + +{% for val in col_max_char %}{{ val }}{% endfor %} From 7db906a61495a5be1c0b1c60daef20c6a474e8f2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 16:39:30 +0200 Subject: [PATCH 30/44] fix bug --- pandas/io/formats/style_render.py | 41 ++++++++++--------- .../tests/io/formats/style/test_to_latex.py | 14 +++++++ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 0ec6a9b470b50..48416b2a2db45 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -621,28 +621,31 @@ def _translate_latex(self, d: dict) -> None: ] body = [] for r, row in enumerate(d["body"]): - if all(self.hide_index_): - row_body_headers = [] - else: - row_body_headers = [ - { - **col, - "display_value": col["display_value"] - if col["is_visible"] - else "", - "cellstyle": self.ctx_index[r, c] if col["is_visible"] else [], - } + if r not in self.hidden_rows: + if all(self.hide_index_): + row_body_headers = [] + else: + row_body_headers = [ + { + **col, + "display_value": col["display_value"] + if col["is_visible"] + else "", + "cellstyle": self.ctx_index[r, c] + if col["is_visible"] + else [], + } + for c, col in enumerate(row) + if col["type"] == "th" + ] + + row_body_cells = [ + {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} for c, col in enumerate(row) - if col["type"] == "th" + if (col["is_visible"] and col["type"] == "td") ] - row_body_cells = [ - {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} - for c, col in enumerate(row) - if (col["is_visible"] and col["type"] == "td") - ] - - body.append(row_body_headers + row_body_cells) + body.append(row_body_headers + row_body_cells) d["body"] = body def format( diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 40ba3ca26afa4..913317e129bf9 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -782,3 +782,17 @@ def test_repr_option(styler): def test_siunitx_basic_headers(styler): assert "{} & {A} & {B} & {C} \\\\" in styler.to_latex(siunitx=True) assert " & A & B & C \\\\" in styler.to_latex() # default siunitx=False + + +def test_hide_index_latex(styler): + styler.hide_index([0]) + result = styler.to_latex() + expected = dedent( + """\ + \\begin{tabular}{lrrl} + & A & B & C \\\\ + 1 & 1 & -1.22 & cd \\\\ + \\end{tabular} + """ + ) + assert expected == result From ed05a90b999b5dbf64b0b0b464075a3ebaea43ce Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 16:48:27 +0200 Subject: [PATCH 31/44] fix bug --- pandas/io/formats/style_render.py | 44 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 48416b2a2db45..451c6f2fca923 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -620,32 +620,30 @@ def _translate_latex(self, d: dict) -> None: for r, row in enumerate(d["head"]) ] body = [] - for r, row in enumerate(d["body"]): - if r not in self.hidden_rows: - if all(self.hide_index_): - row_body_headers = [] - else: - row_body_headers = [ - { - **col, - "display_value": col["display_value"] - if col["is_visible"] - else "", - "cellstyle": self.ctx_index[r, c] - if col["is_visible"] - else [], - } - for c, col in enumerate(row) - if col["type"] == "th" - ] - - row_body_cells = [ - {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} + rows = [row for r, row in enumerate(d["body"]) if r not in self.hidden_rows] + for r, row in enumerate(rows): + if all(self.hide_index_): + row_body_headers = [] + else: + row_body_headers = [ + { + **col, + "display_value": col["display_value"] + if col["is_visible"] + else "", + "cellstyle": self.ctx_index[r, c] if col["is_visible"] else [], + } for c, col in enumerate(row) - if (col["is_visible"] and col["type"] == "td") + if col["type"] == "th" ] - body.append(row_body_headers + row_body_cells) + row_body_cells = [ + {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} + for c, col in enumerate(row) + if (col["is_visible"] and col["type"] == "td") + ] + + body.append(row_body_headers + row_body_cells) d["body"] = body def format( From 8e17483740cd5c92f6bf8a30d87d1ae61dab98aa Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 16:51:17 +0200 Subject: [PATCH 32/44] fix bug --- pandas/tests/io/formats/style/test_to_latex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 913317e129bf9..f252a89179c12 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -785,6 +785,7 @@ def test_siunitx_basic_headers(styler): def test_hide_index_latex(styler): + # GH 43637 styler.hide_index([0]) result = styler.to_latex() expected = dedent( From 22899e4b1d296d04ad29a762724b1727107b0264 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 16:57:46 +0200 Subject: [PATCH 33/44] whats new --- doc/source/whatsnew/v1.3.4.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.4.rst b/doc/source/whatsnew/v1.3.4.rst index daefa9dc618f5..726ea426e9e08 100644 --- a/doc/source/whatsnew/v1.3.4.rst +++ b/doc/source/whatsnew/v1.3.4.rst @@ -28,6 +28,7 @@ Fixed regressions Bug fixes ~~~~~~~~~ - Fixed bug in :meth:`.GroupBy.mean` with datetimelike values including ``NaT`` values returning incorrect results (:issue`:43132`) +- Fixed bug in :meth:`.Styler.to_latex` where hidden rows were not respected (:issue:`43637`) .. --------------------------------------------------------------------------- From 271bbc1ef6268dd9543b2e1ea2b14e988d5b342c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 18:56:27 +0200 Subject: [PATCH 34/44] doc args --- pandas/io/formats/style.py | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d97412a04207c..7e14af7acc327 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1012,6 +1012,7 @@ def to_html( def to_string( self, buf=None, + *, encoding=None, sparse_index: bool | None = None, sparse_columns: bool | None = None, @@ -1020,6 +1021,45 @@ def to_string( align: str | None = None, delimiter: str = " ", ): + """ + Write Styler to a file, buffer or string in text format. + + .. versionadded:: 1.4.0 + + Parameters + ---------- + buf : str, Path, or StringIO-like, optional, default None + Buffer to write to. If ``None``, the output is returned as a string. + encoding : str, optional + Character encoding setting for file output. + Defaults to ``pandas.options.styler.render.encoding`` value of "utf-8". + sparse_index : bool, optional + Whether to sparsify the display of a hierarchical index. Setting to False + will display each explicit level element in a hierarchical key for each row. + Defaults to ``pandas.options.styler.sparse.index`` value. + sparse_columns : bool, optional + Whether to sparsify the display of a hierarchical index. Setting to False + will display each explicit level element in a hierarchical key for each + column. Defaults to ``pandas.options.styler.sparse.columns`` value. + max_colwidth : int, optional + The maximum number of characters in any column, data will be trimmed if + necessary. + line_width : int, optional + The maximum number of characters permitted on any line. + align: str, optional + The justification of values in columns. + delimiter: string, default single space + The separator between data elements. + + Returns + ------- + str or None + If `buf` is None, returns the result as a string. Otherwise returns `None`. + + See Also + -------- + DataFrame.to_html: Write a DataFrame to a file, buffer or string in HTML format. + """ obj = self._copy(deepcopy=True) if sparse_index is None: From e2cd5ae58fd2d61584970ae8c9478bd97b3bbd90 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 18 Sep 2021 19:47:32 +0200 Subject: [PATCH 35/44] doc args --- pandas/io/formats/style.py | 18 +++++++ pandas/io/formats/style_render.py | 6 ++- pandas/io/formats/templates/string.tpl | 5 +- .../tests/io/formats/style/test_to_string.py | 50 +++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 pandas/tests/io/formats/style/test_to_string.py diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 7e14af7acc327..274c4da87757d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1014,8 +1014,11 @@ def to_string( buf=None, *, encoding=None, + hrules: bool = False, sparse_index: bool | None = None, sparse_columns: bool | None = None, + max_rows: int | None = None, + max_columns: int | None = None, max_colwidth: int | None = None, line_width: int | None = None, align: str | None = None, @@ -1033,6 +1036,8 @@ def to_string( encoding : str, optional Character encoding setting for file output. Defaults to ``pandas.options.styler.render.encoding`` value of "utf-8". + hrules : bool + Whether to create a horizontal divide between headers and values. sparse_index : bool, optional Whether to sparsify the display of a hierarchical index. Setting to False will display each explicit level element in a hierarchical key for each row. @@ -1041,6 +1046,16 @@ def to_string( Whether to sparsify the display of a hierarchical index. Setting to False will display each explicit level element in a hierarchical key for each column. Defaults to ``pandas.options.styler.sparse.columns`` value. + max_rows : int, optional + The maximum number of rows that will be rendered. Defaults to + ``pandas.options.styler.render.max_rows``, which is None. + max_columns : int, optional + The maximum number of columns that will be rendered. Defaults to + ``pandas.options.styler.render.max_columns``, which is None. + + Rows and columns may be reduced if the number of total elements is + large. This value is set to ``pandas.options.styler.render.max_elements``, + which is 262144 (18 bit browser rendering). max_colwidth : int, optional The maximum number of characters in any column, data will be trimmed if necessary. @@ -1074,10 +1089,13 @@ def to_string( text = obj._render_string( sparse_columns=sparse_columns, sparse_index=sparse_index, + max_rows=max_rows, + max_cols=max_columns, align=align, max_colwidth=max_colwidth, line_width=line_width, delimiter=delimiter, + hrules=hrules, ) return save_to_buffer( text, buf=buf, encoding=(encoding if buf is not None else None) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index c5d1a0f183016..155f9198b357a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -170,11 +170,13 @@ def _render_string( sparse_columns: bool, align: str, max_colwidth: int, + max_rows: int | None = None, + max_cols: int | None = None, **kwargs, ) -> str: self._compute() - d = self._translate(sparse_index, sparse_columns, blank="") + d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank="") precision = (get_option("styler.format.precision"),) self._translate_string(d, max_colwidth, precision, align) @@ -682,7 +684,7 @@ def _translate_string( - Remove hidden indexes or reinsert missing th elements if part of multiindex or multirow sparsification (so that \multirow and \multicol work correctly). """ - self._translate_latex(d) # post process for hidden elements akin to latex + # self._translate_latex(d) # post process for hidden elements akin to latex # find the maximum length of items in each column d["col_max_char"] = [0] * len(d["body"][0]) diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index dd6f28c60d92e..2208e3fb39b87 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,6 +1,5 @@ {% for r in head %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} +{% if hrules %} {% for val in col_max_char %}{{ "-" * (val if val < max_colwidth else max_colwidth + delimiter|length) }}{% endfor %} - +{% endif %} {% for r in body %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} - -{% for val in col_max_char %}{{ val }}{% endfor %} diff --git a/pandas/tests/io/formats/style/test_to_string.py b/pandas/tests/io/formats/style/test_to_string.py new file mode 100644 index 0000000000000..8b6ba4fd77c6c --- /dev/null +++ b/pandas/tests/io/formats/style/test_to_string.py @@ -0,0 +1,50 @@ +from textwrap import dedent + +import numpy as np +import pytest + +from pandas import ( + DataFrame, + MultiIndex, +) + +pytest.importorskip("jinja2") +from pandas.io.formats.style import Styler + + +@pytest.fixture +def df(): + return DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]}) + + +@pytest.fixture +def styler(df): + return Styler(df, uuid_len=0, precision=2) + + +def test_basic_string(styler): + result = styler.to_string() + expected = dedent( + """\ + A B C + 0 0 -0.61 ab + 1 1 -1.22 cd + """ + ) + assert result == expected + + +def test_comprehesive_string(): + midx = MultiIndex.from_product( + [["A", "B"], ["a", "b"], ["X", "Y"]], names=["zero", "one", "two"] + ) + cidx = MultiIndex.from_product( + [["a", "b"], ["C", "D"], ["V", "W"]], names=["zero", "one", "two"] + ) + df = DataFrame(np.arange(64).reshape(8, 8), index=midx, columns=cidx) + styler = Styler(df) + styler.hide_index(level=0).hide_columns(level=1) + styler.hide_index([("A", "a", "X"), ("A", "b", "X")]) + styler.hide_columns([("a", "C", "W"), ("a", "D", "V")]) + result = styler.to_string() + assert result == 1 From f6b98bf0d033e9343c47cf0e8ea0774c21b59bd6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Nov 2021 07:19:11 +0100 Subject: [PATCH 36/44] simplify to_string --- pandas/io/formats/templates/string.tpl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 2208e3fb39b87..235d94d30dc75 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,4 +1,6 @@ -{% for r in head %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} +{% for r in head %}{% for c in r %} +{{ c["display_value"] }}{% if not loop.last %}{{ delimiter }}{% endif %} +{% endfor %}{% endfor %} {% if hrules %} {% for val in col_max_char %}{{ "-" * (val if val < max_colwidth else max_colwidth + delimiter|length) }}{% endfor %} {% endif %} From 6d93a1a39aba3e7df6fe3577b9a37041926111bd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Nov 2021 18:45:30 +0100 Subject: [PATCH 37/44] simplify method --- pandas/io/formats/style.py | 21 ------- pandas/io/formats/style_render.py | 86 ++------------------------ pandas/io/formats/templates/string.tpl | 17 +++-- 3 files changed, 15 insertions(+), 109 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4844a074e782d..6212dea26c49e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1030,14 +1030,10 @@ def to_string( buf=None, *, encoding=None, - hrules: bool = False, sparse_index: bool | None = None, sparse_columns: bool | None = None, max_rows: int | None = None, max_columns: int | None = None, - max_colwidth: int | None = None, - line_width: int | None = None, - align: str | None = None, delimiter: str = " ", ): """ @@ -1052,8 +1048,6 @@ def to_string( encoding : str, optional Character encoding setting for file output. Defaults to ``pandas.options.styler.render.encoding`` value of "utf-8". - hrules : bool - Whether to create a horizontal divide between headers and values. sparse_index : bool, optional Whether to sparsify the display of a hierarchical index. Setting to False will display each explicit level element in a hierarchical key for each row. @@ -1072,13 +1066,6 @@ def to_string( Rows and columns may be reduced if the number of total elements is large. This value is set to ``pandas.options.styler.render.max_elements``, which is 262144 (18 bit browser rendering). - max_colwidth : int, optional - The maximum number of characters in any column, data will be trimmed if - necessary. - line_width : int, optional - The maximum number of characters permitted on any line. - align: str, optional - The justification of values in columns. delimiter: string, default single space The separator between data elements. @@ -1098,20 +1085,12 @@ def to_string( if sparse_columns is None: sparse_columns = get_option("styler.sparse.columns") - max_colwidth = 1e9 if max_colwidth is None else max_colwidth - line_width = 1e9 if line_width is None else line_width - align = "none" if align is None else align - text = obj._render_string( sparse_columns=sparse_columns, sparse_index=sparse_index, max_rows=max_rows, max_cols=max_columns, - align=align, - max_colwidth=max_colwidth, - line_width=line_width, delimiter=delimiter, - hrules=hrules, ) return save_to_buffer( text, buf=buf, encoding=(encoding if buf is not None else None) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 33e826255af59..83ab6a2e22fff 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -180,22 +180,18 @@ def _render_string( self, sparse_index: bool, sparse_columns: bool, - align: str, - max_colwidth: int, max_rows: int | None = None, max_cols: int | None = None, **kwargs, ) -> str: + """ + Render a Styler in string format + """ self._compute() d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank="") - precision = (get_option("styler.format.precision"),) - self._translate_string(d, max_colwidth, precision, align) - - self.template_string.globals["parse_row"] = _parse_string_row - - d.update({**kwargs, "max_colwidth": max_colwidth}) + d.update(kwargs) return self.template_string.render(**d) def _compute(self): @@ -806,65 +802,6 @@ def _translate_latex(self, d: dict) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body - def _translate_string( - self, - d: dict, - max_colwidth: int, - precision: int, - align: str, - ) -> None: - r""" - Post-process the default render dict for the String template format. - - Processing items included are: - - Measuring total column character length and justifying or trimming. - - Remove hidden indexes or reinsert missing th elements if part of multiindex - or multirow sparsification (so that \multirow and \multicol work correctly). - """ - # self._translate_latex(d) # post process for hidden elements akin to latex - - # find the maximum length of items in each column - d["col_max_char"] = [0] * len(d["body"][0]) - for row in d["head"] + d["body"]: - for cn, col in enumerate(row): - chars = len(col["display_value"]) - if chars > d["col_max_char"][cn]: - d["col_max_char"][cn] = chars - d["line_max_char"] = sum( - (chars if chars < max_colwidth else max_colwidth) - for chars in d["col_max_char"] - ) - - def char_refactor(value, display, max_cw, precision, align): - align_options = { - "r": "rjust", - "c": "center", - "l": "ljust", - "none": "__str__", - } - args = (max_cw,) if align != "none" else () - func = align_options[align] - if len(display) <= max_cw: - return getattr(display, func)(*args) - elif len(display) > max_cw: - if isinstance(value, (float, complex)): - return getattr(f"{{:.{precision}E}}".format(value), func)(*args) - elif isinstance(value, int): - return getattr(f"{value:.0E}", func)(*args) - else: - return display[: max_cw - 3] + "..." - - # refactor display values to satisfy consistent column width - for row in d["head"] + d["body"]: - for cn, col in enumerate(row): - col["display_value"] = char_refactor( - col["value"], - col["display_value"], - min(max_colwidth, d["col_max_char"][cn]), - precision, - align, - ) - def format( self, formatter: ExtFormatter | None = None, @@ -2039,18 +1976,3 @@ def _escape_latex(s): .replace("^", "\\textasciicircum ") .replace("ab2§=§8yz", "\\textbackslash ") ) - - -def _parse_string_row(row: list, spacer: str, max_line: int) -> str: - """ - Parse a row for jinja2 string template - """ - res = "" - len_res = 0 - for c in row: - if (len_res + len(c["display_value"])) > max_line: - res += "\n" - len_res = 0 - res += c["display_value"] + spacer - len_res += len(c["display_value"] + spacer) - return res + "\n" diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 235d94d30dc75..06aeb2b4e413c 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -1,7 +1,12 @@ -{% for r in head %}{% for c in r %} +{% for r in head %} +{% for c in r %}{% if c["is_visible"] %} {{ c["display_value"] }}{% if not loop.last %}{{ delimiter }}{% endif %} -{% endfor %}{% endfor %} -{% if hrules %} -{% for val in col_max_char %}{{ "-" * (val if val < max_colwidth else max_colwidth + delimiter|length) }}{% endfor %} -{% endif %} -{% for r in body %}{{ parse_row(r, delimiter, line_width) }}{% endfor %} +{% endif %}{% endfor %} + +{% endfor %} +{% for r in body %} +{% for c in r %}{% if c["is_visible"] %} +{{ c["display_value"] }}{% if not loop.last %}{{ delimiter }}{% endif %} +{% endif %}{% endfor %} + +{% endfor %} From 8e236a0753194d3293d82f87c7e98512013a5b2b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Nov 2021 19:54:50 +0100 Subject: [PATCH 38/44] basic tests --- .../tests/io/formats/style/test_to_string.py | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_string.py b/pandas/tests/io/formats/style/test_to_string.py index 8b6ba4fd77c6c..5b3e0079bd95c 100644 --- a/pandas/tests/io/formats/style/test_to_string.py +++ b/pandas/tests/io/formats/style/test_to_string.py @@ -1,12 +1,8 @@ from textwrap import dedent -import numpy as np import pytest -from pandas import ( - DataFrame, - MultiIndex, -) +from pandas import DataFrame pytest.importorskip("jinja2") from pandas.io.formats.style import Styler @@ -34,17 +30,13 @@ def test_basic_string(styler): assert result == expected -def test_comprehesive_string(): - midx = MultiIndex.from_product( - [["A", "B"], ["a", "b"], ["X", "Y"]], names=["zero", "one", "two"] - ) - cidx = MultiIndex.from_product( - [["a", "b"], ["C", "D"], ["V", "W"]], names=["zero", "one", "two"] +def test_string_delimiter(styler): + result = styler.to_string(delimiter=";") + expected = dedent( + """\ + ;A;B;C + 0;0;-0.61;ab + 1;1;-1.22;cd + """ ) - df = DataFrame(np.arange(64).reshape(8, 8), index=midx, columns=cidx) - styler = Styler(df) - styler.hide_index(level=0).hide_columns(level=1) - styler.hide_index([("A", "a", "X"), ("A", "b", "X")]) - styler.hide_columns([("a", "C", "W"), ("a", "D", "V")]) - result = styler.to_string() - assert result == 1 + assert result == expected From c318973c7c1a9bf6f6f86d6fc305d2531a213b1d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Nov 2021 20:00:31 +0100 Subject: [PATCH 39/44] whatsnew --- doc/source/whatsnew/v1.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index a593a03de5c25..19a5df77baca0 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -109,6 +109,7 @@ Styler - Bug where row trimming failed to reflect hidden rows (:issue:`43703`, :issue:`44247`) - Update and expand the export and use mechanics (:issue:`40675`) - New method :meth:`.Styler.hide` added and deprecates :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` (:issue:`43758`) + - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) Formerly Styler relied on ``display.html.use_mathjax``, which has now been replaced by ``styler.html.mathjax``. From 3580f04a1ca36ed75b4301c6644d93e21ce337e2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 30 Nov 2021 22:18:49 +0100 Subject: [PATCH 40/44] add to doc toc --- doc/source/reference/style.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index a739993e4d376..7d31b1b39b3f3 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -74,5 +74,6 @@ Style export and import Styler.to_html Styler.to_latex Styler.to_excel + Styler.to_string Styler.export Styler.use From db0181776287f5ffda7743be8b8f8dd082ae5ccb Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 1 Dec 2021 19:17:53 +0100 Subject: [PATCH 41/44] fix validation --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 8f72a2a8eca36..6f7fd066840af 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1094,7 +1094,7 @@ def to_string( Rows and columns may be reduced if the number of total elements is large. This value is set to ``pandas.options.styler.render.max_elements``, which is 262144 (18 bit browser rendering). - delimiter: string, default single space + delimiter : string, default single space The separator between data elements. Returns From 9145afc9e3cf986a7a803fc63be5b88d1cf40437 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 1 Dec 2021 19:18:53 +0100 Subject: [PATCH 42/44] fix doc build --- doc/source/reference/style.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 7d31b1b39b3f3..dd7e2fe7434cd 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -27,6 +27,7 @@ Styler properties Styler.template_html_style Styler.template_html_table Styler.template_latex + Styler.template_string Styler.loader Style application From c37fb18599f77bded102d7161a58b015e1fde91a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 7 Jan 2022 20:45:24 +0100 Subject: [PATCH 43/44] push to 1.5.0 --- doc/source/whatsnew/v1.4.0.rst | 1 - doc/source/whatsnew/v1.5.0.rst | 8 +++++--- pandas/io/formats/style.py | 6 +----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 48cb9d09d96af..3ee34a158902b 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -90,7 +90,6 @@ Styler - The keyword arguments ``level`` and ``names`` have been added to :meth:`.Styler.hide` (and implicitly to the deprecated methods :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns`) for additional control of visibility of MultiIndexes and of index names (:issue:`25475`, :issue:`43404`, :issue:`43346`) - The :meth:`.Styler.export` and :meth:`.Styler.use` have been updated to address all of the added functionality from v1.2.0 and v1.3.0 (:issue:`40675`) - Global options under the category ``pd.options.styler`` have been extended to configure default ``Styler`` properties which address formatting, encoding, and HTML and LaTeX rendering. Note that formerly ``Styler`` relied on ``display.html.use_mathjax``, which has now been replaced by ``styler.html.mathjax``. (:issue:`41395`) - - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) - Validation of certain keyword arguments, e.g. ``caption`` (:issue:`43368`) - Various bug fixes as recorded below diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 7f2a1a9305039..43aaf1068b6c9 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -14,10 +14,12 @@ including other versions of pandas. Enhancements ~~~~~~~~~~~~ -.. _whatsnew_150.enhancements.enhancement1: +.. _whatsnew_150.enhancements.styler: -enhancement1 -^^^^^^^^^^^^ +Styler +^^^^^^ + + - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) .. _whatsnew_150.enhancements.enhancement2: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2b62b23f5f066..6e556b99fdecc 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1086,7 +1086,7 @@ def to_string( """ Write Styler to a file, buffer or string in text format. - .. versionadded:: 1.4.0 + .. versionadded:: 1.5.0 Parameters ---------- @@ -1120,10 +1120,6 @@ def to_string( ------- str or None If `buf` is None, returns the result as a string. Otherwise returns `None`. - - See Also - -------- - DataFrame.to_html: Write a DataFrame to a file, buffer or string in HTML format. """ obj = self._copy(deepcopy=True) From 1672c7be7e50ed200e573054c84067daaf168437 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 8 Jan 2022 00:13:30 +0100 Subject: [PATCH 44/44] fix doc string --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6e556b99fdecc..266c5af47eead 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1113,7 +1113,7 @@ def to_string( Rows and columns may be reduced if the number of total elements is large. This value is set to ``pandas.options.styler.render.max_elements``, which is 262144 (18 bit browser rendering). - delimiter : string, default single space + delimiter : str, default single space The separator between data elements. Returns