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