style.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. """
  2. Module for applying conditional formatting to
  3. DataFrames and Series.
  4. """
  5. from collections import defaultdict
  6. from contextlib import contextmanager
  7. import copy
  8. from functools import partial
  9. from itertools import product
  10. from typing import Any, Callable, DefaultDict, Dict, List, Optional, Sequence, Tuple
  11. from uuid import uuid1
  12. import numpy as np
  13. from pandas._config import get_option
  14. from pandas._libs import lib
  15. from pandas.compat._optional import import_optional_dependency
  16. from pandas.util._decorators import Appender
  17. from pandas.core.dtypes.common import is_float
  18. import pandas as pd
  19. from pandas.api.types import is_dict_like, is_list_like
  20. import pandas.core.common as com
  21. from pandas.core.generic import _shared_docs
  22. from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
  23. jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
  24. try:
  25. import matplotlib.pyplot as plt
  26. from matplotlib import colors
  27. has_mpl = True
  28. except ImportError:
  29. has_mpl = False
  30. no_mpl_message = "{0} requires matplotlib."
  31. @contextmanager
  32. def _mpl(func):
  33. if has_mpl:
  34. yield plt, colors
  35. else:
  36. raise ImportError(no_mpl_message.format(func.__name__))
  37. class Styler:
  38. """
  39. Helps style a DataFrame or Series according to the data with HTML and CSS.
  40. Parameters
  41. ----------
  42. data : Series or DataFrame
  43. Data to be styled - either a Series or DataFrame.
  44. precision : int
  45. Precision to round floats to, defaults to pd.options.display.precision.
  46. table_styles : list-like, default None
  47. List of {selector: (attr, value)} dicts; see Notes.
  48. uuid : str, default None
  49. A unique identifier to avoid CSS collisions; generated automatically.
  50. caption : str, default None
  51. Caption to attach to the table.
  52. table_attributes : str, default None
  53. Items that show up in the opening ``<table>`` tag
  54. in addition to automatic (by default) id.
  55. cell_ids : bool, default True
  56. If True, each cell will have an ``id`` attribute in their HTML tag.
  57. The ``id`` takes the form ``T_<uuid>_row<num_row>_col<num_col>``
  58. where ``<uuid>`` is the unique identifier, ``<num_row>`` is the row
  59. number and ``<num_col>`` is the column number.
  60. na_rep : str, optional
  61. Representation for missing values.
  62. If ``na_rep`` is None, no special formatting is applied
  63. .. versionadded:: 1.0.0
  64. Attributes
  65. ----------
  66. env : Jinja2 jinja2.Environment
  67. template : Jinja2 Template
  68. loader : Jinja2 Loader
  69. See Also
  70. --------
  71. DataFrame.style : Return a Styler object containing methods for building
  72. a styled HTML representation for the DataFrame.
  73. Notes
  74. -----
  75. Most styling will be done by passing style functions into
  76. ``Styler.apply`` or ``Styler.applymap``. Style functions should
  77. return values with strings containing CSS ``'attr: value'`` that will
  78. be applied to the indicated cells.
  79. If using in the Jupyter notebook, Styler has defined a ``_repr_html_``
  80. to automatically render itself. Otherwise call Styler.render to get
  81. the generated HTML.
  82. CSS classes are attached to the generated HTML
  83. * Index and Column names include ``index_name`` and ``level<k>``
  84. where `k` is its level in a MultiIndex
  85. * Index label cells include
  86. * ``row_heading``
  87. * ``row<n>`` where `n` is the numeric position of the row
  88. * ``level<k>`` where `k` is the level in a MultiIndex
  89. * Column label cells include
  90. * ``col_heading``
  91. * ``col<n>`` where `n` is the numeric position of the column
  92. * ``evel<k>`` where `k` is the level in a MultiIndex
  93. * Blank cells include ``blank``
  94. * Data cells include ``data``
  95. """
  96. loader = jinja2.PackageLoader("pandas", "io/formats/templates")
  97. env = jinja2.Environment(loader=loader, trim_blocks=True)
  98. template = env.get_template("html.tpl")
  99. def __init__(
  100. self,
  101. data,
  102. precision=None,
  103. table_styles=None,
  104. uuid=None,
  105. caption=None,
  106. table_attributes=None,
  107. cell_ids=True,
  108. na_rep: Optional[str] = None,
  109. ):
  110. self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list)
  111. self._todo: List[Tuple[Callable, Tuple, Dict]] = []
  112. if not isinstance(data, (pd.Series, pd.DataFrame)):
  113. raise TypeError("``data`` must be a Series or DataFrame")
  114. if data.ndim == 1:
  115. data = data.to_frame()
  116. if not data.index.is_unique or not data.columns.is_unique:
  117. raise ValueError("style is not supported for non-unique indices.")
  118. self.data = data
  119. self.index = data.index
  120. self.columns = data.columns
  121. self.uuid = uuid
  122. self.table_styles = table_styles
  123. self.caption = caption
  124. if precision is None:
  125. precision = get_option("display.precision")
  126. self.precision = precision
  127. self.table_attributes = table_attributes
  128. self.hidden_index = False
  129. self.hidden_columns: Sequence[int] = []
  130. self.cell_ids = cell_ids
  131. self.na_rep = na_rep
  132. # display_funcs maps (row, col) -> formatting function
  133. def default_display_func(x):
  134. if self.na_rep is not None and pd.isna(x):
  135. return self.na_rep
  136. elif is_float(x):
  137. display_format = f"{x:.{self.precision}f}"
  138. return display_format
  139. else:
  140. return x
  141. self._display_funcs: DefaultDict[
  142. Tuple[int, int], Callable[[Any], str]
  143. ] = defaultdict(lambda: default_display_func)
  144. def _repr_html_(self):
  145. """
  146. Hooks into Jupyter notebook rich display system.
  147. """
  148. return self.render()
  149. @Appender(
  150. _shared_docs["to_excel"]
  151. % dict(
  152. axes="index, columns",
  153. klass="Styler",
  154. axes_single_arg="{0 or 'index', 1 or 'columns'}",
  155. optional_by="""
  156. by : str or list of str
  157. Name or list of names which refer to the axis items.""",
  158. versionadded_to_excel="\n .. versionadded:: 0.20",
  159. )
  160. )
  161. def to_excel(
  162. self,
  163. excel_writer,
  164. sheet_name="Sheet1",
  165. na_rep="",
  166. float_format=None,
  167. columns=None,
  168. header=True,
  169. index=True,
  170. index_label=None,
  171. startrow=0,
  172. startcol=0,
  173. engine=None,
  174. merge_cells=True,
  175. encoding=None,
  176. inf_rep="inf",
  177. verbose=True,
  178. freeze_panes=None,
  179. ):
  180. from pandas.io.formats.excel import ExcelFormatter
  181. formatter = ExcelFormatter(
  182. self,
  183. na_rep=na_rep,
  184. cols=columns,
  185. header=header,
  186. float_format=float_format,
  187. index=index,
  188. index_label=index_label,
  189. merge_cells=merge_cells,
  190. inf_rep=inf_rep,
  191. )
  192. formatter.write(
  193. excel_writer,
  194. sheet_name=sheet_name,
  195. startrow=startrow,
  196. startcol=startcol,
  197. freeze_panes=freeze_panes,
  198. engine=engine,
  199. )
  200. def _translate(self):
  201. """
  202. Convert the DataFrame in `self.data` and the attrs from `_build_styles`
  203. into a dictionary of {head, body, uuid, cellstyle}.
  204. """
  205. table_styles = self.table_styles or []
  206. caption = self.caption
  207. ctx = self.ctx
  208. precision = self.precision
  209. hidden_index = self.hidden_index
  210. hidden_columns = self.hidden_columns
  211. uuid = self.uuid or str(uuid1()).replace("-", "_")
  212. ROW_HEADING_CLASS = "row_heading"
  213. COL_HEADING_CLASS = "col_heading"
  214. INDEX_NAME_CLASS = "index_name"
  215. DATA_CLASS = "data"
  216. BLANK_CLASS = "blank"
  217. BLANK_VALUE = ""
  218. def format_attr(pair):
  219. return f"{pair['key']}={pair['value']}"
  220. # for sparsifying a MultiIndex
  221. idx_lengths = _get_level_lengths(self.index)
  222. col_lengths = _get_level_lengths(self.columns, hidden_columns)
  223. cell_context = dict()
  224. n_rlvls = self.data.index.nlevels
  225. n_clvls = self.data.columns.nlevels
  226. rlabels = self.data.index.tolist()
  227. clabels = self.data.columns.tolist()
  228. if n_rlvls == 1:
  229. rlabels = [[x] for x in rlabels]
  230. if n_clvls == 1:
  231. clabels = [[x] for x in clabels]
  232. clabels = list(zip(*clabels))
  233. cellstyle = []
  234. head = []
  235. for r in range(n_clvls):
  236. # Blank for Index columns...
  237. row_es = [
  238. {
  239. "type": "th",
  240. "value": BLANK_VALUE,
  241. "display_value": BLANK_VALUE,
  242. "is_visible": not hidden_index,
  243. "class": " ".join([BLANK_CLASS]),
  244. }
  245. ] * (n_rlvls - 1)
  246. # ... except maybe the last for columns.names
  247. name = self.data.columns.names[r]
  248. cs = [
  249. BLANK_CLASS if name is None else INDEX_NAME_CLASS,
  250. f"level{r}",
  251. ]
  252. name = BLANK_VALUE if name is None else name
  253. row_es.append(
  254. {
  255. "type": "th",
  256. "value": name,
  257. "display_value": name,
  258. "class": " ".join(cs),
  259. "is_visible": not hidden_index,
  260. }
  261. )
  262. if clabels:
  263. for c, value in enumerate(clabels[r]):
  264. cs = [
  265. COL_HEADING_CLASS,
  266. f"level{r}",
  267. f"col{c}",
  268. ]
  269. cs.extend(
  270. cell_context.get("col_headings", {}).get(r, {}).get(c, [])
  271. )
  272. es = {
  273. "type": "th",
  274. "value": value,
  275. "display_value": value,
  276. "class": " ".join(cs),
  277. "is_visible": _is_visible(c, r, col_lengths),
  278. }
  279. colspan = col_lengths.get((r, c), 0)
  280. if colspan > 1:
  281. es["attributes"] = [
  282. format_attr({"key": "colspan", "value": colspan})
  283. ]
  284. row_es.append(es)
  285. head.append(row_es)
  286. if (
  287. self.data.index.names
  288. and com.any_not_none(*self.data.index.names)
  289. and not hidden_index
  290. ):
  291. index_header_row = []
  292. for c, name in enumerate(self.data.index.names):
  293. cs = [INDEX_NAME_CLASS, f"level{c}"]
  294. name = "" if name is None else name
  295. index_header_row.append(
  296. {"type": "th", "value": name, "class": " ".join(cs)}
  297. )
  298. index_header_row.extend(
  299. [{"type": "th", "value": BLANK_VALUE, "class": " ".join([BLANK_CLASS])}]
  300. * (len(clabels[0]) - len(hidden_columns))
  301. )
  302. head.append(index_header_row)
  303. body = []
  304. for r, idx in enumerate(self.data.index):
  305. row_es = []
  306. for c, value in enumerate(rlabels[r]):
  307. rid = [
  308. ROW_HEADING_CLASS,
  309. f"level{c}",
  310. f"row{r}",
  311. ]
  312. es = {
  313. "type": "th",
  314. "is_visible": (_is_visible(r, c, idx_lengths) and not hidden_index),
  315. "value": value,
  316. "display_value": value,
  317. "id": "_".join(rid[1:]),
  318. "class": " ".join(rid),
  319. }
  320. rowspan = idx_lengths.get((c, r), 0)
  321. if rowspan > 1:
  322. es["attributes"] = [
  323. format_attr({"key": "rowspan", "value": rowspan})
  324. ]
  325. row_es.append(es)
  326. for c, col in enumerate(self.data.columns):
  327. cs = [DATA_CLASS, f"row{r}", f"col{c}"]
  328. cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
  329. formatter = self._display_funcs[(r, c)]
  330. value = self.data.iloc[r, c]
  331. row_dict = {
  332. "type": "td",
  333. "value": value,
  334. "class": " ".join(cs),
  335. "display_value": formatter(value),
  336. "is_visible": (c not in hidden_columns),
  337. }
  338. # only add an id if the cell has a style
  339. if self.cell_ids or not (len(ctx[r, c]) == 1 and ctx[r, c][0] == ""):
  340. row_dict["id"] = "_".join(cs[1:])
  341. row_es.append(row_dict)
  342. props = []
  343. for x in ctx[r, c]:
  344. # have to handle empty styles like ['']
  345. if x.count(":"):
  346. props.append(x.split(":"))
  347. else:
  348. props.append(["", ""])
  349. cellstyle.append({"props": props, "selector": f"row{r}_col{c}"})
  350. body.append(row_es)
  351. table_attr = self.table_attributes
  352. use_mathjax = get_option("display.html.use_mathjax")
  353. if not use_mathjax:
  354. table_attr = table_attr or ""
  355. if 'class="' in table_attr:
  356. table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
  357. else:
  358. table_attr += ' class="tex2jax_ignore"'
  359. return dict(
  360. head=head,
  361. cellstyle=cellstyle,
  362. body=body,
  363. uuid=uuid,
  364. precision=precision,
  365. table_styles=table_styles,
  366. caption=caption,
  367. table_attributes=table_attr,
  368. )
  369. def format(self, formatter, subset=None, na_rep: Optional[str] = None):
  370. """
  371. Format the text display value of cells.
  372. Parameters
  373. ----------
  374. formatter : str, callable, dict or None
  375. If ``formatter`` is None, the default formatter is used
  376. subset : IndexSlice
  377. An argument to ``DataFrame.loc`` that restricts which elements
  378. ``formatter`` is applied to.
  379. na_rep : str, optional
  380. Representation for missing values.
  381. If ``na_rep`` is None, no special formatting is applied
  382. .. versionadded:: 1.0.0
  383. Returns
  384. -------
  385. self : Styler
  386. Notes
  387. -----
  388. ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where
  389. ``a`` is one of
  390. - str: this will be wrapped in: ``a.format(x)``
  391. - callable: called with the value of an individual cell
  392. The default display value for numeric values is the "general" (``g``)
  393. format with ``pd.options.display.precision`` precision.
  394. Examples
  395. --------
  396. >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b'])
  397. >>> df.style.format("{:.2%}")
  398. >>> df['c'] = ['a', 'b', 'c', 'd']
  399. >>> df.style.format({'c': str.upper})
  400. """
  401. if formatter is None:
  402. assert self._display_funcs.default_factory is not None
  403. formatter = self._display_funcs.default_factory()
  404. if subset is None:
  405. row_locs = range(len(self.data))
  406. col_locs = range(len(self.data.columns))
  407. else:
  408. subset = _non_reducing_slice(subset)
  409. if len(subset) == 1:
  410. subset = subset, self.data.columns
  411. sub_df = self.data.loc[subset]
  412. row_locs = self.data.index.get_indexer_for(sub_df.index)
  413. col_locs = self.data.columns.get_indexer_for(sub_df.columns)
  414. if is_dict_like(formatter):
  415. for col, col_formatter in formatter.items():
  416. # formatter must be callable, so '{}' are converted to lambdas
  417. col_formatter = _maybe_wrap_formatter(col_formatter, na_rep)
  418. col_num = self.data.columns.get_indexer_for([col])[0]
  419. for row_num in row_locs:
  420. self._display_funcs[(row_num, col_num)] = col_formatter
  421. else:
  422. # single scalar to format all cells with
  423. formatter = _maybe_wrap_formatter(formatter, na_rep)
  424. locs = product(*(row_locs, col_locs))
  425. for i, j in locs:
  426. self._display_funcs[(i, j)] = formatter
  427. return self
  428. def render(self, **kwargs):
  429. """
  430. Render the built up styles to HTML.
  431. Parameters
  432. ----------
  433. **kwargs
  434. Any additional keyword arguments are passed
  435. through to ``self.template.render``.
  436. This is useful when you need to provide
  437. additional variables for a custom template.
  438. Returns
  439. -------
  440. rendered : str
  441. The rendered HTML.
  442. Notes
  443. -----
  444. ``Styler`` objects have defined the ``_repr_html_`` method
  445. which automatically calls ``self.render()`` when it's the
  446. last item in a Notebook cell. When calling ``Styler.render()``
  447. directly, wrap the result in ``IPython.display.HTML`` to view
  448. the rendered HTML in the notebook.
  449. Pandas uses the following keys in render. Arguments passed
  450. in ``**kwargs`` take precedence, so think carefully if you want
  451. to override them:
  452. * head
  453. * cellstyle
  454. * body
  455. * uuid
  456. * precision
  457. * table_styles
  458. * caption
  459. * table_attributes
  460. """
  461. self._compute()
  462. # TODO: namespace all the pandas keys
  463. d = self._translate()
  464. # filter out empty styles, every cell will have a class
  465. # but the list of props may just be [['', '']].
  466. # so we have the neested anys below
  467. trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])]
  468. d["cellstyle"] = trimmed
  469. d.update(kwargs)
  470. return self.template.render(**d)
  471. def _update_ctx(self, attrs):
  472. """
  473. Update the state of the Styler.
  474. Collects a mapping of {index_label: ['<property>: <value>']}.
  475. attrs : Series or DataFrame
  476. should contain strings of '<property>: <value>;<prop2>: <val2>'
  477. Whitespace shouldn't matter and the final trailing ';' shouldn't
  478. matter.
  479. """
  480. for row_label, v in attrs.iterrows():
  481. for col_label, col in v.items():
  482. i = self.index.get_indexer([row_label])[0]
  483. j = self.columns.get_indexer([col_label])[0]
  484. for pair in col.rstrip(";").split(";"):
  485. self.ctx[(i, j)].append(pair)
  486. def _copy(self, deepcopy=False):
  487. styler = Styler(
  488. self.data,
  489. precision=self.precision,
  490. caption=self.caption,
  491. uuid=self.uuid,
  492. table_styles=self.table_styles,
  493. na_rep=self.na_rep,
  494. )
  495. if deepcopy:
  496. styler.ctx = copy.deepcopy(self.ctx)
  497. styler._todo = copy.deepcopy(self._todo)
  498. else:
  499. styler.ctx = self.ctx
  500. styler._todo = self._todo
  501. return styler
  502. def __copy__(self):
  503. """
  504. Deep copy by default.
  505. """
  506. return self._copy(deepcopy=False)
  507. def __deepcopy__(self, memo):
  508. return self._copy(deepcopy=True)
  509. def clear(self):
  510. """
  511. Reset the styler, removing any previously applied styles.
  512. Returns None.
  513. """
  514. self.ctx.clear()
  515. self._todo = []
  516. def _compute(self):
  517. """
  518. Execute the style functions built up in `self._todo`.
  519. Relies on the conventions that all style functions go through
  520. .apply or .applymap. The append styles to apply as tuples of
  521. (application method, *args, **kwargs)
  522. """
  523. r = self
  524. for func, args, kwargs in self._todo:
  525. r = func(self)(*args, **kwargs)
  526. return r
  527. def _apply(self, func, axis=0, subset=None, **kwargs):
  528. subset = slice(None) if subset is None else subset
  529. subset = _non_reducing_slice(subset)
  530. data = self.data.loc[subset]
  531. if axis is not None:
  532. result = data.apply(func, axis=axis, result_type="expand", **kwargs)
  533. result.columns = data.columns
  534. else:
  535. result = func(data, **kwargs)
  536. if not isinstance(result, pd.DataFrame):
  537. raise TypeError(
  538. f"Function {repr(func)} must return a DataFrame when "
  539. f"passed to `Styler.apply` with axis=None"
  540. )
  541. if not (
  542. result.index.equals(data.index) and result.columns.equals(data.columns)
  543. ):
  544. raise ValueError(
  545. f"Result of {repr(func)} must have identical "
  546. f"index and columns as the input"
  547. )
  548. result_shape = result.shape
  549. expected_shape = self.data.loc[subset].shape
  550. if result_shape != expected_shape:
  551. raise ValueError(
  552. f"Function {repr(func)} returned the wrong shape.\n"
  553. f"Result has shape: {result.shape}\n"
  554. f"Expected shape: {expected_shape}"
  555. )
  556. self._update_ctx(result)
  557. return self
  558. def apply(self, func, axis=0, subset=None, **kwargs):
  559. """
  560. Apply a function column-wise, row-wise, or table-wise.
  561. Updates the HTML representation with the result.
  562. Parameters
  563. ----------
  564. func : function
  565. ``func`` should take a Series or DataFrame (depending
  566. on ``axis``), and return an object with the same shape.
  567. Must return a DataFrame with identical index and
  568. column labels when ``axis=None``.
  569. axis : {0 or 'index', 1 or 'columns', None}, default 0
  570. Apply to each column (``axis=0`` or ``'index'``), to each row
  571. (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
  572. with ``axis=None``.
  573. subset : IndexSlice
  574. A valid indexer to limit ``data`` to *before* applying the
  575. function. Consider using a pandas.IndexSlice.
  576. **kwargs : dict
  577. Pass along to ``func``.
  578. Returns
  579. -------
  580. self : Styler
  581. Notes
  582. -----
  583. The output shape of ``func`` should match the input, i.e. if
  584. ``x`` is the input row, column, or table (depending on ``axis``),
  585. then ``func(x).shape == x.shape`` should be true.
  586. This is similar to ``DataFrame.apply``, except that ``axis=None``
  587. applies the function to the entire DataFrame at once,
  588. rather than column-wise or row-wise.
  589. Examples
  590. --------
  591. >>> def highlight_max(x):
  592. ... return ['background-color: yellow' if v == x.max() else ''
  593. for v in x]
  594. ...
  595. >>> df = pd.DataFrame(np.random.randn(5, 2))
  596. >>> df.style.apply(highlight_max)
  597. """
  598. self._todo.append(
  599. (lambda instance: getattr(instance, "_apply"), (func, axis, subset), kwargs)
  600. )
  601. return self
  602. def _applymap(self, func, subset=None, **kwargs):
  603. func = partial(func, **kwargs) # applymap doesn't take kwargs?
  604. if subset is None:
  605. subset = pd.IndexSlice[:]
  606. subset = _non_reducing_slice(subset)
  607. result = self.data.loc[subset].applymap(func)
  608. self._update_ctx(result)
  609. return self
  610. def applymap(self, func, subset=None, **kwargs):
  611. """
  612. Apply a function elementwise.
  613. Updates the HTML representation with the result.
  614. Parameters
  615. ----------
  616. func : function
  617. ``func`` should take a scalar and return a scalar.
  618. subset : IndexSlice
  619. A valid indexer to limit ``data`` to *before* applying the
  620. function. Consider using a pandas.IndexSlice.
  621. **kwargs : dict
  622. Pass along to ``func``.
  623. Returns
  624. -------
  625. self : Styler
  626. See Also
  627. --------
  628. Styler.where
  629. """
  630. self._todo.append(
  631. (lambda instance: getattr(instance, "_applymap"), (func, subset), kwargs)
  632. )
  633. return self
  634. def where(self, cond, value, other=None, subset=None, **kwargs):
  635. """
  636. Apply a function elementwise.
  637. Updates the HTML representation with a style which is
  638. selected in accordance with the return value of a function.
  639. .. versionadded:: 0.21.0
  640. Parameters
  641. ----------
  642. cond : callable
  643. ``cond`` should take a scalar and return a boolean.
  644. value : str
  645. Applied when ``cond`` returns true.
  646. other : str
  647. Applied when ``cond`` returns false.
  648. subset : IndexSlice
  649. A valid indexer to limit ``data`` to *before* applying the
  650. function. Consider using a pandas.IndexSlice.
  651. **kwargs : dict
  652. Pass along to ``cond``.
  653. Returns
  654. -------
  655. self : Styler
  656. See Also
  657. --------
  658. Styler.applymap
  659. """
  660. if other is None:
  661. other = ""
  662. return self.applymap(
  663. lambda val: value if cond(val) else other, subset=subset, **kwargs
  664. )
  665. def set_precision(self, precision):
  666. """
  667. Set the precision used to render.
  668. Parameters
  669. ----------
  670. precision : int
  671. Returns
  672. -------
  673. self : Styler
  674. """
  675. self.precision = precision
  676. return self
  677. def set_table_attributes(self, attributes):
  678. """
  679. Set the table attributes.
  680. These are the items that show up in the opening ``<table>`` tag
  681. in addition to to automatic (by default) id.
  682. Parameters
  683. ----------
  684. attributes : str
  685. Returns
  686. -------
  687. self : Styler
  688. Examples
  689. --------
  690. >>> df = pd.DataFrame(np.random.randn(10, 4))
  691. >>> df.style.set_table_attributes('class="pure-table"')
  692. # ... <table class="pure-table"> ...
  693. """
  694. self.table_attributes = attributes
  695. return self
  696. def export(self):
  697. """
  698. Export the styles to applied to the current Styler.
  699. Can be applied to a second style with ``Styler.use``.
  700. Returns
  701. -------
  702. styles : list
  703. See Also
  704. --------
  705. Styler.use
  706. """
  707. return self._todo
  708. def use(self, styles):
  709. """
  710. Set the styles on the current Styler.
  711. Possibly uses styles from ``Styler.export``.
  712. Parameters
  713. ----------
  714. styles : list
  715. List of style functions.
  716. Returns
  717. -------
  718. self : Styler
  719. See Also
  720. --------
  721. Styler.export
  722. """
  723. self._todo.extend(styles)
  724. return self
  725. def set_uuid(self, uuid):
  726. """
  727. Set the uuid for a Styler.
  728. Parameters
  729. ----------
  730. uuid : str
  731. Returns
  732. -------
  733. self : Styler
  734. """
  735. self.uuid = uuid
  736. return self
  737. def set_caption(self, caption):
  738. """
  739. Set the caption on a Styler.
  740. Parameters
  741. ----------
  742. caption : str
  743. Returns
  744. -------
  745. self : Styler
  746. """
  747. self.caption = caption
  748. return self
  749. def set_table_styles(self, table_styles):
  750. """
  751. Set the table styles on a Styler.
  752. These are placed in a ``<style>`` tag before the generated HTML table.
  753. Parameters
  754. ----------
  755. table_styles : list
  756. Each individual table_style should be a dictionary with
  757. ``selector`` and ``props`` keys. ``selector`` should be a CSS
  758. selector that the style will be applied to (automatically
  759. prefixed by the table's UUID) and ``props`` should be a list of
  760. tuples with ``(attribute, value)``.
  761. Returns
  762. -------
  763. self : Styler
  764. Examples
  765. --------
  766. >>> df = pd.DataFrame(np.random.randn(10, 4))
  767. >>> df.style.set_table_styles(
  768. ... [{'selector': 'tr:hover',
  769. ... 'props': [('background-color', 'yellow')]}]
  770. ... )
  771. """
  772. self.table_styles = table_styles
  773. return self
  774. def set_na_rep(self, na_rep: str) -> "Styler":
  775. """
  776. Set the missing data representation on a Styler.
  777. .. versionadded:: 1.0.0
  778. Parameters
  779. ----------
  780. na_rep : str
  781. Returns
  782. -------
  783. self : Styler
  784. """
  785. self.na_rep = na_rep
  786. return self
  787. def hide_index(self):
  788. """
  789. Hide any indices from rendering.
  790. .. versionadded:: 0.23.0
  791. Returns
  792. -------
  793. self : Styler
  794. """
  795. self.hidden_index = True
  796. return self
  797. def hide_columns(self, subset):
  798. """
  799. Hide columns from rendering.
  800. .. versionadded:: 0.23.0
  801. Parameters
  802. ----------
  803. subset : IndexSlice
  804. An argument to ``DataFrame.loc`` that identifies which columns
  805. are hidden.
  806. Returns
  807. -------
  808. self : Styler
  809. """
  810. subset = _non_reducing_slice(subset)
  811. hidden_df = self.data.loc[subset]
  812. self.hidden_columns = self.columns.get_indexer_for(hidden_df.columns)
  813. return self
  814. # -----------------------------------------------------------------------
  815. # A collection of "builtin" styles
  816. # -----------------------------------------------------------------------
  817. @staticmethod
  818. def _highlight_null(v, null_color):
  819. return f"background-color: {null_color}" if pd.isna(v) else ""
  820. def highlight_null(self, null_color="red"):
  821. """
  822. Shade the background ``null_color`` for missing values.
  823. Parameters
  824. ----------
  825. null_color : str
  826. Returns
  827. -------
  828. self : Styler
  829. """
  830. self.applymap(self._highlight_null, null_color=null_color)
  831. return self
  832. def background_gradient(
  833. self,
  834. cmap="PuBu",
  835. low=0,
  836. high=0,
  837. axis=0,
  838. subset=None,
  839. text_color_threshold=0.408,
  840. vmin: Optional[float] = None,
  841. vmax: Optional[float] = None,
  842. ):
  843. """
  844. Color the background in a gradient style.
  845. The background color is determined according
  846. to the data in each column (optionally row). Requires matplotlib.
  847. Parameters
  848. ----------
  849. cmap : str or colormap
  850. Matplotlib colormap.
  851. low : float
  852. Compress the range by the low.
  853. high : float
  854. Compress the range by the high.
  855. axis : {0 or 'index', 1 or 'columns', None}, default 0
  856. Apply to each column (``axis=0`` or ``'index'``), to each row
  857. (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
  858. with ``axis=None``.
  859. subset : IndexSlice
  860. A valid slice for ``data`` to limit the style application to.
  861. text_color_threshold : float or int
  862. Luminance threshold for determining text color. Facilitates text
  863. visibility across varying background colors. From 0 to 1.
  864. 0 = all text is dark colored, 1 = all text is light colored.
  865. .. versionadded:: 0.24.0
  866. vmin : float, optional
  867. Minimum data value that corresponds to colormap minimum value.
  868. When None (default): the minimum value of the data will be used.
  869. .. versionadded:: 1.0.0
  870. vmax : float, optional
  871. Maximum data value that corresponds to colormap maximum value.
  872. When None (default): the maximum value of the data will be used.
  873. .. versionadded:: 1.0.0
  874. Returns
  875. -------
  876. self : Styler
  877. Raises
  878. ------
  879. ValueError
  880. If ``text_color_threshold`` is not a value from 0 to 1.
  881. Notes
  882. -----
  883. Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the
  884. text legible by not using the entire range of the color map. The range
  885. of the data is extended by ``low * (x.max() - x.min())`` and ``high *
  886. (x.max() - x.min())`` before normalizing.
  887. """
  888. subset = _maybe_numeric_slice(self.data, subset)
  889. subset = _non_reducing_slice(subset)
  890. self.apply(
  891. self._background_gradient,
  892. cmap=cmap,
  893. subset=subset,
  894. axis=axis,
  895. low=low,
  896. high=high,
  897. text_color_threshold=text_color_threshold,
  898. vmin=vmin,
  899. vmax=vmax,
  900. )
  901. return self
  902. @staticmethod
  903. def _background_gradient(
  904. s,
  905. cmap="PuBu",
  906. low=0,
  907. high=0,
  908. text_color_threshold=0.408,
  909. vmin: Optional[float] = None,
  910. vmax: Optional[float] = None,
  911. ):
  912. """
  913. Color background in a range according to the data.
  914. """
  915. if (
  916. not isinstance(text_color_threshold, (float, int))
  917. or not 0 <= text_color_threshold <= 1
  918. ):
  919. msg = "`text_color_threshold` must be a value from 0 to 1."
  920. raise ValueError(msg)
  921. with _mpl(Styler.background_gradient) as (plt, colors):
  922. smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
  923. smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
  924. rng = smax - smin
  925. # extend lower / upper bounds, compresses color range
  926. norm = colors.Normalize(smin - (rng * low), smax + (rng * high))
  927. # matplotlib colors.Normalize modifies inplace?
  928. # https://github.com/matplotlib/matplotlib/issues/5427
  929. rgbas = plt.cm.get_cmap(cmap)(norm(s.to_numpy(dtype=float)))
  930. def relative_luminance(rgba):
  931. """
  932. Calculate relative luminance of a color.
  933. The calculation adheres to the W3C standards
  934. (https://www.w3.org/WAI/GL/wiki/Relative_luminance)
  935. Parameters
  936. ----------
  937. color : rgb or rgba tuple
  938. Returns
  939. -------
  940. float
  941. The relative luminance as a value from 0 to 1
  942. """
  943. r, g, b = (
  944. x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055 ** 2.4)
  945. for x in rgba[:3]
  946. )
  947. return 0.2126 * r + 0.7152 * g + 0.0722 * b
  948. def css(rgba):
  949. dark = relative_luminance(rgba) < text_color_threshold
  950. text_color = "#f1f1f1" if dark else "#000000"
  951. return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
  952. if s.ndim == 1:
  953. return [css(rgba) for rgba in rgbas]
  954. else:
  955. return pd.DataFrame(
  956. [[css(rgba) for rgba in row] for row in rgbas],
  957. index=s.index,
  958. columns=s.columns,
  959. )
  960. def set_properties(self, subset=None, **kwargs):
  961. """
  962. Method to set one or more non-data dependent properties or each cell.
  963. Parameters
  964. ----------
  965. subset : IndexSlice
  966. A valid slice for ``data`` to limit the style application to.
  967. **kwargs : dict
  968. A dictionary of property, value pairs to be set for each cell.
  969. Returns
  970. -------
  971. self : Styler
  972. Examples
  973. --------
  974. >>> df = pd.DataFrame(np.random.randn(10, 4))
  975. >>> df.style.set_properties(color="white", align="right")
  976. >>> df.style.set_properties(**{'background-color': 'yellow'})
  977. """
  978. values = ";".join(f"{p}: {v}" for p, v in kwargs.items())
  979. f = lambda x: values
  980. return self.applymap(f, subset=subset)
  981. @staticmethod
  982. def _bar(s, align, colors, width=100, vmin=None, vmax=None):
  983. """
  984. Draw bar chart in dataframe cells.
  985. """
  986. # Get input value range.
  987. smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
  988. smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
  989. if align == "mid":
  990. smin = min(0, smin)
  991. smax = max(0, smax)
  992. elif align == "zero":
  993. # For "zero" mode, we want the range to be symmetrical around zero.
  994. smax = max(abs(smin), abs(smax))
  995. smin = -smax
  996. # Transform to percent-range of linear-gradient
  997. normed = width * (s.to_numpy(dtype=float) - smin) / (smax - smin + 1e-12)
  998. zero = -width * smin / (smax - smin + 1e-12)
  999. def css_bar(start, end, color):
  1000. """
  1001. Generate CSS code to draw a bar from start to end.
  1002. """
  1003. css = "width: 10em; height: 80%;"
  1004. if end > start:
  1005. css += "background: linear-gradient(90deg,"
  1006. if start > 0:
  1007. css += f" transparent {start:.1f}%, {color} {start:.1f}%, "
  1008. e = min(end, width)
  1009. css += f"{color} {e:.1f}%, transparent {e:.1f}%)"
  1010. return css
  1011. def css(x):
  1012. if pd.isna(x):
  1013. return ""
  1014. # avoid deprecated indexing `colors[x > zero]`
  1015. color = colors[1] if x > zero else colors[0]
  1016. if align == "left":
  1017. return css_bar(0, x, color)
  1018. else:
  1019. return css_bar(min(x, zero), max(x, zero), color)
  1020. if s.ndim == 1:
  1021. return [css(x) for x in normed]
  1022. else:
  1023. return pd.DataFrame(
  1024. [[css(x) for x in row] for row in normed],
  1025. index=s.index,
  1026. columns=s.columns,
  1027. )
  1028. def bar(
  1029. self,
  1030. subset=None,
  1031. axis=0,
  1032. color="#d65f5f",
  1033. width=100,
  1034. align="left",
  1035. vmin=None,
  1036. vmax=None,
  1037. ):
  1038. """
  1039. Draw bar chart in the cell backgrounds.
  1040. Parameters
  1041. ----------
  1042. subset : IndexSlice, optional
  1043. A valid slice for `data` to limit the style application to.
  1044. axis : {0 or 'index', 1 or 'columns', None}, default 0
  1045. Apply to each column (``axis=0`` or ``'index'``), to each row
  1046. (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
  1047. with ``axis=None``.
  1048. color : str or 2-tuple/list
  1049. If a str is passed, the color is the same for both
  1050. negative and positive numbers. If 2-tuple/list is used, the
  1051. first element is the color_negative and the second is the
  1052. color_positive (eg: ['#d65f5f', '#5fba7d']).
  1053. width : float, default 100
  1054. A number between 0 or 100. The largest value will cover `width`
  1055. percent of the cell's width.
  1056. align : {'left', 'zero',' mid'}, default 'left'
  1057. How to align the bars with the cells.
  1058. - 'left' : the min value starts at the left of the cell.
  1059. - 'zero' : a value of zero is located at the center of the cell.
  1060. - 'mid' : the center of the cell is at (max-min)/2, or
  1061. if values are all negative (positive) the zero is aligned
  1062. at the right (left) of the cell.
  1063. vmin : float, optional
  1064. Minimum bar value, defining the left hand limit
  1065. of the bar drawing range, lower values are clipped to `vmin`.
  1066. When None (default): the minimum value of the data will be used.
  1067. .. versionadded:: 0.24.0
  1068. vmax : float, optional
  1069. Maximum bar value, defining the right hand limit
  1070. of the bar drawing range, higher values are clipped to `vmax`.
  1071. When None (default): the maximum value of the data will be used.
  1072. .. versionadded:: 0.24.0
  1073. Returns
  1074. -------
  1075. self : Styler
  1076. """
  1077. if align not in ("left", "zero", "mid"):
  1078. raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
  1079. if not (is_list_like(color)):
  1080. color = [color, color]
  1081. elif len(color) == 1:
  1082. color = [color[0], color[0]]
  1083. elif len(color) > 2:
  1084. raise ValueError(
  1085. "`color` must be string or a list-like "
  1086. "of length 2: [`color_neg`, `color_pos`] "
  1087. "(eg: color=['#d65f5f', '#5fba7d'])"
  1088. )
  1089. subset = _maybe_numeric_slice(self.data, subset)
  1090. subset = _non_reducing_slice(subset)
  1091. self.apply(
  1092. self._bar,
  1093. subset=subset,
  1094. axis=axis,
  1095. align=align,
  1096. colors=color,
  1097. width=width,
  1098. vmin=vmin,
  1099. vmax=vmax,
  1100. )
  1101. return self
  1102. def highlight_max(self, subset=None, color="yellow", axis=0):
  1103. """
  1104. Highlight the maximum by shading the background.
  1105. Parameters
  1106. ----------
  1107. subset : IndexSlice, default None
  1108. A valid slice for ``data`` to limit the style application to.
  1109. color : str, default 'yellow'
  1110. axis : {0 or 'index', 1 or 'columns', None}, default 0
  1111. Apply to each column (``axis=0`` or ``'index'``), to each row
  1112. (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
  1113. with ``axis=None``.
  1114. Returns
  1115. -------
  1116. self : Styler
  1117. """
  1118. return self._highlight_handler(subset=subset, color=color, axis=axis, max_=True)
  1119. def highlight_min(self, subset=None, color="yellow", axis=0):
  1120. """
  1121. Highlight the minimum by shading the background.
  1122. Parameters
  1123. ----------
  1124. subset : IndexSlice, default None
  1125. A valid slice for ``data`` to limit the style application to.
  1126. color : str, default 'yellow'
  1127. axis : {0 or 'index', 1 or 'columns', None}, default 0
  1128. Apply to each column (``axis=0`` or ``'index'``), to each row
  1129. (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
  1130. with ``axis=None``.
  1131. Returns
  1132. -------
  1133. self : Styler
  1134. """
  1135. return self._highlight_handler(
  1136. subset=subset, color=color, axis=axis, max_=False
  1137. )
  1138. def _highlight_handler(self, subset=None, color="yellow", axis=None, max_=True):
  1139. subset = _non_reducing_slice(_maybe_numeric_slice(self.data, subset))
  1140. self.apply(
  1141. self._highlight_extrema, color=color, axis=axis, subset=subset, max_=max_
  1142. )
  1143. return self
  1144. @staticmethod
  1145. def _highlight_extrema(data, color="yellow", max_=True):
  1146. """
  1147. Highlight the min or max in a Series or DataFrame.
  1148. """
  1149. attr = f"background-color: {color}"
  1150. if max_:
  1151. extrema = data == np.nanmax(data.to_numpy())
  1152. else:
  1153. extrema = data == np.nanmin(data.to_numpy())
  1154. if data.ndim == 1: # Series from .apply
  1155. return [attr if v else "" for v in extrema]
  1156. else: # DataFrame from .tee
  1157. return pd.DataFrame(
  1158. np.where(extrema, attr, ""), index=data.index, columns=data.columns
  1159. )
  1160. @classmethod
  1161. def from_custom_template(cls, searchpath, name):
  1162. """
  1163. Factory function for creating a subclass of ``Styler``.
  1164. Uses a custom template and Jinja environment.
  1165. Parameters
  1166. ----------
  1167. searchpath : str or list
  1168. Path or paths of directories containing the templates.
  1169. name : str
  1170. Name of your custom template to use for rendering.
  1171. Returns
  1172. -------
  1173. MyStyler : subclass of Styler
  1174. Has the correct ``env`` and ``template`` class attributes set.
  1175. """
  1176. loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(searchpath), cls.loader])
  1177. class MyStyler(cls):
  1178. env = jinja2.Environment(loader=loader)
  1179. template = env.get_template(name)
  1180. return MyStyler
  1181. def pipe(self, func, *args, **kwargs):
  1182. """
  1183. Apply ``func(self, *args, **kwargs)``, and return the result.
  1184. .. versionadded:: 0.24.0
  1185. Parameters
  1186. ----------
  1187. func : function
  1188. Function to apply to the Styler. Alternatively, a
  1189. ``(callable, keyword)`` tuple where ``keyword`` is a string
  1190. indicating the keyword of ``callable`` that expects the Styler.
  1191. *args : optional
  1192. Arguments passed to `func`.
  1193. **kwargs : optional
  1194. A dictionary of keyword arguments passed into ``func``.
  1195. Returns
  1196. -------
  1197. object :
  1198. The value returned by ``func``.
  1199. See Also
  1200. --------
  1201. DataFrame.pipe : Analogous method for DataFrame.
  1202. Styler.apply : Apply a function row-wise, column-wise, or table-wise to
  1203. modify the dataframe's styling.
  1204. Notes
  1205. -----
  1206. Like :meth:`DataFrame.pipe`, this method can simplify the
  1207. application of several user-defined functions to a styler. Instead
  1208. of writing:
  1209. .. code-block:: python
  1210. f(g(df.style.set_precision(3), arg1=a), arg2=b, arg3=c)
  1211. users can write:
  1212. .. code-block:: python
  1213. (df.style.set_precision(3)
  1214. .pipe(g, arg1=a)
  1215. .pipe(f, arg2=b, arg3=c))
  1216. In particular, this allows users to define functions that take a
  1217. styler object, along with other parameters, and return the styler after
  1218. making styling changes (such as calling :meth:`Styler.apply` or
  1219. :meth:`Styler.set_properties`). Using ``.pipe``, these user-defined
  1220. style "transformations" can be interleaved with calls to the built-in
  1221. Styler interface.
  1222. Examples
  1223. --------
  1224. >>> def format_conversion(styler):
  1225. ... return (styler.set_properties(**{'text-align': 'right'})
  1226. ... .format({'conversion': '{:.1%}'}))
  1227. The user-defined ``format_conversion`` function above can be called
  1228. within a sequence of other style modifications:
  1229. >>> df = pd.DataFrame({'trial': list(range(5)),
  1230. ... 'conversion': [0.75, 0.85, np.nan, 0.7, 0.72]})
  1231. >>> (df.style
  1232. ... .highlight_min(subset=['conversion'], color='yellow')
  1233. ... .pipe(format_conversion)
  1234. ... .set_caption("Results with minimum conversion highlighted."))
  1235. """
  1236. return com.pipe(self, func, *args, **kwargs)
  1237. def _is_visible(idx_row, idx_col, lengths):
  1238. """
  1239. Index -> {(idx_row, idx_col): bool}).
  1240. """
  1241. return (idx_col, idx_row) in lengths
  1242. def _get_level_lengths(index, hidden_elements=None):
  1243. """
  1244. Given an index, find the level length for each element.
  1245. Optional argument is a list of index positions which
  1246. should not be visible.
  1247. Result is a dictionary of (level, initial_position): span
  1248. """
  1249. levels = index.format(sparsify=lib.no_default, adjoin=False, names=False)
  1250. if hidden_elements is None:
  1251. hidden_elements = []
  1252. lengths = {}
  1253. if index.nlevels == 1:
  1254. for i, value in enumerate(levels):
  1255. if i not in hidden_elements:
  1256. lengths[(0, i)] = 1
  1257. return lengths
  1258. for i, lvl in enumerate(levels):
  1259. for j, row in enumerate(lvl):
  1260. if not get_option("display.multi_sparse"):
  1261. lengths[(i, j)] = 1
  1262. elif (row is not lib.no_default) and (j not in hidden_elements):
  1263. last_label = j
  1264. lengths[(i, last_label)] = 1
  1265. elif row is not lib.no_default:
  1266. # even if its hidden, keep track of it in case
  1267. # length >1 and later elements are visible
  1268. last_label = j
  1269. lengths[(i, last_label)] = 0
  1270. elif j not in hidden_elements:
  1271. lengths[(i, last_label)] += 1
  1272. non_zero_lengths = {
  1273. element: length for element, length in lengths.items() if length >= 1
  1274. }
  1275. return non_zero_lengths
  1276. def _maybe_wrap_formatter(formatter, na_rep: Optional[str]):
  1277. if isinstance(formatter, str):
  1278. formatter_func = lambda x: formatter.format(x)
  1279. elif callable(formatter):
  1280. formatter_func = formatter
  1281. else:
  1282. msg = f"Expected a template string or callable, got {formatter} instead"
  1283. raise TypeError(msg)
  1284. if na_rep is None:
  1285. return formatter_func
  1286. elif isinstance(na_rep, str):
  1287. return lambda x: na_rep if pd.isna(x) else formatter_func(x)
  1288. else:
  1289. msg = f"Expected a string, got {na_rep} instead"
  1290. raise TypeError(msg)