1"""
2Module for applying conditional formatting to DataFrames and Series.
3"""
4from collections import defaultdict
5from contextlib import contextmanager
6import copy
7from functools import partial
8from itertools import product
9from typing import (
10    Any,
11    Callable,
12    DefaultDict,
13    Dict,
14    List,
15    Optional,
16    Sequence,
17    Tuple,
18    Union,
19)
20from uuid import uuid4
21
22import numpy as np
23
24from pandas._config import get_option
25
26from pandas._libs import lib
27from pandas._typing import Axis, FrameOrSeries, FrameOrSeriesUnion, Label
28from pandas.compat._optional import import_optional_dependency
29from pandas.util._decorators import doc
30
31from pandas.core.dtypes.common import is_float
32
33import pandas as pd
34from pandas.api.types import is_dict_like, is_list_like
35from pandas.core import generic
36import pandas.core.common as com
37from pandas.core.frame import DataFrame
38from pandas.core.generic import NDFrame
39from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice
40
41jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
42
43
44try:
45    from matplotlib import colors
46    import matplotlib.pyplot as plt
47
48    has_mpl = True
49except ImportError:
50    has_mpl = False
51    no_mpl_message = "{0} requires matplotlib."
52
53
54@contextmanager
55def _mpl(func: Callable):
56    if has_mpl:
57        yield plt, colors
58    else:
59        raise ImportError(no_mpl_message.format(func.__name__))
60
61
62class Styler:
63    """
64    Helps style a DataFrame or Series according to the data with HTML and CSS.
65
66    Parameters
67    ----------
68    data : Series or DataFrame
69        Data to be styled - either a Series or DataFrame.
70    precision : int
71        Precision to round floats to, defaults to pd.options.display.precision.
72    table_styles : list-like, default None
73        List of {selector: (attr, value)} dicts; see Notes.
74    uuid : str, default None
75        A unique identifier to avoid CSS collisions; generated automatically.
76    caption : str, default None
77        Caption to attach to the table.
78    table_attributes : str, default None
79        Items that show up in the opening ``<table>`` tag
80        in addition to automatic (by default) id.
81    cell_ids : bool, default True
82        If True, each cell will have an ``id`` attribute in their HTML tag.
83        The ``id`` takes the form ``T_<uuid>_row<num_row>_col<num_col>``
84        where ``<uuid>`` is the unique identifier, ``<num_row>`` is the row
85        number and ``<num_col>`` is the column number.
86    na_rep : str, optional
87        Representation for missing values.
88        If ``na_rep`` is None, no special formatting is applied.
89
90        .. versionadded:: 1.0.0
91
92    uuid_len : int, default 5
93        If ``uuid`` is not specified, the length of the ``uuid`` to randomly generate
94        expressed in hex characters, in range [0, 32].
95
96        .. versionadded:: 1.2.0
97
98    Attributes
99    ----------
100    env : Jinja2 jinja2.Environment
101    template : Jinja2 Template
102    loader : Jinja2 Loader
103
104    See Also
105    --------
106    DataFrame.style : Return a Styler object containing methods for building
107        a styled HTML representation for the DataFrame.
108
109    Notes
110    -----
111    Most styling will be done by passing style functions into
112    ``Styler.apply`` or ``Styler.applymap``. Style functions should
113    return values with strings containing CSS ``'attr: value'`` that will
114    be applied to the indicated cells.
115
116    If using in the Jupyter notebook, Styler has defined a ``_repr_html_``
117    to automatically render itself. Otherwise call Styler.render to get
118    the generated HTML.
119
120    CSS classes are attached to the generated HTML
121
122    * Index and Column names include ``index_name`` and ``level<k>``
123      where `k` is its level in a MultiIndex
124    * Index label cells include
125
126      * ``row_heading``
127      * ``row<n>`` where `n` is the numeric position of the row
128      * ``level<k>`` where `k` is the level in a MultiIndex
129
130    * Column label cells include
131      * ``col_heading``
132      * ``col<n>`` where `n` is the numeric position of the column
133      * ``level<k>`` where `k` is the level in a MultiIndex
134
135    * Blank cells include ``blank``
136    * Data cells include ``data``
137    """
138
139    loader = jinja2.PackageLoader("pandas", "io/formats/templates")
140    env = jinja2.Environment(loader=loader, trim_blocks=True)
141    template = env.get_template("html.tpl")
142
143    def __init__(
144        self,
145        data: FrameOrSeriesUnion,
146        precision: Optional[int] = None,
147        table_styles: Optional[List[Dict[str, List[Tuple[str, str]]]]] = None,
148        uuid: Optional[str] = None,
149        caption: Optional[str] = None,
150        table_attributes: Optional[str] = None,
151        cell_ids: bool = True,
152        na_rep: Optional[str] = None,
153        uuid_len: int = 5,
154    ):
155        self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list)
156        self._todo: List[Tuple[Callable, Tuple, Dict]] = []
157
158        if not isinstance(data, (pd.Series, pd.DataFrame)):
159            raise TypeError("``data`` must be a Series or DataFrame")
160        if data.ndim == 1:
161            data = data.to_frame()
162        if not data.index.is_unique or not data.columns.is_unique:
163            raise ValueError("style is not supported for non-unique indices.")
164
165        self.data = data
166        self.index = data.index
167        self.columns = data.columns
168
169        if not isinstance(uuid_len, int) or not uuid_len >= 0:
170            raise TypeError("``uuid_len`` must be an integer in range [0, 32].")
171        self.uuid_len = min(32, uuid_len)
172        self.uuid = (uuid or uuid4().hex[: self.uuid_len]) + "_"
173        self.table_styles = table_styles
174        self.caption = caption
175        if precision is None:
176            precision = get_option("display.precision")
177        self.precision = precision
178        self.table_attributes = table_attributes
179        self.hidden_index = False
180        self.hidden_columns: Sequence[int] = []
181        self.cell_ids = cell_ids
182        self.na_rep = na_rep
183
184        self.cell_context: Dict[str, Any] = {}
185
186        # display_funcs maps (row, col) -> formatting function
187
188        def default_display_func(x):
189            if self.na_rep is not None and pd.isna(x):
190                return self.na_rep
191            elif is_float(x):
192                display_format = f"{x:.{self.precision}f}"
193                return display_format
194            else:
195                return x
196
197        self._display_funcs: DefaultDict[
198            Tuple[int, int], Callable[[Any], str]
199        ] = defaultdict(lambda: default_display_func)
200
201    def _repr_html_(self) -> str:
202        """
203        Hooks into Jupyter notebook rich display system.
204        """
205        return self.render()
206
207    @doc(
208        NDFrame.to_excel,
209        klass="Styler",
210        storage_options=generic._shared_docs["storage_options"],
211    )
212    def to_excel(
213        self,
214        excel_writer,
215        sheet_name: str = "Sheet1",
216        na_rep: str = "",
217        float_format: Optional[str] = None,
218        columns: Optional[Sequence[Label]] = None,
219        header: Union[Sequence[Label], bool] = True,
220        index: bool = True,
221        index_label: Optional[Union[Label, Sequence[Label]]] = None,
222        startrow: int = 0,
223        startcol: int = 0,
224        engine: Optional[str] = None,
225        merge_cells: bool = True,
226        encoding: Optional[str] = None,
227        inf_rep: str = "inf",
228        verbose: bool = True,
229        freeze_panes: Optional[Tuple[int, int]] = None,
230    ) -> None:
231
232        from pandas.io.formats.excel import ExcelFormatter
233
234        formatter = ExcelFormatter(
235            self,
236            na_rep=na_rep,
237            cols=columns,
238            header=header,
239            float_format=float_format,
240            index=index,
241            index_label=index_label,
242            merge_cells=merge_cells,
243            inf_rep=inf_rep,
244        )
245        formatter.write(
246            excel_writer,
247            sheet_name=sheet_name,
248            startrow=startrow,
249            startcol=startcol,
250            freeze_panes=freeze_panes,
251            engine=engine,
252        )
253
254    def _translate(self):
255        """
256        Convert the DataFrame in `self.data` and the attrs from `_build_styles`
257        into a dictionary of {head, body, uuid, cellstyle}.
258        """
259        table_styles = self.table_styles or []
260        caption = self.caption
261        ctx = self.ctx
262        precision = self.precision
263        hidden_index = self.hidden_index
264        hidden_columns = self.hidden_columns
265        uuid = self.uuid
266        ROW_HEADING_CLASS = "row_heading"
267        COL_HEADING_CLASS = "col_heading"
268        INDEX_NAME_CLASS = "index_name"
269
270        DATA_CLASS = "data"
271        BLANK_CLASS = "blank"
272        BLANK_VALUE = ""
273
274        def format_attr(pair):
275            return f"{pair['key']}={pair['value']}"
276
277        # for sparsifying a MultiIndex
278        idx_lengths = _get_level_lengths(self.index)
279        col_lengths = _get_level_lengths(self.columns, hidden_columns)
280
281        cell_context = self.cell_context
282
283        n_rlvls = self.data.index.nlevels
284        n_clvls = self.data.columns.nlevels
285        rlabels = self.data.index.tolist()
286        clabels = self.data.columns.tolist()
287
288        if n_rlvls == 1:
289            rlabels = [[x] for x in rlabels]
290        if n_clvls == 1:
291            clabels = [[x] for x in clabels]
292        clabels = list(zip(*clabels))
293
294        cellstyle_map = defaultdict(list)
295        head = []
296
297        for r in range(n_clvls):
298            # Blank for Index columns...
299            row_es = [
300                {
301                    "type": "th",
302                    "value": BLANK_VALUE,
303                    "display_value": BLANK_VALUE,
304                    "is_visible": not hidden_index,
305                    "class": " ".join([BLANK_CLASS]),
306                }
307            ] * (n_rlvls - 1)
308
309            # ... except maybe the last for columns.names
310            name = self.data.columns.names[r]
311            cs = [
312                BLANK_CLASS if name is None else INDEX_NAME_CLASS,
313                f"level{r}",
314            ]
315            name = BLANK_VALUE if name is None else name
316            row_es.append(
317                {
318                    "type": "th",
319                    "value": name,
320                    "display_value": name,
321                    "class": " ".join(cs),
322                    "is_visible": not hidden_index,
323                }
324            )
325
326            if clabels:
327                for c, value in enumerate(clabels[r]):
328                    cs = [
329                        COL_HEADING_CLASS,
330                        f"level{r}",
331                        f"col{c}",
332                    ]
333                    cs.extend(
334                        cell_context.get("col_headings", {}).get(r, {}).get(c, [])
335                    )
336                    es = {
337                        "type": "th",
338                        "value": value,
339                        "display_value": value,
340                        "class": " ".join(cs),
341                        "is_visible": _is_visible(c, r, col_lengths),
342                    }
343                    colspan = col_lengths.get((r, c), 0)
344                    if colspan > 1:
345                        es["attributes"] = [
346                            format_attr({"key": "colspan", "value": f'"{colspan}"'})
347                        ]
348                    row_es.append(es)
349                head.append(row_es)
350
351        if (
352            self.data.index.names
353            and com.any_not_none(*self.data.index.names)
354            and not hidden_index
355        ):
356            index_header_row = []
357
358            for c, name in enumerate(self.data.index.names):
359                cs = [INDEX_NAME_CLASS, f"level{c}"]
360                name = "" if name is None else name
361                index_header_row.append(
362                    {"type": "th", "value": name, "class": " ".join(cs)}
363                )
364
365            index_header_row.extend(
366                [{"type": "th", "value": BLANK_VALUE, "class": " ".join([BLANK_CLASS])}]
367                * (len(clabels[0]) - len(hidden_columns))
368            )
369
370            head.append(index_header_row)
371
372        body = []
373        for r, idx in enumerate(self.data.index):
374            row_es = []
375            for c, value in enumerate(rlabels[r]):
376                rid = [
377                    ROW_HEADING_CLASS,
378                    f"level{c}",
379                    f"row{r}",
380                ]
381                es = {
382                    "type": "th",
383                    "is_visible": (_is_visible(r, c, idx_lengths) and not hidden_index),
384                    "value": value,
385                    "display_value": value,
386                    "id": "_".join(rid[1:]),
387                    "class": " ".join(rid),
388                }
389                rowspan = idx_lengths.get((c, r), 0)
390                if rowspan > 1:
391                    es["attributes"] = [
392                        format_attr({"key": "rowspan", "value": f'"{rowspan}"'})
393                    ]
394                row_es.append(es)
395
396            for c, col in enumerate(self.data.columns):
397                cs = [DATA_CLASS, f"row{r}", f"col{c}"]
398                cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
399                formatter = self._display_funcs[(r, c)]
400                value = self.data.iloc[r, c]
401                row_dict = {
402                    "type": "td",
403                    "value": value,
404                    "class": " ".join(cs),
405                    "display_value": formatter(value),
406                    "is_visible": (c not in hidden_columns),
407                }
408                # only add an id if the cell has a style
409                props = []
410                if self.cell_ids or (r, c) in ctx:
411                    row_dict["id"] = "_".join(cs[1:])
412                    for x in ctx[r, c]:
413                        # have to handle empty styles like ['']
414                        if x.count(":"):
415                            props.append(tuple(x.split(":")))
416                        else:
417                            props.append(("", ""))
418                row_es.append(row_dict)
419                cellstyle_map[tuple(props)].append(f"row{r}_col{c}")
420            body.append(row_es)
421
422        cellstyle = [
423            {"props": list(props), "selectors": selectors}
424            for props, selectors in cellstyle_map.items()
425        ]
426
427        table_attr = self.table_attributes
428        use_mathjax = get_option("display.html.use_mathjax")
429        if not use_mathjax:
430            table_attr = table_attr or ""
431            if 'class="' in table_attr:
432                table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
433            else:
434                table_attr += ' class="tex2jax_ignore"'
435
436        return {
437            "head": head,
438            "cellstyle": cellstyle,
439            "body": body,
440            "uuid": uuid,
441            "precision": precision,
442            "table_styles": table_styles,
443            "caption": caption,
444            "table_attributes": table_attr,
445        }
446
447    def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Styler":
448        """
449        Format the text display value of cells.
450
451        Parameters
452        ----------
453        formatter : str, callable, dict or None
454            If ``formatter`` is None, the default formatter is used.
455        subset : IndexSlice
456            An argument to ``DataFrame.loc`` that restricts which elements
457            ``formatter`` is applied to.
458        na_rep : str, optional
459            Representation for missing values.
460            If ``na_rep`` is None, no special formatting is applied.
461
462            .. versionadded:: 1.0.0
463
464        Returns
465        -------
466        self : Styler
467
468        Notes
469        -----
470        ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where
471        ``a`` is one of
472
473        - str: this will be wrapped in: ``a.format(x)``
474        - callable: called with the value of an individual cell
475
476        The default display value for numeric values is the "general" (``g``)
477        format with ``pd.options.display.precision`` precision.
478
479        Examples
480        --------
481        >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b'])
482        >>> df.style.format("{:.2%}")
483        >>> df['c'] = ['a', 'b', 'c', 'd']
484        >>> df.style.format({'c': str.upper})
485        """
486        if formatter is None:
487            assert self._display_funcs.default_factory is not None
488            formatter = self._display_funcs.default_factory()
489
490        if subset is None:
491            row_locs = range(len(self.data))
492            col_locs = range(len(self.data.columns))
493        else:
494            subset = non_reducing_slice(subset)
495            if len(subset) == 1:
496                subset = subset, self.data.columns
497
498            sub_df = self.data.loc[subset]
499            row_locs = self.data.index.get_indexer_for(sub_df.index)
500            col_locs = self.data.columns.get_indexer_for(sub_df.columns)
501
502        if is_dict_like(formatter):
503            for col, col_formatter in formatter.items():
504                # formatter must be callable, so '{}' are converted to lambdas
505                col_formatter = _maybe_wrap_formatter(col_formatter, na_rep)
506                col_num = self.data.columns.get_indexer_for([col])[0]
507
508                for row_num in row_locs:
509                    self._display_funcs[(row_num, col_num)] = col_formatter
510        else:
511            # single scalar to format all cells with
512            formatter = _maybe_wrap_formatter(formatter, na_rep)
513            locs = product(*(row_locs, col_locs))
514            for i, j in locs:
515                self._display_funcs[(i, j)] = formatter
516        return self
517
518    def set_td_classes(self, classes: DataFrame) -> "Styler":
519        """
520        Add string based CSS class names to data cells that will appear within the
521        `Styler` HTML result. These classes are added within specified `<td>` elements.
522
523        Parameters
524        ----------
525        classes : DataFrame
526            DataFrame containing strings that will be translated to CSS classes,
527            mapped by identical column and index values that must exist on the
528            underlying `Styler` data. None, NaN values, and empty strings will
529            be ignored and not affect the rendered HTML.
530
531        Returns
532        -------
533        self : Styler
534
535        Examples
536        --------
537        >>> df = pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"])
538        >>> classes = pd.DataFrame([
539        ...     ["min-val red", "", "blue"],
540        ...     ["red", None, "blue max-val"]
541        ... ], index=df.index, columns=df.columns)
542        >>> df.style.set_td_classes(classes)
543
544        Using `MultiIndex` columns and a `classes` `DataFrame` as a subset of the
545        underlying,
546
547        >>> df = pd.DataFrame([[1,2],[3,4]], index=["a", "b"],
548        ...     columns=[["level0", "level0"], ["level1a", "level1b"]])
549        >>> classes = pd.DataFrame(["min-val"], index=["a"],
550        ...     columns=[["level0"],["level1a"]])
551        >>> df.style.set_td_classes(classes)
552
553        Form of the output with new additional css classes,
554
555        >>> df = pd.DataFrame([[1]])
556        >>> css = pd.DataFrame(["other-class"])
557        >>> s = Styler(df, uuid="_", cell_ids=False).set_td_classes(css)
558        >>> s.hide_index().render()
559        '<style  type="text/css" ></style>'
560        '<table id="T__" >'
561        '  <thead>'
562        '    <tr><th class="col_heading level0 col0" >0</th></tr>'
563        '  </thead>'
564        '  <tbody>'
565        '    <tr><td  class="data row0 col0 other-class" >1</td></tr>'
566        '  </tbody>'
567        '</table>'
568        """
569        classes = classes.reindex_like(self.data)
570
571        mask = (classes.isna()) | (classes.eq(""))
572        self.cell_context["data"] = {
573            r: {c: [str(classes.iloc[r, c])]}
574            for r, rn in enumerate(classes.index)
575            for c, cn in enumerate(classes.columns)
576            if not mask.iloc[r, c]
577        }
578
579        return self
580
581    def render(self, **kwargs) -> str:
582        """
583        Render the built up styles to HTML.
584
585        Parameters
586        ----------
587        **kwargs
588            Any additional keyword arguments are passed
589            through to ``self.template.render``.
590            This is useful when you need to provide
591            additional variables for a custom template.
592
593        Returns
594        -------
595        rendered : str
596            The rendered HTML.
597
598        Notes
599        -----
600        ``Styler`` objects have defined the ``_repr_html_`` method
601        which automatically calls ``self.render()`` when it's the
602        last item in a Notebook cell. When calling ``Styler.render()``
603        directly, wrap the result in ``IPython.display.HTML`` to view
604        the rendered HTML in the notebook.
605
606        Pandas uses the following keys in render. Arguments passed
607        in ``**kwargs`` take precedence, so think carefully if you want
608        to override them:
609
610        * head
611        * cellstyle
612        * body
613        * uuid
614        * precision
615        * table_styles
616        * caption
617        * table_attributes
618        """
619        self._compute()
620        # TODO: namespace all the pandas keys
621        d = self._translate()
622        # filter out empty styles, every cell will have a class
623        # but the list of props may just be [['', '']].
624        # so we have the nested anys below
625        trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])]
626        d["cellstyle"] = trimmed
627        d.update(kwargs)
628        return self.template.render(**d)
629
630    def _update_ctx(self, attrs: DataFrame) -> None:
631        """
632        Update the state of the Styler.
633
634        Collects a mapping of {index_label: ['<property>: <value>']}.
635
636        Parameters
637        ----------
638        attrs : DataFrame
639            should contain strings of '<property>: <value>;<prop2>: <val2>'
640            Whitespace shouldn't matter and the final trailing ';' shouldn't
641            matter.
642        """
643        coli = {k: i for i, k in enumerate(self.columns)}
644        rowi = {k: i for i, k in enumerate(self.index)}
645        for jj in range(len(attrs.columns)):
646            cn = attrs.columns[jj]
647            j = coli[cn]
648            for rn, c in attrs[[cn]].itertuples():
649                if not c:
650                    continue
651                c = c.rstrip(";")
652                if not c:
653                    continue
654                i = rowi[rn]
655                for pair in c.split(";"):
656                    self.ctx[(i, j)].append(pair)
657
658    def _copy(self, deepcopy: bool = False) -> "Styler":
659        styler = Styler(
660            self.data,
661            precision=self.precision,
662            caption=self.caption,
663            uuid=self.uuid,
664            table_styles=self.table_styles,
665            na_rep=self.na_rep,
666        )
667        if deepcopy:
668            styler.ctx = copy.deepcopy(self.ctx)
669            styler._todo = copy.deepcopy(self._todo)
670        else:
671            styler.ctx = self.ctx
672            styler._todo = self._todo
673        return styler
674
675    def __copy__(self) -> "Styler":
676        """
677        Deep copy by default.
678        """
679        return self._copy(deepcopy=False)
680
681    def __deepcopy__(self, memo) -> "Styler":
682        return self._copy(deepcopy=True)
683
684    def clear(self) -> None:
685        """
686        Reset the styler, removing any previously applied styles.
687
688        Returns None.
689        """
690        self.ctx.clear()
691        self.cell_context = {}
692        self._todo = []
693
694    def _compute(self):
695        """
696        Execute the style functions built up in `self._todo`.
697
698        Relies on the conventions that all style functions go through
699        .apply or .applymap. The append styles to apply as tuples of
700
701        (application method, *args, **kwargs)
702        """
703        r = self
704        for func, args, kwargs in self._todo:
705            r = func(self)(*args, **kwargs)
706        return r
707
708    def _apply(
709        self,
710        func: Callable[..., "Styler"],
711        axis: Optional[Axis] = 0,
712        subset=None,
713        **kwargs,
714    ) -> "Styler":
715        subset = slice(None) if subset is None else subset
716        subset = non_reducing_slice(subset)
717        data = self.data.loc[subset]
718        if axis is not None:
719            result = data.apply(func, axis=axis, result_type="expand", **kwargs)
720            result.columns = data.columns
721        else:
722            result = func(data, **kwargs)
723            if not isinstance(result, pd.DataFrame):
724                raise TypeError(
725                    f"Function {repr(func)} must return a DataFrame when "
726                    f"passed to `Styler.apply` with axis=None"
727                )
728            if not (
729                result.index.equals(data.index) and result.columns.equals(data.columns)
730            ):
731                raise ValueError(
732                    f"Result of {repr(func)} must have identical "
733                    f"index and columns as the input"
734                )
735
736        result_shape = result.shape
737        expected_shape = self.data.loc[subset].shape
738        if result_shape != expected_shape:
739            raise ValueError(
740                f"Function {repr(func)} returned the wrong shape.\n"
741                f"Result has shape: {result.shape}\n"
742                f"Expected shape:   {expected_shape}"
743            )
744        self._update_ctx(result)
745        return self
746
747    def apply(
748        self,
749        func: Callable[..., "Styler"],
750        axis: Optional[Axis] = 0,
751        subset=None,
752        **kwargs,
753    ) -> "Styler":
754        """
755        Apply a function column-wise, row-wise, or table-wise.
756
757        Updates the HTML representation with the result.
758
759        Parameters
760        ----------
761        func : function
762            ``func`` should take a Series or DataFrame (depending
763            on ``axis``), and return an object with the same shape.
764            Must return a DataFrame with identical index and
765            column labels when ``axis=None``.
766        axis : {0 or 'index', 1 or 'columns', None}, default 0
767            Apply to each column (``axis=0`` or ``'index'``), to each row
768            (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
769            with ``axis=None``.
770        subset : IndexSlice
771            A valid indexer to limit ``data`` to *before* applying the
772            function. Consider using a pandas.IndexSlice.
773        **kwargs : dict
774            Pass along to ``func``.
775
776        Returns
777        -------
778        self : Styler
779
780        Notes
781        -----
782        The output shape of ``func`` should match the input, i.e. if
783        ``x`` is the input row, column, or table (depending on ``axis``),
784        then ``func(x).shape == x.shape`` should be true.
785
786        This is similar to ``DataFrame.apply``, except that ``axis=None``
787        applies the function to the entire DataFrame at once,
788        rather than column-wise or row-wise.
789
790        Examples
791        --------
792        >>> def highlight_max(x):
793        ...     return ['background-color: yellow' if v == x.max() else ''
794                        for v in x]
795        ...
796        >>> df = pd.DataFrame(np.random.randn(5, 2))
797        >>> df.style.apply(highlight_max)
798        """
799        self._todo.append(
800            (lambda instance: getattr(instance, "_apply"), (func, axis, subset), kwargs)
801        )
802        return self
803
804    def _applymap(self, func: Callable, subset=None, **kwargs) -> "Styler":
805        func = partial(func, **kwargs)  # applymap doesn't take kwargs?
806        if subset is None:
807            subset = pd.IndexSlice[:]
808        subset = non_reducing_slice(subset)
809        result = self.data.loc[subset].applymap(func)
810        self._update_ctx(result)
811        return self
812
813    def applymap(self, func: Callable, subset=None, **kwargs) -> "Styler":
814        """
815        Apply a function elementwise.
816
817        Updates the HTML representation with the result.
818
819        Parameters
820        ----------
821        func : function
822            ``func`` should take a scalar and return a scalar.
823        subset : IndexSlice
824            A valid indexer to limit ``data`` to *before* applying the
825            function. Consider using a pandas.IndexSlice.
826        **kwargs : dict
827            Pass along to ``func``.
828
829        Returns
830        -------
831        self : Styler
832
833        See Also
834        --------
835        Styler.where: Updates the HTML representation with a style which is
836            selected in accordance with the return value of a function.
837        """
838        self._todo.append(
839            (lambda instance: getattr(instance, "_applymap"), (func, subset), kwargs)
840        )
841        return self
842
843    def where(
844        self,
845        cond: Callable,
846        value: str,
847        other: Optional[str] = None,
848        subset=None,
849        **kwargs,
850    ) -> "Styler":
851        """
852        Apply a function elementwise.
853
854        Updates the HTML representation with a style which is
855        selected in accordance with the return value of a function.
856
857        Parameters
858        ----------
859        cond : callable
860            ``cond`` should take a scalar and return a boolean.
861        value : str
862            Applied when ``cond`` returns true.
863        other : str
864            Applied when ``cond`` returns false.
865        subset : IndexSlice
866            A valid indexer to limit ``data`` to *before* applying the
867            function. Consider using a pandas.IndexSlice.
868        **kwargs : dict
869            Pass along to ``cond``.
870
871        Returns
872        -------
873        self : Styler
874
875        See Also
876        --------
877        Styler.applymap: Updates the HTML representation with the result.
878        """
879        if other is None:
880            other = ""
881
882        return self.applymap(
883            lambda val: value if cond(val) else other, subset=subset, **kwargs
884        )
885
886    def set_precision(self, precision: int) -> "Styler":
887        """
888        Set the precision used to render.
889
890        Parameters
891        ----------
892        precision : int
893
894        Returns
895        -------
896        self : Styler
897        """
898        self.precision = precision
899        return self
900
901    def set_table_attributes(self, attributes: str) -> "Styler":
902        """
903        Set the table attributes.
904
905        These are the items that show up in the opening ``<table>`` tag
906        in addition to automatic (by default) id.
907
908        Parameters
909        ----------
910        attributes : str
911
912        Returns
913        -------
914        self : Styler
915
916        Examples
917        --------
918        >>> df = pd.DataFrame(np.random.randn(10, 4))
919        >>> df.style.set_table_attributes('class="pure-table"')
920        # ... <table class="pure-table"> ...
921        """
922        self.table_attributes = attributes
923        return self
924
925    def export(self) -> List[Tuple[Callable, Tuple, Dict]]:
926        """
927        Export the styles to applied to the current Styler.
928
929        Can be applied to a second style with ``Styler.use``.
930
931        Returns
932        -------
933        styles : list
934
935        See Also
936        --------
937        Styler.use: Set the styles on the current Styler.
938        """
939        return self._todo
940
941    def use(self, styles: List[Tuple[Callable, Tuple, Dict]]) -> "Styler":
942        """
943        Set the styles on the current Styler.
944
945        Possibly uses styles from ``Styler.export``.
946
947        Parameters
948        ----------
949        styles : list
950            List of style functions.
951
952        Returns
953        -------
954        self : Styler
955
956        See Also
957        --------
958        Styler.export : Export the styles to applied to the current Styler.
959        """
960        self._todo.extend(styles)
961        return self
962
963    def set_uuid(self, uuid: str) -> "Styler":
964        """
965        Set the uuid for a Styler.
966
967        Parameters
968        ----------
969        uuid : str
970
971        Returns
972        -------
973        self : Styler
974        """
975        self.uuid = uuid
976        return self
977
978    def set_caption(self, caption: str) -> "Styler":
979        """
980        Set the caption on a Styler.
981
982        Parameters
983        ----------
984        caption : str
985
986        Returns
987        -------
988        self : Styler
989        """
990        self.caption = caption
991        return self
992
993    def set_table_styles(self, table_styles, axis=0, overwrite=True) -> "Styler":
994        """
995        Set the table styles on a Styler.
996
997        These are placed in a ``<style>`` tag before the generated HTML table.
998
999        This function can be used to style the entire table, columns, rows or
1000        specific HTML selectors.
1001
1002        Parameters
1003        ----------
1004        table_styles : list or dict
1005            If supplying a list, each individual table_style should be a
1006            dictionary with ``selector`` and ``props`` keys. ``selector``
1007            should be a CSS selector that the style will be applied to
1008            (automatically prefixed by the table's UUID) and ``props``
1009            should be a list of tuples with ``(attribute, value)``.
1010            If supplying a dict, the dict keys should correspond to
1011            column names or index values, depending upon the specified
1012            `axis` argument. These will be mapped to row or col CSS
1013            selectors. MultiIndex values as dict keys should be
1014            in their respective tuple form. The dict values should be
1015            a list as specified in the form with CSS selectors and
1016            props that will be applied to the specified row or column.
1017
1018            .. versionchanged:: 1.2.0
1019
1020        axis : {0 or 'index', 1 or 'columns', None}, default 0
1021            Apply to each column (``axis=0`` or ``'index'``), to each row
1022            (``axis=1`` or ``'columns'``). Only used if `table_styles` is
1023            dict.
1024
1025            .. versionadded:: 1.2.0
1026
1027        overwrite : boolean, default True
1028            Styles are replaced if `True`, or extended if `False`. CSS
1029            rules are preserved so most recent styles set will dominate
1030            if selectors intersect.
1031
1032            .. versionadded:: 1.2.0
1033
1034        Returns
1035        -------
1036        self : Styler
1037
1038        Examples
1039        --------
1040        >>> df = pd.DataFrame(np.random.randn(10, 4),
1041        ...                   columns=['A', 'B', 'C', 'D'])
1042        >>> df.style.set_table_styles(
1043        ...     [{'selector': 'tr:hover',
1044        ...       'props': [('background-color', 'yellow')]}]
1045        ... )
1046
1047        Adding column styling by name
1048
1049        >>> df.style.set_table_styles({
1050        ...     'A': [{'selector': '',
1051        ...            'props': [('color', 'red')]}],
1052        ...     'B': [{'selector': 'td',
1053        ...            'props': [('color', 'blue')]}]
1054        ... }, overwrite=False)
1055
1056        Adding row styling
1057
1058        >>> df.style.set_table_styles({
1059        ...     0: [{'selector': 'td:hover',
1060        ...          'props': [('font-size', '25px')]}]
1061        ... }, axis=1, overwrite=False)
1062        """
1063        if is_dict_like(table_styles):
1064            if axis in [0, "index"]:
1065                obj, idf = self.data.columns, ".col"
1066            else:
1067                obj, idf = self.data.index, ".row"
1068
1069            table_styles = [
1070                {
1071                    "selector": s["selector"] + idf + str(obj.get_loc(key)),
1072                    "props": s["props"],
1073                }
1074                for key, styles in table_styles.items()
1075                for s in styles
1076            ]
1077
1078        if not overwrite and self.table_styles is not None:
1079            self.table_styles.extend(table_styles)
1080        else:
1081            self.table_styles = table_styles
1082        return self
1083
1084    def set_na_rep(self, na_rep: str) -> "Styler":
1085        """
1086        Set the missing data representation on a Styler.
1087
1088        .. versionadded:: 1.0.0
1089
1090        Parameters
1091        ----------
1092        na_rep : str
1093
1094        Returns
1095        -------
1096        self : Styler
1097        """
1098        self.na_rep = na_rep
1099        return self
1100
1101    def hide_index(self) -> "Styler":
1102        """
1103        Hide any indices from rendering.
1104
1105        Returns
1106        -------
1107        self : Styler
1108        """
1109        self.hidden_index = True
1110        return self
1111
1112    def hide_columns(self, subset) -> "Styler":
1113        """
1114        Hide columns from rendering.
1115
1116        Parameters
1117        ----------
1118        subset : IndexSlice
1119            An argument to ``DataFrame.loc`` that identifies which columns
1120            are hidden.
1121
1122        Returns
1123        -------
1124        self : Styler
1125        """
1126        subset = non_reducing_slice(subset)
1127        hidden_df = self.data.loc[subset]
1128        self.hidden_columns = self.columns.get_indexer_for(hidden_df.columns)
1129        return self
1130
1131    # -----------------------------------------------------------------------
1132    # A collection of "builtin" styles
1133    # -----------------------------------------------------------------------
1134
1135    @staticmethod
1136    def _highlight_null(v, null_color: str) -> str:
1137        return f"background-color: {null_color}" if pd.isna(v) else ""
1138
1139    def highlight_null(
1140        self,
1141        null_color: str = "red",
1142        subset: Optional[Union[Label, Sequence[Label]]] = None,
1143    ) -> "Styler":
1144        """
1145        Shade the background ``null_color`` for missing values.
1146
1147        Parameters
1148        ----------
1149        null_color : str, default 'red'
1150        subset : label or list of labels, default None
1151            A valid slice for ``data`` to limit the style application to.
1152
1153            .. versionadded:: 1.1.0
1154
1155        Returns
1156        -------
1157        self : Styler
1158        """
1159        self.applymap(self._highlight_null, null_color=null_color, subset=subset)
1160        return self
1161
1162    def background_gradient(
1163        self,
1164        cmap="PuBu",
1165        low: float = 0,
1166        high: float = 0,
1167        axis: Optional[Axis] = 0,
1168        subset=None,
1169        text_color_threshold: float = 0.408,
1170        vmin: Optional[float] = None,
1171        vmax: Optional[float] = None,
1172    ) -> "Styler":
1173        """
1174        Color the background in a gradient style.
1175
1176        The background color is determined according
1177        to the data in each column (optionally row). Requires matplotlib.
1178
1179        Parameters
1180        ----------
1181        cmap : str or colormap
1182            Matplotlib colormap.
1183        low : float
1184            Compress the range by the low.
1185        high : float
1186            Compress the range by the high.
1187        axis : {0 or 'index', 1 or 'columns', None}, default 0
1188            Apply to each column (``axis=0`` or ``'index'``), to each row
1189            (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
1190            with ``axis=None``.
1191        subset : IndexSlice
1192            A valid slice for ``data`` to limit the style application to.
1193        text_color_threshold : float or int
1194            Luminance threshold for determining text color. Facilitates text
1195            visibility across varying background colors. From 0 to 1.
1196            0 = all text is dark colored, 1 = all text is light colored.
1197
1198            .. versionadded:: 0.24.0
1199
1200        vmin : float, optional
1201            Minimum data value that corresponds to colormap minimum value.
1202            When None (default): the minimum value of the data will be used.
1203
1204            .. versionadded:: 1.0.0
1205
1206        vmax : float, optional
1207            Maximum data value that corresponds to colormap maximum value.
1208            When None (default): the maximum value of the data will be used.
1209
1210            .. versionadded:: 1.0.0
1211
1212        Returns
1213        -------
1214        self : Styler
1215
1216        Raises
1217        ------
1218        ValueError
1219            If ``text_color_threshold`` is not a value from 0 to 1.
1220
1221        Notes
1222        -----
1223        Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the
1224        text legible by not using the entire range of the color map. The range
1225        of the data is extended by ``low * (x.max() - x.min())`` and ``high *
1226        (x.max() - x.min())`` before normalizing.
1227        """
1228        subset = maybe_numeric_slice(self.data, subset)
1229        subset = non_reducing_slice(subset)
1230        self.apply(
1231            self._background_gradient,
1232            cmap=cmap,
1233            subset=subset,
1234            axis=axis,
1235            low=low,
1236            high=high,
1237            text_color_threshold=text_color_threshold,
1238            vmin=vmin,
1239            vmax=vmax,
1240        )
1241        return self
1242
1243    @staticmethod
1244    def _background_gradient(
1245        s,
1246        cmap="PuBu",
1247        low: float = 0,
1248        high: float = 0,
1249        text_color_threshold: float = 0.408,
1250        vmin: Optional[float] = None,
1251        vmax: Optional[float] = None,
1252    ):
1253        """
1254        Color background in a range according to the data.
1255        """
1256        if (
1257            not isinstance(text_color_threshold, (float, int))
1258            or not 0 <= text_color_threshold <= 1
1259        ):
1260            msg = "`text_color_threshold` must be a value from 0 to 1."
1261            raise ValueError(msg)
1262
1263        with _mpl(Styler.background_gradient) as (plt, colors):
1264            smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
1265            smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
1266            rng = smax - smin
1267            # extend lower / upper bounds, compresses color range
1268            norm = colors.Normalize(smin - (rng * low), smax + (rng * high))
1269            # matplotlib colors.Normalize modifies inplace?
1270            # https://github.com/matplotlib/matplotlib/issues/5427
1271            rgbas = plt.cm.get_cmap(cmap)(norm(s.to_numpy(dtype=float)))
1272
1273            def relative_luminance(rgba) -> float:
1274                """
1275                Calculate relative luminance of a color.
1276
1277                The calculation adheres to the W3C standards
1278                (https://www.w3.org/WAI/GL/wiki/Relative_luminance)
1279
1280                Parameters
1281                ----------
1282                color : rgb or rgba tuple
1283
1284                Returns
1285                -------
1286                float
1287                    The relative luminance as a value from 0 to 1
1288                """
1289                r, g, b = (
1290                    x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055 ** 2.4)
1291                    for x in rgba[:3]
1292                )
1293                return 0.2126 * r + 0.7152 * g + 0.0722 * b
1294
1295            def css(rgba) -> str:
1296                dark = relative_luminance(rgba) < text_color_threshold
1297                text_color = "#f1f1f1" if dark else "#000000"
1298                return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
1299
1300            if s.ndim == 1:
1301                return [css(rgba) for rgba in rgbas]
1302            else:
1303                return pd.DataFrame(
1304                    [[css(rgba) for rgba in row] for row in rgbas],
1305                    index=s.index,
1306                    columns=s.columns,
1307                )
1308
1309    def set_properties(self, subset=None, **kwargs) -> "Styler":
1310        """
1311        Method to set one or more non-data dependent properties or each cell.
1312
1313        Parameters
1314        ----------
1315        subset : IndexSlice
1316            A valid slice for ``data`` to limit the style application to.
1317        **kwargs : dict
1318            A dictionary of property, value pairs to be set for each cell.
1319
1320        Returns
1321        -------
1322        self : Styler
1323
1324        Examples
1325        --------
1326        >>> df = pd.DataFrame(np.random.randn(10, 4))
1327        >>> df.style.set_properties(color="white", align="right")
1328        >>> df.style.set_properties(**{'background-color': 'yellow'})
1329        """
1330        values = ";".join(f"{p}: {v}" for p, v in kwargs.items())
1331        f = lambda x: values
1332        return self.applymap(f, subset=subset)
1333
1334    @staticmethod
1335    def _bar(
1336        s,
1337        align: str,
1338        colors: List[str],
1339        width: float = 100,
1340        vmin: Optional[float] = None,
1341        vmax: Optional[float] = None,
1342    ):
1343        """
1344        Draw bar chart in dataframe cells.
1345        """
1346        # Get input value range.
1347        smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
1348        smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
1349        if align == "mid":
1350            smin = min(0, smin)
1351            smax = max(0, smax)
1352        elif align == "zero":
1353            # For "zero" mode, we want the range to be symmetrical around zero.
1354            smax = max(abs(smin), abs(smax))
1355            smin = -smax
1356        # Transform to percent-range of linear-gradient
1357        normed = width * (s.to_numpy(dtype=float) - smin) / (smax - smin + 1e-12)
1358        zero = -width * smin / (smax - smin + 1e-12)
1359
1360        def css_bar(start: float, end: float, color: str) -> str:
1361            """
1362            Generate CSS code to draw a bar from start to end.
1363            """
1364            css = "width: 10em; height: 80%;"
1365            if end > start:
1366                css += "background: linear-gradient(90deg,"
1367                if start > 0:
1368                    css += f" transparent {start:.1f}%, {color} {start:.1f}%, "
1369                e = min(end, width)
1370                css += f"{color} {e:.1f}%, transparent {e:.1f}%)"
1371            return css
1372
1373        def css(x):
1374            if pd.isna(x):
1375                return ""
1376
1377            # avoid deprecated indexing `colors[x > zero]`
1378            color = colors[1] if x > zero else colors[0]
1379
1380            if align == "left":
1381                return css_bar(0, x, color)
1382            else:
1383                return css_bar(min(x, zero), max(x, zero), color)
1384
1385        if s.ndim == 1:
1386            return [css(x) for x in normed]
1387        else:
1388            return pd.DataFrame(
1389                [[css(x) for x in row] for row in normed],
1390                index=s.index,
1391                columns=s.columns,
1392            )
1393
1394    def bar(
1395        self,
1396        subset=None,
1397        axis: Optional[Axis] = 0,
1398        color="#d65f5f",
1399        width: float = 100,
1400        align: str = "left",
1401        vmin: Optional[float] = None,
1402        vmax: Optional[float] = None,
1403    ) -> "Styler":
1404        """
1405        Draw bar chart in the cell backgrounds.
1406
1407        Parameters
1408        ----------
1409        subset : IndexSlice, optional
1410            A valid slice for `data` to limit the style application to.
1411        axis : {0 or 'index', 1 or 'columns', None}, default 0
1412            Apply to each column (``axis=0`` or ``'index'``), to each row
1413            (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
1414            with ``axis=None``.
1415        color : str or 2-tuple/list
1416            If a str is passed, the color is the same for both
1417            negative and positive numbers. If 2-tuple/list is used, the
1418            first element is the color_negative and the second is the
1419            color_positive (eg: ['#d65f5f', '#5fba7d']).
1420        width : float, default 100
1421            A number between 0 or 100. The largest value will cover `width`
1422            percent of the cell's width.
1423        align : {'left', 'zero',' mid'}, default 'left'
1424            How to align the bars with the cells.
1425
1426            - 'left' : the min value starts at the left of the cell.
1427            - 'zero' : a value of zero is located at the center of the cell.
1428            - 'mid' : the center of the cell is at (max-min)/2, or
1429              if values are all negative (positive) the zero is aligned
1430              at the right (left) of the cell.
1431        vmin : float, optional
1432            Minimum bar value, defining the left hand limit
1433            of the bar drawing range, lower values are clipped to `vmin`.
1434            When None (default): the minimum value of the data will be used.
1435
1436            .. versionadded:: 0.24.0
1437
1438        vmax : float, optional
1439            Maximum bar value, defining the right hand limit
1440            of the bar drawing range, higher values are clipped to `vmax`.
1441            When None (default): the maximum value of the data will be used.
1442
1443            .. versionadded:: 0.24.0
1444
1445        Returns
1446        -------
1447        self : Styler
1448        """
1449        if align not in ("left", "zero", "mid"):
1450            raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
1451
1452        if not (is_list_like(color)):
1453            color = [color, color]
1454        elif len(color) == 1:
1455            color = [color[0], color[0]]
1456        elif len(color) > 2:
1457            raise ValueError(
1458                "`color` must be string or a list-like "
1459                "of length 2: [`color_neg`, `color_pos`] "
1460                "(eg: color=['#d65f5f', '#5fba7d'])"
1461            )
1462
1463        subset = maybe_numeric_slice(self.data, subset)
1464        subset = non_reducing_slice(subset)
1465        self.apply(
1466            self._bar,
1467            subset=subset,
1468            axis=axis,
1469            align=align,
1470            colors=color,
1471            width=width,
1472            vmin=vmin,
1473            vmax=vmax,
1474        )
1475
1476        return self
1477
1478    def highlight_max(
1479        self, subset=None, color: str = "yellow", axis: Optional[Axis] = 0
1480    ) -> "Styler":
1481        """
1482        Highlight the maximum by shading the background.
1483
1484        Parameters
1485        ----------
1486        subset : IndexSlice, default None
1487            A valid slice for ``data`` to limit the style application to.
1488        color : str, default 'yellow'
1489        axis : {0 or 'index', 1 or 'columns', None}, default 0
1490            Apply to each column (``axis=0`` or ``'index'``), to each row
1491            (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
1492            with ``axis=None``.
1493
1494        Returns
1495        -------
1496        self : Styler
1497        """
1498        return self._highlight_handler(subset=subset, color=color, axis=axis, max_=True)
1499
1500    def highlight_min(
1501        self, subset=None, color: str = "yellow", axis: Optional[Axis] = 0
1502    ) -> "Styler":
1503        """
1504        Highlight the minimum by shading the background.
1505
1506        Parameters
1507        ----------
1508        subset : IndexSlice, default None
1509            A valid slice for ``data`` to limit the style application to.
1510        color : str, default 'yellow'
1511        axis : {0 or 'index', 1 or 'columns', None}, default 0
1512            Apply to each column (``axis=0`` or ``'index'``), to each row
1513            (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
1514            with ``axis=None``.
1515
1516        Returns
1517        -------
1518        self : Styler
1519        """
1520        return self._highlight_handler(
1521            subset=subset, color=color, axis=axis, max_=False
1522        )
1523
1524    def _highlight_handler(
1525        self,
1526        subset=None,
1527        color: str = "yellow",
1528        axis: Optional[Axis] = None,
1529        max_: bool = True,
1530    ) -> "Styler":
1531        subset = non_reducing_slice(maybe_numeric_slice(self.data, subset))
1532        self.apply(
1533            self._highlight_extrema, color=color, axis=axis, subset=subset, max_=max_
1534        )
1535        return self
1536
1537    @staticmethod
1538    def _highlight_extrema(
1539        data: FrameOrSeries, color: str = "yellow", max_: bool = True
1540    ):
1541        """
1542        Highlight the min or max in a Series or DataFrame.
1543        """
1544        attr = f"background-color: {color}"
1545
1546        if max_:
1547            extrema = data == np.nanmax(data.to_numpy())
1548        else:
1549            extrema = data == np.nanmin(data.to_numpy())
1550
1551        if data.ndim == 1:  # Series from .apply
1552            return [attr if v else "" for v in extrema]
1553        else:  # DataFrame from .tee
1554            return pd.DataFrame(
1555                np.where(extrema, attr, ""), index=data.index, columns=data.columns
1556            )
1557
1558    @classmethod
1559    def from_custom_template(cls, searchpath, name):
1560        """
1561        Factory function for creating a subclass of ``Styler``.
1562
1563        Uses a custom template and Jinja environment.
1564
1565        Parameters
1566        ----------
1567        searchpath : str or list
1568            Path or paths of directories containing the templates.
1569        name : str
1570            Name of your custom template to use for rendering.
1571
1572        Returns
1573        -------
1574        MyStyler : subclass of Styler
1575            Has the correct ``env`` and ``template`` class attributes set.
1576        """
1577        loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(searchpath), cls.loader])
1578
1579        # mypy doesnt like dynamically-defined class
1580        # error: Variable "cls" is not valid as a type  [valid-type]
1581        # error: Invalid base class "cls"  [misc]
1582        class MyStyler(cls):  # type:ignore[valid-type,misc]
1583            env = jinja2.Environment(loader=loader)
1584            template = env.get_template(name)
1585
1586        return MyStyler
1587
1588    def pipe(self, func: Callable, *args, **kwargs):
1589        """
1590        Apply ``func(self, *args, **kwargs)``, and return the result.
1591
1592        .. versionadded:: 0.24.0
1593
1594        Parameters
1595        ----------
1596        func : function
1597            Function to apply to the Styler.  Alternatively, a
1598            ``(callable, keyword)`` tuple where ``keyword`` is a string
1599            indicating the keyword of ``callable`` that expects the Styler.
1600        *args : optional
1601            Arguments passed to `func`.
1602        **kwargs : optional
1603            A dictionary of keyword arguments passed into ``func``.
1604
1605        Returns
1606        -------
1607        object :
1608            The value returned by ``func``.
1609
1610        See Also
1611        --------
1612        DataFrame.pipe : Analogous method for DataFrame.
1613        Styler.apply : Apply a function row-wise, column-wise, or table-wise to
1614            modify the dataframe's styling.
1615
1616        Notes
1617        -----
1618        Like :meth:`DataFrame.pipe`, this method can simplify the
1619        application of several user-defined functions to a styler.  Instead
1620        of writing:
1621
1622        .. code-block:: python
1623
1624            f(g(df.style.set_precision(3), arg1=a), arg2=b, arg3=c)
1625
1626        users can write:
1627
1628        .. code-block:: python
1629
1630            (df.style.set_precision(3)
1631               .pipe(g, arg1=a)
1632               .pipe(f, arg2=b, arg3=c))
1633
1634        In particular, this allows users to define functions that take a
1635        styler object, along with other parameters, and return the styler after
1636        making styling changes (such as calling :meth:`Styler.apply` or
1637        :meth:`Styler.set_properties`).  Using ``.pipe``, these user-defined
1638        style "transformations" can be interleaved with calls to the built-in
1639        Styler interface.
1640
1641        Examples
1642        --------
1643        >>> def format_conversion(styler):
1644        ...     return (styler.set_properties(**{'text-align': 'right'})
1645        ...                   .format({'conversion': '{:.1%}'}))
1646
1647        The user-defined ``format_conversion`` function above can be called
1648        within a sequence of other style modifications:
1649
1650        >>> df = pd.DataFrame({'trial': list(range(5)),
1651        ...                    'conversion': [0.75, 0.85, np.nan, 0.7, 0.72]})
1652        >>> (df.style
1653        ...    .highlight_min(subset=['conversion'], color='yellow')
1654        ...    .pipe(format_conversion)
1655        ...    .set_caption("Results with minimum conversion highlighted."))
1656        """
1657        return com.pipe(self, func, *args, **kwargs)
1658
1659
1660def _is_visible(idx_row, idx_col, lengths) -> bool:
1661    """
1662    Index -> {(idx_row, idx_col): bool}).
1663    """
1664    return (idx_col, idx_row) in lengths
1665
1666
1667def _get_level_lengths(index, hidden_elements=None):
1668    """
1669    Given an index, find the level length for each element.
1670
1671    Optional argument is a list of index positions which
1672    should not be visible.
1673
1674    Result is a dictionary of (level, initial_position): span
1675    """
1676    if isinstance(index, pd.MultiIndex):
1677        levels = index.format(sparsify=lib.no_default, adjoin=False)
1678    else:
1679        levels = index.format()
1680
1681    if hidden_elements is None:
1682        hidden_elements = []
1683
1684    lengths = {}
1685    if index.nlevels == 1:
1686        for i, value in enumerate(levels):
1687            if i not in hidden_elements:
1688                lengths[(0, i)] = 1
1689        return lengths
1690
1691    for i, lvl in enumerate(levels):
1692        for j, row in enumerate(lvl):
1693            if not get_option("display.multi_sparse"):
1694                lengths[(i, j)] = 1
1695            elif (row is not lib.no_default) and (j not in hidden_elements):
1696                last_label = j
1697                lengths[(i, last_label)] = 1
1698            elif row is not lib.no_default:
1699                # even if its hidden, keep track of it in case
1700                # length >1 and later elements are visible
1701                last_label = j
1702                lengths[(i, last_label)] = 0
1703            elif j not in hidden_elements:
1704                lengths[(i, last_label)] += 1
1705
1706    non_zero_lengths = {
1707        element: length for element, length in lengths.items() if length >= 1
1708    }
1709
1710    return non_zero_lengths
1711
1712
1713def _maybe_wrap_formatter(
1714    formatter: Union[Callable, str], na_rep: Optional[str]
1715) -> Callable:
1716    if isinstance(formatter, str):
1717        formatter_func = lambda x: formatter.format(x)
1718    elif callable(formatter):
1719        formatter_func = formatter
1720    else:
1721        msg = f"Expected a template string or callable, got {formatter} instead"
1722        raise TypeError(msg)
1723
1724    if na_rep is None:
1725        return formatter_func
1726    elif isinstance(na_rep, str):
1727        return lambda x: na_rep if pd.isna(x) else formatter_func(x)
1728    else:
1729        msg = f"Expected a string, got {na_rep} instead"
1730        raise TypeError(msg)
1731