style_render.py 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497
  1. from __future__ import annotations
  2. from collections import defaultdict
  3. from collections.abc import Sequence
  4. from functools import partial
  5. import re
  6. from typing import (
  7. TYPE_CHECKING,
  8. Any,
  9. Callable,
  10. DefaultDict,
  11. Optional,
  12. TypedDict,
  13. Union,
  14. )
  15. from uuid import uuid4
  16. import numpy as np
  17. from pandas._config import get_option
  18. from pandas._libs import lib
  19. from pandas.compat._optional import import_optional_dependency
  20. from pandas.core.dtypes.common import (
  21. is_complex,
  22. is_float,
  23. is_integer,
  24. )
  25. from pandas.core.dtypes.generic import ABCSeries
  26. from pandas import (
  27. DataFrame,
  28. Index,
  29. IndexSlice,
  30. MultiIndex,
  31. Series,
  32. isna,
  33. )
  34. from pandas.api.types import is_list_like
  35. import pandas.core.common as com
  36. if TYPE_CHECKING:
  37. from pandas._typing import (
  38. Axis,
  39. Level,
  40. )
  41. jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
  42. from markupsafe import escape as escape_html # markupsafe is jinja2 dependency
  43. BaseFormatter = Union[str, Callable]
  44. ExtFormatter = Union[BaseFormatter, dict[Any, Optional[BaseFormatter]]]
  45. CSSPair = tuple[str, Union[str, float]]
  46. CSSList = list[CSSPair]
  47. CSSProperties = Union[str, CSSList]
  48. class CSSDict(TypedDict):
  49. selector: str
  50. props: CSSProperties
  51. CSSStyles = list[CSSDict]
  52. Subset = Union[slice, Sequence, Index]
  53. class StylerRenderer:
  54. """
  55. Base class to process rendering a Styler with a specified jinja2 template.
  56. """
  57. loader = jinja2.PackageLoader("pandas", "io/formats/templates")
  58. env = jinja2.Environment(loader=loader, trim_blocks=True)
  59. template_html = env.get_template("html.tpl")
  60. template_html_table = env.get_template("html_table.tpl")
  61. template_html_style = env.get_template("html_style.tpl")
  62. template_latex = env.get_template("latex.tpl")
  63. template_string = env.get_template("string.tpl")
  64. def __init__(
  65. self,
  66. data: DataFrame | Series,
  67. uuid: str | None = None,
  68. uuid_len: int = 5,
  69. table_styles: CSSStyles | None = None,
  70. table_attributes: str | None = None,
  71. caption: str | tuple | list | None = None,
  72. cell_ids: bool = True,
  73. precision: int | None = None,
  74. ) -> None:
  75. # validate ordered args
  76. if isinstance(data, Series):
  77. data = data.to_frame()
  78. if not isinstance(data, DataFrame):
  79. raise TypeError("``data`` must be a Series or DataFrame")
  80. self.data: DataFrame = data
  81. self.index: Index = data.index
  82. self.columns: Index = data.columns
  83. if not isinstance(uuid_len, int) or uuid_len < 0:
  84. raise TypeError("``uuid_len`` must be an integer in range [0, 32].")
  85. self.uuid = uuid or uuid4().hex[: min(32, uuid_len)]
  86. self.uuid_len = len(self.uuid)
  87. self.table_styles = table_styles
  88. self.table_attributes = table_attributes
  89. self.caption = caption
  90. self.cell_ids = cell_ids
  91. self.css = {
  92. "row_heading": "row_heading",
  93. "col_heading": "col_heading",
  94. "index_name": "index_name",
  95. "col": "col",
  96. "row": "row",
  97. "col_trim": "col_trim",
  98. "row_trim": "row_trim",
  99. "level": "level",
  100. "data": "data",
  101. "blank": "blank",
  102. "foot": "foot",
  103. }
  104. self.concatenated: list[StylerRenderer] = []
  105. # add rendering variables
  106. self.hide_index_names: bool = False
  107. self.hide_column_names: bool = False
  108. self.hide_index_: list = [False] * self.index.nlevels
  109. self.hide_columns_: list = [False] * self.columns.nlevels
  110. self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
  111. self.hidden_columns: Sequence[int] = []
  112. self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
  113. self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
  114. self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
  115. self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str)
  116. self._todo: list[tuple[Callable, tuple, dict]] = []
  117. self.tooltips: Tooltips | None = None
  118. precision = (
  119. get_option("styler.format.precision") if precision is None else precision
  120. )
  121. self._display_funcs: DefaultDict[ # maps (row, col) -> format func
  122. tuple[int, int], Callable[[Any], str]
  123. ] = defaultdict(lambda: partial(_default_formatter, precision=precision))
  124. self._display_funcs_index: DefaultDict[ # maps (row, level) -> format func
  125. tuple[int, int], Callable[[Any], str]
  126. ] = defaultdict(lambda: partial(_default_formatter, precision=precision))
  127. self._display_funcs_columns: DefaultDict[ # maps (level, col) -> format func
  128. tuple[int, int], Callable[[Any], str]
  129. ] = defaultdict(lambda: partial(_default_formatter, precision=precision))
  130. def _render(
  131. self,
  132. sparse_index: bool,
  133. sparse_columns: bool,
  134. max_rows: int | None = None,
  135. max_cols: int | None = None,
  136. blank: str = "",
  137. ):
  138. """
  139. Computes and applies styles and then generates the general render dicts.
  140. Also extends the `ctx` and `ctx_index` attributes with those of concatenated
  141. stylers for use within `_translate_latex`
  142. """
  143. self._compute()
  144. dxs = []
  145. ctx_len = len(self.index)
  146. for i, concatenated in enumerate(self.concatenated):
  147. concatenated.hide_index_ = self.hide_index_
  148. concatenated.hidden_columns = self.hidden_columns
  149. foot = f"{self.css['foot']}{i}"
  150. concatenated.css = {
  151. **self.css,
  152. "data": f"{foot}_data",
  153. "row_heading": f"{foot}_row_heading",
  154. "row": f"{foot}_row",
  155. "foot": f"{foot}_foot",
  156. }
  157. dx = concatenated._render(
  158. sparse_index, sparse_columns, max_rows, max_cols, blank
  159. )
  160. dxs.append(dx)
  161. for (r, c), v in concatenated.ctx.items():
  162. self.ctx[(r + ctx_len, c)] = v
  163. for (r, c), v in concatenated.ctx_index.items():
  164. self.ctx_index[(r + ctx_len, c)] = v
  165. ctx_len += len(concatenated.index)
  166. d = self._translate(
  167. sparse_index, sparse_columns, max_rows, max_cols, blank, dxs
  168. )
  169. return d
  170. def _render_html(
  171. self,
  172. sparse_index: bool,
  173. sparse_columns: bool,
  174. max_rows: int | None = None,
  175. max_cols: int | None = None,
  176. **kwargs,
  177. ) -> str:
  178. """
  179. Renders the ``Styler`` including all applied styles to HTML.
  180. Generates a dict with necessary kwargs passed to jinja2 template.
  181. """
  182. d = self._render(sparse_index, sparse_columns, max_rows, max_cols, "&nbsp;")
  183. d.update(kwargs)
  184. return self.template_html.render(
  185. **d,
  186. html_table_tpl=self.template_html_table,
  187. html_style_tpl=self.template_html_style,
  188. )
  189. def _render_latex(
  190. self, sparse_index: bool, sparse_columns: bool, clines: str | None, **kwargs
  191. ) -> str:
  192. """
  193. Render a Styler in latex format
  194. """
  195. d = self._render(sparse_index, sparse_columns, None, None)
  196. self._translate_latex(d, clines=clines)
  197. self.template_latex.globals["parse_wrap"] = _parse_latex_table_wrapping
  198. self.template_latex.globals["parse_table"] = _parse_latex_table_styles
  199. self.template_latex.globals["parse_cell"] = _parse_latex_cell_styles
  200. self.template_latex.globals["parse_header"] = _parse_latex_header_span
  201. d.update(kwargs)
  202. return self.template_latex.render(**d)
  203. def _render_string(
  204. self,
  205. sparse_index: bool,
  206. sparse_columns: bool,
  207. max_rows: int | None = None,
  208. max_cols: int | None = None,
  209. **kwargs,
  210. ) -> str:
  211. """
  212. Render a Styler in string format
  213. """
  214. d = self._render(sparse_index, sparse_columns, max_rows, max_cols)
  215. d.update(kwargs)
  216. return self.template_string.render(**d)
  217. def _compute(self):
  218. """
  219. Execute the style functions built up in `self._todo`.
  220. Relies on the conventions that all style functions go through
  221. .apply or .map. The append styles to apply as tuples of
  222. (application method, *args, **kwargs)
  223. """
  224. self.ctx.clear()
  225. self.ctx_index.clear()
  226. self.ctx_columns.clear()
  227. r = self
  228. for func, args, kwargs in self._todo:
  229. r = func(self)(*args, **kwargs)
  230. return r
  231. def _translate(
  232. self,
  233. sparse_index: bool,
  234. sparse_cols: bool,
  235. max_rows: int | None = None,
  236. max_cols: int | None = None,
  237. blank: str = "&nbsp;",
  238. dxs: list[dict] | None = None,
  239. ):
  240. """
  241. Process Styler data and settings into a dict for template rendering.
  242. Convert data and settings from ``Styler`` attributes such as ``self.data``,
  243. ``self.tooltips`` including applying any methods in ``self._todo``.
  244. Parameters
  245. ----------
  246. sparse_index : bool
  247. Whether to sparsify the index or print all hierarchical index elements.
  248. Upstream defaults are typically to `pandas.options.styler.sparse.index`.
  249. sparse_cols : bool
  250. Whether to sparsify the columns or print all hierarchical column elements.
  251. Upstream defaults are typically to `pandas.options.styler.sparse.columns`.
  252. max_rows, max_cols : int, optional
  253. Specific max rows and cols. max_elements always take precedence in render.
  254. blank : str
  255. Entry to top-left blank cells.
  256. dxs : list[dict]
  257. The render dicts of the concatenated Stylers.
  258. Returns
  259. -------
  260. d : dict
  261. The following structure: {uuid, table_styles, caption, head, body,
  262. cellstyle, table_attributes}
  263. """
  264. if dxs is None:
  265. dxs = []
  266. self.css["blank_value"] = blank
  267. # construct render dict
  268. d = {
  269. "uuid": self.uuid,
  270. "table_styles": format_table_styles(self.table_styles or []),
  271. "caption": self.caption,
  272. }
  273. max_elements = get_option("styler.render.max_elements")
  274. max_rows = max_rows if max_rows else get_option("styler.render.max_rows")
  275. max_cols = max_cols if max_cols else get_option("styler.render.max_columns")
  276. max_rows, max_cols = _get_trimming_maximums(
  277. len(self.data.index),
  278. len(self.data.columns),
  279. max_elements,
  280. max_rows,
  281. max_cols,
  282. )
  283. self.cellstyle_map_columns: DefaultDict[
  284. tuple[CSSPair, ...], list[str]
  285. ] = defaultdict(list)
  286. head = self._translate_header(sparse_cols, max_cols)
  287. d.update({"head": head})
  288. # for sparsifying a MultiIndex and for use with latex clines
  289. idx_lengths = _get_level_lengths(
  290. self.index, sparse_index, max_rows, self.hidden_rows
  291. )
  292. d.update({"index_lengths": idx_lengths})
  293. self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
  294. list
  295. )
  296. self.cellstyle_map_index: DefaultDict[
  297. tuple[CSSPair, ...], list[str]
  298. ] = defaultdict(list)
  299. body: list = self._translate_body(idx_lengths, max_rows, max_cols)
  300. d.update({"body": body})
  301. ctx_maps = {
  302. "cellstyle": "cellstyle_map",
  303. "cellstyle_index": "cellstyle_map_index",
  304. "cellstyle_columns": "cellstyle_map_columns",
  305. } # add the cell_ids styles map to the render dictionary in right format
  306. for k, attr in ctx_maps.items():
  307. map = [
  308. {"props": list(props), "selectors": selectors}
  309. for props, selectors in getattr(self, attr).items()
  310. ]
  311. d.update({k: map})
  312. for dx in dxs: # self.concatenated is not empty
  313. d["body"].extend(dx["body"]) # type: ignore[union-attr]
  314. d["cellstyle"].extend(dx["cellstyle"]) # type: ignore[union-attr]
  315. d["cellstyle_index"].extend( # type: ignore[union-attr]
  316. dx["cellstyle_index"]
  317. )
  318. table_attr = self.table_attributes
  319. if not get_option("styler.html.mathjax"):
  320. table_attr = table_attr or ""
  321. if 'class="' in table_attr:
  322. table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
  323. else:
  324. table_attr += ' class="tex2jax_ignore"'
  325. d.update({"table_attributes": table_attr})
  326. if self.tooltips:
  327. d = self.tooltips._translate(self, d)
  328. return d
  329. def _translate_header(self, sparsify_cols: bool, max_cols: int):
  330. """
  331. Build each <tr> within table <head> as a list
  332. Using the structure:
  333. +----------------------------+---------------+---------------------------+
  334. | index_blanks ... | column_name_0 | column_headers (level_0) |
  335. 1) | .. | .. | .. |
  336. | index_blanks ... | column_name_n | column_headers (level_n) |
  337. +----------------------------+---------------+---------------------------+
  338. 2) | index_names (level_0 to level_n) ... | column_blanks ... |
  339. +----------------------------+---------------+---------------------------+
  340. Parameters
  341. ----------
  342. sparsify_cols : bool
  343. Whether column_headers section will add colspan attributes (>1) to elements.
  344. max_cols : int
  345. Maximum number of columns to render. If exceeded will contain `...` filler.
  346. Returns
  347. -------
  348. head : list
  349. The associated HTML elements needed for template rendering.
  350. """
  351. # for sparsifying a MultiIndex
  352. col_lengths = _get_level_lengths(
  353. self.columns, sparsify_cols, max_cols, self.hidden_columns
  354. )
  355. clabels = self.data.columns.tolist()
  356. if self.data.columns.nlevels == 1:
  357. clabels = [[x] for x in clabels]
  358. clabels = list(zip(*clabels))
  359. head = []
  360. # 1) column headers
  361. for r, hide in enumerate(self.hide_columns_):
  362. if hide or not clabels:
  363. continue
  364. header_row = self._generate_col_header_row(
  365. (r, clabels), max_cols, col_lengths
  366. )
  367. head.append(header_row)
  368. # 2) index names
  369. if (
  370. self.data.index.names
  371. and com.any_not_none(*self.data.index.names)
  372. and not all(self.hide_index_)
  373. and not self.hide_index_names
  374. ):
  375. index_names_row = self._generate_index_names_row(
  376. clabels, max_cols, col_lengths
  377. )
  378. head.append(index_names_row)
  379. return head
  380. def _generate_col_header_row(
  381. self, iter: Sequence, max_cols: int, col_lengths: dict
  382. ):
  383. """
  384. Generate the row containing column headers:
  385. +----------------------------+---------------+---------------------------+
  386. | index_blanks ... | column_name_i | column_headers (level_i) |
  387. +----------------------------+---------------+---------------------------+
  388. Parameters
  389. ----------
  390. iter : tuple
  391. Looping variables from outer scope
  392. max_cols : int
  393. Permissible number of columns
  394. col_lengths :
  395. c
  396. Returns
  397. -------
  398. list of elements
  399. """
  400. r, clabels = iter
  401. # number of index blanks is governed by number of hidden index levels
  402. index_blanks = [
  403. _element("th", self.css["blank"], self.css["blank_value"], True)
  404. ] * (self.index.nlevels - sum(self.hide_index_) - 1)
  405. name = self.data.columns.names[r]
  406. column_name = [
  407. _element(
  408. "th",
  409. (
  410. f"{self.css['blank']} {self.css['level']}{r}"
  411. if name is None
  412. else f"{self.css['index_name']} {self.css['level']}{r}"
  413. ),
  414. name
  415. if (name is not None and not self.hide_column_names)
  416. else self.css["blank_value"],
  417. not all(self.hide_index_),
  418. )
  419. ]
  420. column_headers: list = []
  421. visible_col_count: int = 0
  422. for c, value in enumerate(clabels[r]):
  423. header_element_visible = _is_visible(c, r, col_lengths)
  424. if header_element_visible:
  425. visible_col_count += col_lengths.get((r, c), 0)
  426. if self._check_trim(
  427. visible_col_count,
  428. max_cols,
  429. column_headers,
  430. "th",
  431. f"{self.css['col_heading']} {self.css['level']}{r} "
  432. f"{self.css['col_trim']}",
  433. ):
  434. break
  435. header_element = _element(
  436. "th",
  437. (
  438. f"{self.css['col_heading']} {self.css['level']}{r} "
  439. f"{self.css['col']}{c}"
  440. ),
  441. value,
  442. header_element_visible,
  443. display_value=self._display_funcs_columns[(r, c)](value),
  444. attributes=(
  445. f'colspan="{col_lengths.get((r, c), 0)}"'
  446. if col_lengths.get((r, c), 0) > 1
  447. else ""
  448. ),
  449. )
  450. if self.cell_ids:
  451. header_element["id"] = f"{self.css['level']}{r}_{self.css['col']}{c}"
  452. if (
  453. header_element_visible
  454. and (r, c) in self.ctx_columns
  455. and self.ctx_columns[r, c]
  456. ):
  457. header_element["id"] = f"{self.css['level']}{r}_{self.css['col']}{c}"
  458. self.cellstyle_map_columns[tuple(self.ctx_columns[r, c])].append(
  459. f"{self.css['level']}{r}_{self.css['col']}{c}"
  460. )
  461. column_headers.append(header_element)
  462. return index_blanks + column_name + column_headers
  463. def _generate_index_names_row(
  464. self, iter: Sequence, max_cols: int, col_lengths: dict
  465. ):
  466. """
  467. Generate the row containing index names
  468. +----------------------------+---------------+---------------------------+
  469. | index_names (level_0 to level_n) ... | column_blanks ... |
  470. +----------------------------+---------------+---------------------------+
  471. Parameters
  472. ----------
  473. iter : tuple
  474. Looping variables from outer scope
  475. max_cols : int
  476. Permissible number of columns
  477. Returns
  478. -------
  479. list of elements
  480. """
  481. clabels = iter
  482. index_names = [
  483. _element(
  484. "th",
  485. f"{self.css['index_name']} {self.css['level']}{c}",
  486. self.css["blank_value"] if name is None else name,
  487. not self.hide_index_[c],
  488. )
  489. for c, name in enumerate(self.data.index.names)
  490. ]
  491. column_blanks: list = []
  492. visible_col_count: int = 0
  493. if clabels:
  494. last_level = self.columns.nlevels - 1 # use last level since never sparsed
  495. for c, value in enumerate(clabels[last_level]):
  496. header_element_visible = _is_visible(c, last_level, col_lengths)
  497. if header_element_visible:
  498. visible_col_count += 1
  499. if self._check_trim(
  500. visible_col_count,
  501. max_cols,
  502. column_blanks,
  503. "th",
  504. f"{self.css['blank']} {self.css['col']}{c} {self.css['col_trim']}",
  505. self.css["blank_value"],
  506. ):
  507. break
  508. column_blanks.append(
  509. _element(
  510. "th",
  511. f"{self.css['blank']} {self.css['col']}{c}",
  512. self.css["blank_value"],
  513. c not in self.hidden_columns,
  514. )
  515. )
  516. return index_names + column_blanks
  517. def _translate_body(self, idx_lengths: dict, max_rows: int, max_cols: int):
  518. """
  519. Build each <tr> within table <body> as a list
  520. Use the following structure:
  521. +--------------------------------------------+---------------------------+
  522. | index_header_0 ... index_header_n | data_by_column ... |
  523. +--------------------------------------------+---------------------------+
  524. Also add elements to the cellstyle_map for more efficient grouped elements in
  525. <style></style> block
  526. Parameters
  527. ----------
  528. sparsify_index : bool
  529. Whether index_headers section will add rowspan attributes (>1) to elements.
  530. Returns
  531. -------
  532. body : list
  533. The associated HTML elements needed for template rendering.
  534. """
  535. rlabels = self.data.index.tolist()
  536. if not isinstance(self.data.index, MultiIndex):
  537. rlabels = [[x] for x in rlabels]
  538. body: list = []
  539. visible_row_count: int = 0
  540. for r, row_tup in [
  541. z for z in enumerate(self.data.itertuples()) if z[0] not in self.hidden_rows
  542. ]:
  543. visible_row_count += 1
  544. if self._check_trim(
  545. visible_row_count,
  546. max_rows,
  547. body,
  548. "row",
  549. ):
  550. break
  551. body_row = self._generate_body_row(
  552. (r, row_tup, rlabels), max_cols, idx_lengths
  553. )
  554. body.append(body_row)
  555. return body
  556. def _check_trim(
  557. self,
  558. count: int,
  559. max: int,
  560. obj: list,
  561. element: str,
  562. css: str | None = None,
  563. value: str = "...",
  564. ) -> bool:
  565. """
  566. Indicates whether to break render loops and append a trimming indicator
  567. Parameters
  568. ----------
  569. count : int
  570. The loop count of previous visible items.
  571. max : int
  572. The allowable rendered items in the loop.
  573. obj : list
  574. The current render collection of the rendered items.
  575. element : str
  576. The type of element to append in the case a trimming indicator is needed.
  577. css : str, optional
  578. The css to add to the trimming indicator element.
  579. value : str, optional
  580. The value of the elements display if necessary.
  581. Returns
  582. -------
  583. result : bool
  584. Whether a trimming element was required and appended.
  585. """
  586. if count > max:
  587. if element == "row":
  588. obj.append(self._generate_trimmed_row(max))
  589. else:
  590. obj.append(_element(element, css, value, True, attributes=""))
  591. return True
  592. return False
  593. def _generate_trimmed_row(self, max_cols: int) -> list:
  594. """
  595. When a render has too many rows we generate a trimming row containing "..."
  596. Parameters
  597. ----------
  598. max_cols : int
  599. Number of permissible columns
  600. Returns
  601. -------
  602. list of elements
  603. """
  604. index_headers = [
  605. _element(
  606. "th",
  607. (
  608. f"{self.css['row_heading']} {self.css['level']}{c} "
  609. f"{self.css['row_trim']}"
  610. ),
  611. "...",
  612. not self.hide_index_[c],
  613. attributes="",
  614. )
  615. for c in range(self.data.index.nlevels)
  616. ]
  617. data: list = []
  618. visible_col_count: int = 0
  619. for c, _ in enumerate(self.columns):
  620. data_element_visible = c not in self.hidden_columns
  621. if data_element_visible:
  622. visible_col_count += 1
  623. if self._check_trim(
  624. visible_col_count,
  625. max_cols,
  626. data,
  627. "td",
  628. f"{self.css['data']} {self.css['row_trim']} {self.css['col_trim']}",
  629. ):
  630. break
  631. data.append(
  632. _element(
  633. "td",
  634. f"{self.css['data']} {self.css['col']}{c} {self.css['row_trim']}",
  635. "...",
  636. data_element_visible,
  637. attributes="",
  638. )
  639. )
  640. return index_headers + data
  641. def _generate_body_row(
  642. self,
  643. iter: tuple,
  644. max_cols: int,
  645. idx_lengths: dict,
  646. ):
  647. """
  648. Generate a regular row for the body section of appropriate format.
  649. +--------------------------------------------+---------------------------+
  650. | index_header_0 ... index_header_n | data_by_column ... |
  651. +--------------------------------------------+---------------------------+
  652. Parameters
  653. ----------
  654. iter : tuple
  655. Iterable from outer scope: row number, row data tuple, row index labels.
  656. max_cols : int
  657. Number of permissible columns.
  658. idx_lengths : dict
  659. A map of the sparsification structure of the index
  660. Returns
  661. -------
  662. list of elements
  663. """
  664. r, row_tup, rlabels = iter
  665. index_headers = []
  666. for c, value in enumerate(rlabels[r]):
  667. header_element_visible = (
  668. _is_visible(r, c, idx_lengths) and not self.hide_index_[c]
  669. )
  670. header_element = _element(
  671. "th",
  672. (
  673. f"{self.css['row_heading']} {self.css['level']}{c} "
  674. f"{self.css['row']}{r}"
  675. ),
  676. value,
  677. header_element_visible,
  678. display_value=self._display_funcs_index[(r, c)](value),
  679. attributes=(
  680. f'rowspan="{idx_lengths.get((c, r), 0)}"'
  681. if idx_lengths.get((c, r), 0) > 1
  682. else ""
  683. ),
  684. )
  685. if self.cell_ids:
  686. header_element[
  687. "id"
  688. ] = f"{self.css['level']}{c}_{self.css['row']}{r}" # id is given
  689. if (
  690. header_element_visible
  691. and (r, c) in self.ctx_index
  692. and self.ctx_index[r, c]
  693. ):
  694. # always add id if a style is specified
  695. header_element["id"] = f"{self.css['level']}{c}_{self.css['row']}{r}"
  696. self.cellstyle_map_index[tuple(self.ctx_index[r, c])].append(
  697. f"{self.css['level']}{c}_{self.css['row']}{r}"
  698. )
  699. index_headers.append(header_element)
  700. data: list = []
  701. visible_col_count: int = 0
  702. for c, value in enumerate(row_tup[1:]):
  703. data_element_visible = (
  704. c not in self.hidden_columns and r not in self.hidden_rows
  705. )
  706. if data_element_visible:
  707. visible_col_count += 1
  708. if self._check_trim(
  709. visible_col_count,
  710. max_cols,
  711. data,
  712. "td",
  713. f"{self.css['data']} {self.css['row']}{r} {self.css['col_trim']}",
  714. ):
  715. break
  716. # add custom classes from cell context
  717. cls = ""
  718. if (r, c) in self.cell_context:
  719. cls = " " + self.cell_context[r, c]
  720. data_element = _element(
  721. "td",
  722. (
  723. f"{self.css['data']} {self.css['row']}{r} "
  724. f"{self.css['col']}{c}{cls}"
  725. ),
  726. value,
  727. data_element_visible,
  728. attributes="",
  729. display_value=self._display_funcs[(r, c)](value),
  730. )
  731. if self.cell_ids:
  732. data_element["id"] = f"{self.css['row']}{r}_{self.css['col']}{c}"
  733. if data_element_visible and (r, c) in self.ctx and self.ctx[r, c]:
  734. # always add id if needed due to specified style
  735. data_element["id"] = f"{self.css['row']}{r}_{self.css['col']}{c}"
  736. self.cellstyle_map[tuple(self.ctx[r, c])].append(
  737. f"{self.css['row']}{r}_{self.css['col']}{c}"
  738. )
  739. data.append(data_element)
  740. return index_headers + data
  741. def _translate_latex(self, d: dict, clines: str | None) -> None:
  742. r"""
  743. Post-process the default render dict for the LaTeX template format.
  744. Processing items included are:
  745. - Remove hidden columns from the non-headers part of the body.
  746. - Place cellstyles directly in td cells rather than use cellstyle_map.
  747. - Remove hidden indexes or reinsert missing th elements if part of multiindex
  748. or multirow sparsification (so that \multirow and \multicol work correctly).
  749. """
  750. index_levels = self.index.nlevels
  751. visible_index_level_n = index_levels - sum(self.hide_index_)
  752. d["head"] = [
  753. [
  754. {**col, "cellstyle": self.ctx_columns[r, c - visible_index_level_n]}
  755. for c, col in enumerate(row)
  756. if col["is_visible"]
  757. ]
  758. for r, row in enumerate(d["head"])
  759. ]
  760. def _concatenated_visible_rows(obj, n, row_indices):
  761. """
  762. Extract all visible row indices recursively from concatenated stylers.
  763. """
  764. row_indices.extend(
  765. [r + n for r in range(len(obj.index)) if r not in obj.hidden_rows]
  766. )
  767. n += len(obj.index)
  768. for concatenated in obj.concatenated:
  769. n = _concatenated_visible_rows(concatenated, n, row_indices)
  770. return n
  771. def concatenated_visible_rows(obj):
  772. row_indices: list[int] = []
  773. _concatenated_visible_rows(obj, 0, row_indices)
  774. # TODO try to consolidate the concat visible rows
  775. # methods to a single function / recursion for simplicity
  776. return row_indices
  777. body = []
  778. for r, row in zip(concatenated_visible_rows(self), d["body"]):
  779. # note: cannot enumerate d["body"] because rows were dropped if hidden
  780. # during _translate_body so must zip to acquire the true r-index associated
  781. # with the ctx obj which contains the cell styles.
  782. if all(self.hide_index_):
  783. row_body_headers = []
  784. else:
  785. row_body_headers = [
  786. {
  787. **col,
  788. "display_value": col["display_value"]
  789. if col["is_visible"]
  790. else "",
  791. "cellstyle": self.ctx_index[r, c],
  792. }
  793. for c, col in enumerate(row[:index_levels])
  794. if (col["type"] == "th" and not self.hide_index_[c])
  795. ]
  796. row_body_cells = [
  797. {**col, "cellstyle": self.ctx[r, c]}
  798. for c, col in enumerate(row[index_levels:])
  799. if (col["is_visible"] and col["type"] == "td")
  800. ]
  801. body.append(row_body_headers + row_body_cells)
  802. d["body"] = body
  803. # clines are determined from info on index_lengths and hidden_rows and input
  804. # to a dict defining which row clines should be added in the template.
  805. if clines not in [
  806. None,
  807. "all;data",
  808. "all;index",
  809. "skip-last;data",
  810. "skip-last;index",
  811. ]:
  812. raise ValueError(
  813. f"`clines` value of {clines} is invalid. Should either be None or one "
  814. f"of 'all;data', 'all;index', 'skip-last;data', 'skip-last;index'."
  815. )
  816. if clines is not None:
  817. data_len = len(row_body_cells) if "data" in clines and d["body"] else 0
  818. d["clines"] = defaultdict(list)
  819. visible_row_indexes: list[int] = [
  820. r for r in range(len(self.data.index)) if r not in self.hidden_rows
  821. ]
  822. visible_index_levels: list[int] = [
  823. i for i in range(index_levels) if not self.hide_index_[i]
  824. ]
  825. for rn, r in enumerate(visible_row_indexes):
  826. for lvln, lvl in enumerate(visible_index_levels):
  827. if lvl == index_levels - 1 and "skip-last" in clines:
  828. continue
  829. idx_len = d["index_lengths"].get((lvl, r), None)
  830. if idx_len is not None: # i.e. not a sparsified entry
  831. d["clines"][rn + idx_len].append(
  832. f"\\cline{{{lvln+1}-{len(visible_index_levels)+data_len}}}"
  833. )
  834. def format(
  835. self,
  836. formatter: ExtFormatter | None = None,
  837. subset: Subset | None = None,
  838. na_rep: str | None = None,
  839. precision: int | None = None,
  840. decimal: str = ".",
  841. thousands: str | None = None,
  842. escape: str | None = None,
  843. hyperlinks: str | None = None,
  844. ) -> StylerRenderer:
  845. r"""
  846. Format the text display value of cells.
  847. Parameters
  848. ----------
  849. formatter : str, callable, dict or None
  850. Object to define how values are displayed. See notes.
  851. subset : label, array-like, IndexSlice, optional
  852. A valid 2d input to `DataFrame.loc[<subset>]`, or, in the case of a 1d input
  853. or single key, to `DataFrame.loc[:, <subset>]` where the columns are
  854. prioritised, to limit ``data`` to *before* applying the function.
  855. na_rep : str, optional
  856. Representation for missing values.
  857. If ``na_rep`` is None, no special formatting is applied.
  858. precision : int, optional
  859. Floating point precision to use for display purposes, if not determined by
  860. the specified ``formatter``.
  861. .. versionadded:: 1.3.0
  862. decimal : str, default "."
  863. Character used as decimal separator for floats, complex and integers.
  864. .. versionadded:: 1.3.0
  865. thousands : str, optional, default None
  866. Character used as thousands separator for floats, complex and integers.
  867. .. versionadded:: 1.3.0
  868. escape : str, optional
  869. Use 'html' to replace the characters ``&``, ``<``, ``>``, ``'``, and ``"``
  870. in cell display string with HTML-safe sequences.
  871. Use 'latex' to replace the characters ``&``, ``%``, ``$``, ``#``, ``_``,
  872. ``{``, ``}``, ``~``, ``^``, and ``\`` in the cell display string with
  873. LaTeX-safe sequences.
  874. Use 'latex-math' to replace the characters the same way as in 'latex' mode,
  875. except for math substrings, which either are surrounded
  876. by two characters ``$`` or start with the character ``\(`` and
  877. end with ``\)``. Escaping is done before ``formatter``.
  878. .. versionadded:: 1.3.0
  879. hyperlinks : {"html", "latex"}, optional
  880. Convert string patterns containing https://, http://, ftp:// or www. to
  881. HTML <a> tags as clickable URL hyperlinks if "html", or LaTeX \href
  882. commands if "latex".
  883. .. versionadded:: 1.4.0
  884. Returns
  885. -------
  886. Styler
  887. See Also
  888. --------
  889. Styler.format_index: Format the text display value of index labels.
  890. Notes
  891. -----
  892. This method assigns a formatting function, ``formatter``, to each cell in the
  893. DataFrame. If ``formatter`` is ``None``, then the default formatter is used.
  894. If a callable then that function should take a data value as input and return
  895. a displayable representation, such as a string. If ``formatter`` is
  896. given as a string this is assumed to be a valid Python format specification
  897. and is wrapped to a callable as ``string.format(x)``. If a ``dict`` is given,
  898. keys should correspond to column names, and values should be string or
  899. callable, as above.
  900. The default formatter currently expresses floats and complex numbers with the
  901. pandas display precision unless using the ``precision`` argument here. The
  902. default formatter does not adjust the representation of missing values unless
  903. the ``na_rep`` argument is used.
  904. The ``subset`` argument defines which region to apply the formatting function
  905. to. If the ``formatter`` argument is given in dict form but does not include
  906. all columns within the subset then these columns will have the default formatter
  907. applied. Any columns in the formatter dict excluded from the subset will
  908. be ignored.
  909. When using a ``formatter`` string the dtypes must be compatible, otherwise a
  910. `ValueError` will be raised.
  911. When instantiating a Styler, default formatting can be applied be setting the
  912. ``pandas.options``:
  913. - ``styler.format.formatter``: default None.
  914. - ``styler.format.na_rep``: default None.
  915. - ``styler.format.precision``: default 6.
  916. - ``styler.format.decimal``: default ".".
  917. - ``styler.format.thousands``: default None.
  918. - ``styler.format.escape``: default None.
  919. .. warning::
  920. `Styler.format` is ignored when using the output format `Styler.to_excel`,
  921. since Excel and Python have inherrently different formatting structures.
  922. However, it is possible to use the `number-format` pseudo CSS attribute
  923. to force Excel permissible formatting. See examples.
  924. Examples
  925. --------
  926. Using ``na_rep`` and ``precision`` with the default ``formatter``
  927. >>> df = pd.DataFrame([[np.nan, 1.0, 'A'], [2.0, np.nan, 3.0]])
  928. >>> df.style.format(na_rep='MISS', precision=3) # doctest: +SKIP
  929. 0 1 2
  930. 0 MISS 1.000 A
  931. 1 2.000 MISS 3.000
  932. Using a ``formatter`` specification on consistent column dtypes
  933. >>> df.style.format('{:.2f}', na_rep='MISS', subset=[0,1]) # doctest: +SKIP
  934. 0 1 2
  935. 0 MISS 1.00 A
  936. 1 2.00 MISS 3.000000
  937. Using the default ``formatter`` for unspecified columns
  938. >>> df.style.format({0: '{:.2f}', 1: '£ {:.1f}'}, na_rep='MISS', precision=1)
  939. ... # doctest: +SKIP
  940. 0 1 2
  941. 0 MISS £ 1.0 A
  942. 1 2.00 MISS 3.0
  943. Multiple ``na_rep`` or ``precision`` specifications under the default
  944. ``formatter``.
  945. >>> (df.style.format(na_rep='MISS', precision=1, subset=[0])
  946. ... .format(na_rep='PASS', precision=2, subset=[1, 2])) # doctest: +SKIP
  947. 0 1 2
  948. 0 MISS 1.00 A
  949. 1 2.0 PASS 3.00
  950. Using a callable ``formatter`` function.
  951. >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT'
  952. >>> df.style.format({0: '{:.1f}', 2: func}, precision=4, na_rep='MISS')
  953. ... # doctest: +SKIP
  954. 0 1 2
  955. 0 MISS 1.0000 STRING
  956. 1 2.0 MISS FLOAT
  957. Using a ``formatter`` with HTML ``escape`` and ``na_rep``.
  958. >>> df = pd.DataFrame([['<div></div>', '"A&B"', None]])
  959. >>> s = df.style.format(
  960. ... '<a href="a.com/{0}">{0}</a>', escape="html", na_rep="NA"
  961. ... )
  962. >>> s.to_html() # doctest: +SKIP
  963. ...
  964. <td .. ><a href="a.com/&lt;div&gt;&lt;/div&gt;">&lt;div&gt;&lt;/div&gt;</a></td>
  965. <td .. ><a href="a.com/&#34;A&amp;B&#34;">&#34;A&amp;B&#34;</a></td>
  966. <td .. >NA</td>
  967. ...
  968. Using a ``formatter`` with ``escape`` in 'latex' mode.
  969. >>> df = pd.DataFrame([["123"], ["~ ^"], ["$%#"]])
  970. >>> df.style.format("\\textbf{{{}}}", escape="latex").to_latex()
  971. ... # doctest: +SKIP
  972. \begin{tabular}{ll}
  973. & 0 \\
  974. 0 & \textbf{123} \\
  975. 1 & \textbf{\textasciitilde \space \textasciicircum } \\
  976. 2 & \textbf{\$\%\#} \\
  977. \end{tabular}
  978. Applying ``escape`` in 'latex-math' mode. In the example below
  979. we enter math mode using the character ``$``.
  980. >>> df = pd.DataFrame([[r"$\sum_{i=1}^{10} a_i$ a~b $\alpha \
  981. ... = \frac{\beta}{\zeta^2}$"], ["%#^ $ \$x^2 $"]])
  982. >>> df.style.format(escape="latex-math").to_latex()
  983. ... # doctest: +SKIP
  984. \begin{tabular}{ll}
  985. & 0 \\
  986. 0 & $\sum_{i=1}^{10} a_i$ a\textasciitilde b $\alpha = \frac{\beta}{\zeta^2}$ \\
  987. 1 & \%\#\textasciicircum \space $ \$x^2 $ \\
  988. \end{tabular}
  989. We can use the character ``\(`` to enter math mode and the character ``\)``
  990. to close math mode.
  991. >>> df = pd.DataFrame([[r"\(\sum_{i=1}^{10} a_i\) a~b \(\alpha \
  992. ... = \frac{\beta}{\zeta^2}\)"], ["%#^ \( \$x^2 \)"]])
  993. >>> df.style.format(escape="latex-math").to_latex()
  994. ... # doctest: +SKIP
  995. \begin{tabular}{ll}
  996. & 0 \\
  997. 0 & \(\sum_{i=1}^{10} a_i\) a\textasciitilde b \(\alpha
  998. = \frac{\beta}{\zeta^2}\) \\
  999. 1 & \%\#\textasciicircum \space \( \$x^2 \) \\
  1000. \end{tabular}
  1001. If we have in one DataFrame cell a combination of both shorthands
  1002. for math formulas, the shorthand with the sign ``$`` will be applied.
  1003. >>> df = pd.DataFrame([[r"\( x^2 \) $x^2$"], \
  1004. ... [r"$\frac{\beta}{\zeta}$ \(\frac{\beta}{\zeta}\)"]])
  1005. >>> df.style.format(escape="latex-math").to_latex()
  1006. ... # doctest: +SKIP
  1007. \begin{tabular}{ll}
  1008. & 0 \\
  1009. 0 & \textbackslash ( x\textasciicircum 2 \textbackslash ) $x^2$ \\
  1010. 1 & $\frac{\beta}{\zeta}$ \textbackslash (\textbackslash
  1011. frac\{\textbackslash beta\}\{\textbackslash zeta\}\textbackslash ) \\
  1012. \end{tabular}
  1013. Pandas defines a `number-format` pseudo CSS attribute instead of the `.format`
  1014. method to create `to_excel` permissible formatting. Note that semi-colons are
  1015. CSS protected characters but used as separators in Excel's format string.
  1016. Replace semi-colons with the section separator character (ASCII-245) when
  1017. defining the formatting here.
  1018. >>> df = pd.DataFrame({"A": [1, 0, -1]})
  1019. >>> pseudo_css = "number-format: 0§[Red](0)§-§@;"
  1020. >>> filename = "formatted_file.xlsx"
  1021. >>> df.style.map(lambda v: pseudo_css).to_excel(filename) # doctest: +SKIP
  1022. .. figure:: ../../_static/style/format_excel_css.png
  1023. """
  1024. if all(
  1025. (
  1026. formatter is None,
  1027. subset is None,
  1028. precision is None,
  1029. decimal == ".",
  1030. thousands is None,
  1031. na_rep is None,
  1032. escape is None,
  1033. hyperlinks is None,
  1034. )
  1035. ):
  1036. self._display_funcs.clear()
  1037. return self # clear the formatter / revert to default and avoid looping
  1038. subset = slice(None) if subset is None else subset
  1039. subset = non_reducing_slice(subset)
  1040. data = self.data.loc[subset]
  1041. if not isinstance(formatter, dict):
  1042. formatter = {col: formatter for col in data.columns}
  1043. cis = self.columns.get_indexer_for(data.columns)
  1044. ris = self.index.get_indexer_for(data.index)
  1045. for ci in cis:
  1046. format_func = _maybe_wrap_formatter(
  1047. formatter.get(self.columns[ci]),
  1048. na_rep=na_rep,
  1049. precision=precision,
  1050. decimal=decimal,
  1051. thousands=thousands,
  1052. escape=escape,
  1053. hyperlinks=hyperlinks,
  1054. )
  1055. for ri in ris:
  1056. self._display_funcs[(ri, ci)] = format_func
  1057. return self
  1058. def format_index(
  1059. self,
  1060. formatter: ExtFormatter | None = None,
  1061. axis: Axis = 0,
  1062. level: Level | list[Level] | None = None,
  1063. na_rep: str | None = None,
  1064. precision: int | None = None,
  1065. decimal: str = ".",
  1066. thousands: str | None = None,
  1067. escape: str | None = None,
  1068. hyperlinks: str | None = None,
  1069. ) -> StylerRenderer:
  1070. r"""
  1071. Format the text display value of index labels or column headers.
  1072. .. versionadded:: 1.4.0
  1073. Parameters
  1074. ----------
  1075. formatter : str, callable, dict or None
  1076. Object to define how values are displayed. See notes.
  1077. axis : {0, "index", 1, "columns"}
  1078. Whether to apply the formatter to the index or column headers.
  1079. level : int, str, list
  1080. The level(s) over which to apply the generic formatter.
  1081. na_rep : str, optional
  1082. Representation for missing values.
  1083. If ``na_rep`` is None, no special formatting is applied.
  1084. precision : int, optional
  1085. Floating point precision to use for display purposes, if not determined by
  1086. the specified ``formatter``.
  1087. decimal : str, default "."
  1088. Character used as decimal separator for floats, complex and integers.
  1089. thousands : str, optional, default None
  1090. Character used as thousands separator for floats, complex and integers.
  1091. escape : str, optional
  1092. Use 'html' to replace the characters ``&``, ``<``, ``>``, ``'``, and ``"``
  1093. in cell display string with HTML-safe sequences.
  1094. Use 'latex' to replace the characters ``&``, ``%``, ``$``, ``#``, ``_``,
  1095. ``{``, ``}``, ``~``, ``^``, and ``\`` in the cell display string with
  1096. LaTeX-safe sequences.
  1097. Escaping is done before ``formatter``.
  1098. hyperlinks : {"html", "latex"}, optional
  1099. Convert string patterns containing https://, http://, ftp:// or www. to
  1100. HTML <a> tags as clickable URL hyperlinks if "html", or LaTeX \href
  1101. commands if "latex".
  1102. Returns
  1103. -------
  1104. Styler
  1105. See Also
  1106. --------
  1107. Styler.format: Format the text display value of data cells.
  1108. Notes
  1109. -----
  1110. This method assigns a formatting function, ``formatter``, to each level label
  1111. in the DataFrame's index or column headers. If ``formatter`` is ``None``,
  1112. then the default formatter is used.
  1113. If a callable then that function should take a label value as input and return
  1114. a displayable representation, such as a string. If ``formatter`` is
  1115. given as a string this is assumed to be a valid Python format specification
  1116. and is wrapped to a callable as ``string.format(x)``. If a ``dict`` is given,
  1117. keys should correspond to MultiIndex level numbers or names, and values should
  1118. be string or callable, as above.
  1119. The default formatter currently expresses floats and complex numbers with the
  1120. pandas display precision unless using the ``precision`` argument here. The
  1121. default formatter does not adjust the representation of missing values unless
  1122. the ``na_rep`` argument is used.
  1123. The ``level`` argument defines which levels of a MultiIndex to apply the
  1124. method to. If the ``formatter`` argument is given in dict form but does
  1125. not include all levels within the level argument then these unspecified levels
  1126. will have the default formatter applied. Any levels in the formatter dict
  1127. specifically excluded from the level argument will be ignored.
  1128. When using a ``formatter`` string the dtypes must be compatible, otherwise a
  1129. `ValueError` will be raised.
  1130. .. warning::
  1131. `Styler.format_index` is ignored when using the output format
  1132. `Styler.to_excel`, since Excel and Python have inherrently different
  1133. formatting structures.
  1134. However, it is possible to use the `number-format` pseudo CSS attribute
  1135. to force Excel permissible formatting. See documentation for `Styler.format`.
  1136. Examples
  1137. --------
  1138. Using ``na_rep`` and ``precision`` with the default ``formatter``
  1139. >>> df = pd.DataFrame([[1, 2, 3]], columns=[2.0, np.nan, 4.0])
  1140. >>> df.style.format_index(axis=1, na_rep='MISS', precision=3) # doctest: +SKIP
  1141. 2.000 MISS 4.000
  1142. 0 1 2 3
  1143. Using a ``formatter`` specification on consistent dtypes in a level
  1144. >>> df.style.format_index('{:.2f}', axis=1, na_rep='MISS') # doctest: +SKIP
  1145. 2.00 MISS 4.00
  1146. 0 1 2 3
  1147. Using the default ``formatter`` for unspecified levels
  1148. >>> df = pd.DataFrame([[1, 2, 3]],
  1149. ... columns=pd.MultiIndex.from_arrays([["a", "a", "b"],[2, np.nan, 4]]))
  1150. >>> df.style.format_index({0: lambda v: v.upper()}, axis=1, precision=1)
  1151. ... # doctest: +SKIP
  1152. A B
  1153. 2.0 nan 4.0
  1154. 0 1 2 3
  1155. Using a callable ``formatter`` function.
  1156. >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT'
  1157. >>> df.style.format_index(func, axis=1, na_rep='MISS')
  1158. ... # doctest: +SKIP
  1159. STRING STRING
  1160. FLOAT MISS FLOAT
  1161. 0 1 2 3
  1162. Using a ``formatter`` with HTML ``escape`` and ``na_rep``.
  1163. >>> df = pd.DataFrame([[1, 2, 3]], columns=['"A"', 'A&B', None])
  1164. >>> s = df.style.format_index('$ {0}', axis=1, escape="html", na_rep="NA")
  1165. ... # doctest: +SKIP
  1166. <th .. >$ &#34;A&#34;</th>
  1167. <th .. >$ A&amp;B</th>
  1168. <th .. >NA</td>
  1169. ...
  1170. Using a ``formatter`` with LaTeX ``escape``.
  1171. >>> df = pd.DataFrame([[1, 2, 3]], columns=["123", "~", "$%#"])
  1172. >>> df.style.format_index("\\textbf{{{}}}", escape="latex", axis=1).to_latex()
  1173. ... # doctest: +SKIP
  1174. \begin{tabular}{lrrr}
  1175. {} & {\textbf{123}} & {\textbf{\textasciitilde }} & {\textbf{\$\%\#}} \\
  1176. 0 & 1 & 2 & 3 \\
  1177. \end{tabular}
  1178. """
  1179. axis = self.data._get_axis_number(axis)
  1180. if axis == 0:
  1181. display_funcs_, obj = self._display_funcs_index, self.index
  1182. else:
  1183. display_funcs_, obj = self._display_funcs_columns, self.columns
  1184. levels_ = refactor_levels(level, obj)
  1185. if all(
  1186. (
  1187. formatter is None,
  1188. level is None,
  1189. precision is None,
  1190. decimal == ".",
  1191. thousands is None,
  1192. na_rep is None,
  1193. escape is None,
  1194. hyperlinks is None,
  1195. )
  1196. ):
  1197. display_funcs_.clear()
  1198. return self # clear the formatter / revert to default and avoid looping
  1199. if not isinstance(formatter, dict):
  1200. formatter = {level: formatter for level in levels_}
  1201. else:
  1202. formatter = {
  1203. obj._get_level_number(level): formatter_
  1204. for level, formatter_ in formatter.items()
  1205. }
  1206. for lvl in levels_:
  1207. format_func = _maybe_wrap_formatter(
  1208. formatter.get(lvl),
  1209. na_rep=na_rep,
  1210. precision=precision,
  1211. decimal=decimal,
  1212. thousands=thousands,
  1213. escape=escape,
  1214. hyperlinks=hyperlinks,
  1215. )
  1216. for idx in [(i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj))]:
  1217. display_funcs_[idx] = format_func
  1218. return self
  1219. def relabel_index(
  1220. self,
  1221. labels: Sequence | Index,
  1222. axis: Axis = 0,
  1223. level: Level | list[Level] | None = None,
  1224. ) -> StylerRenderer:
  1225. r"""
  1226. Relabel the index, or column header, keys to display a set of specified values.
  1227. .. versionadded:: 1.5.0
  1228. Parameters
  1229. ----------
  1230. labels : list-like or Index
  1231. New labels to display. Must have same length as the underlying values not
  1232. hidden.
  1233. axis : {"index", 0, "columns", 1}
  1234. Apply to the index or columns.
  1235. level : int, str, list, optional
  1236. The level(s) over which to apply the new labels. If `None` will apply
  1237. to all levels of an Index or MultiIndex which are not hidden.
  1238. Returns
  1239. -------
  1240. Styler
  1241. See Also
  1242. --------
  1243. Styler.format_index: Format the text display value of index or column headers.
  1244. Styler.hide: Hide the index, column headers, or specified data from display.
  1245. Notes
  1246. -----
  1247. As part of Styler, this method allows the display of an index to be
  1248. completely user-specified without affecting the underlying DataFrame data,
  1249. index, or column headers. This means that the flexibility of indexing is
  1250. maintained whilst the final display is customisable.
  1251. Since Styler is designed to be progressively constructed with method chaining,
  1252. this method is adapted to react to the **currently specified hidden elements**.
  1253. This is useful because it means one does not have to specify all the new
  1254. labels if the majority of an index, or column headers, have already been hidden.
  1255. The following produce equivalent display (note the length of ``labels`` in
  1256. each case).
  1257. .. code-block:: python
  1258. # relabel first, then hide
  1259. df = pd.DataFrame({"col": ["a", "b", "c"]})
  1260. df.style.relabel_index(["A", "B", "C"]).hide([0,1])
  1261. # hide first, then relabel
  1262. df = pd.DataFrame({"col": ["a", "b", "c"]})
  1263. df.style.hide([0,1]).relabel_index(["C"])
  1264. This method should be used, rather than :meth:`Styler.format_index`, in one of
  1265. the following cases (see examples):
  1266. - A specified set of labels are required which are not a function of the
  1267. underlying index keys.
  1268. - The function of the underlying index keys requires a counter variable,
  1269. such as those available upon enumeration.
  1270. Examples
  1271. --------
  1272. Basic use
  1273. >>> df = pd.DataFrame({"col": ["a", "b", "c"]})
  1274. >>> df.style.relabel_index(["A", "B", "C"]) # doctest: +SKIP
  1275. col
  1276. A a
  1277. B b
  1278. C c
  1279. Chaining with pre-hidden elements
  1280. >>> df.style.hide([0,1]).relabel_index(["C"]) # doctest: +SKIP
  1281. col
  1282. C c
  1283. Using a MultiIndex
  1284. >>> midx = pd.MultiIndex.from_product([[0, 1], [0, 1], [0, 1]])
  1285. >>> df = pd.DataFrame({"col": list(range(8))}, index=midx)
  1286. >>> styler = df.style # doctest: +SKIP
  1287. col
  1288. 0 0 0 0
  1289. 1 1
  1290. 1 0 2
  1291. 1 3
  1292. 1 0 0 4
  1293. 1 5
  1294. 1 0 6
  1295. 1 7
  1296. >>> styler.hide((midx.get_level_values(0)==0)|(midx.get_level_values(1)==0))
  1297. ... # doctest: +SKIP
  1298. >>> styler.hide(level=[0,1]) # doctest: +SKIP
  1299. >>> styler.relabel_index(["binary6", "binary7"]) # doctest: +SKIP
  1300. col
  1301. binary6 6
  1302. binary7 7
  1303. We can also achieve the above by indexing first and then re-labeling
  1304. >>> styler = df.loc[[(1,1,0), (1,1,1)]].style
  1305. >>> styler.hide(level=[0,1]).relabel_index(["binary6", "binary7"])
  1306. ... # doctest: +SKIP
  1307. col
  1308. binary6 6
  1309. binary7 7
  1310. Defining a formatting function which uses an enumeration counter. Also note
  1311. that the value of the index key is passed in the case of string labels so it
  1312. can also be inserted into the label, using curly brackets (or double curly
  1313. brackets if the string if pre-formatted),
  1314. >>> df = pd.DataFrame({"samples": np.random.rand(10)})
  1315. >>> styler = df.loc[np.random.randint(0,10,3)].style
  1316. >>> styler.relabel_index([f"sample{i+1} ({{}})" for i in range(3)])
  1317. ... # doctest: +SKIP
  1318. samples
  1319. sample1 (5) 0.315811
  1320. sample2 (0) 0.495941
  1321. sample3 (2) 0.067946
  1322. """
  1323. axis = self.data._get_axis_number(axis)
  1324. if axis == 0:
  1325. display_funcs_, obj = self._display_funcs_index, self.index
  1326. hidden_labels, hidden_lvls = self.hidden_rows, self.hide_index_
  1327. else:
  1328. display_funcs_, obj = self._display_funcs_columns, self.columns
  1329. hidden_labels, hidden_lvls = self.hidden_columns, self.hide_columns_
  1330. visible_len = len(obj) - len(set(hidden_labels))
  1331. if len(labels) != visible_len:
  1332. raise ValueError(
  1333. "``labels`` must be of length equal to the number of "
  1334. f"visible labels along ``axis`` ({visible_len})."
  1335. )
  1336. if level is None:
  1337. level = [i for i in range(obj.nlevels) if not hidden_lvls[i]]
  1338. levels_ = refactor_levels(level, obj)
  1339. def alias_(x, value):
  1340. if isinstance(value, str):
  1341. return value.format(x)
  1342. return value
  1343. for ai, i in enumerate([i for i in range(len(obj)) if i not in hidden_labels]):
  1344. if len(levels_) == 1:
  1345. idx = (i, levels_[0]) if axis == 0 else (levels_[0], i)
  1346. display_funcs_[idx] = partial(alias_, value=labels[ai])
  1347. else:
  1348. for aj, lvl in enumerate(levels_):
  1349. idx = (i, lvl) if axis == 0 else (lvl, i)
  1350. display_funcs_[idx] = partial(alias_, value=labels[ai][aj])
  1351. return self
  1352. def _element(
  1353. html_element: str,
  1354. html_class: str | None,
  1355. value: Any,
  1356. is_visible: bool,
  1357. **kwargs,
  1358. ) -> dict:
  1359. """
  1360. Template to return container with information for a <td></td> or <th></th> element.
  1361. """
  1362. if "display_value" not in kwargs:
  1363. kwargs["display_value"] = value
  1364. return {
  1365. "type": html_element,
  1366. "value": value,
  1367. "class": html_class,
  1368. "is_visible": is_visible,
  1369. **kwargs,
  1370. }
  1371. def _get_trimming_maximums(
  1372. rn,
  1373. cn,
  1374. max_elements,
  1375. max_rows=None,
  1376. max_cols=None,
  1377. scaling_factor: float = 0.8,
  1378. ) -> tuple[int, int]:
  1379. """
  1380. Recursively reduce the number of rows and columns to satisfy max elements.
  1381. Parameters
  1382. ----------
  1383. rn, cn : int
  1384. The number of input rows / columns
  1385. max_elements : int
  1386. The number of allowable elements
  1387. max_rows, max_cols : int, optional
  1388. Directly specify an initial maximum rows or columns before compression.
  1389. scaling_factor : float
  1390. Factor at which to reduce the number of rows / columns to fit.
  1391. Returns
  1392. -------
  1393. rn, cn : tuple
  1394. New rn and cn values that satisfy the max_elements constraint
  1395. """
  1396. def scale_down(rn, cn):
  1397. if cn >= rn:
  1398. return rn, int(cn * scaling_factor)
  1399. else:
  1400. return int(rn * scaling_factor), cn
  1401. if max_rows:
  1402. rn = max_rows if rn > max_rows else rn
  1403. if max_cols:
  1404. cn = max_cols if cn > max_cols else cn
  1405. while rn * cn > max_elements:
  1406. rn, cn = scale_down(rn, cn)
  1407. return rn, cn
  1408. def _get_level_lengths(
  1409. index: Index,
  1410. sparsify: bool,
  1411. max_index: int,
  1412. hidden_elements: Sequence[int] | None = None,
  1413. ):
  1414. """
  1415. Given an index, find the level length for each element.
  1416. Parameters
  1417. ----------
  1418. index : Index
  1419. Index or columns to determine lengths of each element
  1420. sparsify : bool
  1421. Whether to hide or show each distinct element in a MultiIndex
  1422. max_index : int
  1423. The maximum number of elements to analyse along the index due to trimming
  1424. hidden_elements : sequence of int
  1425. Index positions of elements hidden from display in the index affecting
  1426. length
  1427. Returns
  1428. -------
  1429. Dict :
  1430. Result is a dictionary of (level, initial_position): span
  1431. """
  1432. if isinstance(index, MultiIndex):
  1433. levels = index._format_multi(sparsify=lib.no_default, include_names=False)
  1434. else:
  1435. levels = index._format_flat(include_name=False)
  1436. if hidden_elements is None:
  1437. hidden_elements = []
  1438. lengths = {}
  1439. if not isinstance(index, MultiIndex):
  1440. for i, value in enumerate(levels):
  1441. if i not in hidden_elements:
  1442. lengths[(0, i)] = 1
  1443. return lengths
  1444. for i, lvl in enumerate(levels):
  1445. visible_row_count = 0 # used to break loop due to display trimming
  1446. for j, row in enumerate(lvl):
  1447. if visible_row_count > max_index:
  1448. break
  1449. if not sparsify:
  1450. # then lengths will always equal 1 since no aggregation.
  1451. if j not in hidden_elements:
  1452. lengths[(i, j)] = 1
  1453. visible_row_count += 1
  1454. elif (row is not lib.no_default) and (j not in hidden_elements):
  1455. # this element has not been sparsified so must be the start of section
  1456. last_label = j
  1457. lengths[(i, last_label)] = 1
  1458. visible_row_count += 1
  1459. elif row is not lib.no_default:
  1460. # even if the above is hidden, keep track of it in case length > 1 and
  1461. # later elements are visible
  1462. last_label = j
  1463. lengths[(i, last_label)] = 0
  1464. elif j not in hidden_elements:
  1465. # then element must be part of sparsified section and is visible
  1466. visible_row_count += 1
  1467. if visible_row_count > max_index:
  1468. break # do not add a length since the render trim limit reached
  1469. if lengths[(i, last_label)] == 0:
  1470. # if previous iteration was first-of-section but hidden then offset
  1471. last_label = j
  1472. lengths[(i, last_label)] = 1
  1473. else:
  1474. # else add to previous iteration
  1475. lengths[(i, last_label)] += 1
  1476. non_zero_lengths = {
  1477. element: length for element, length in lengths.items() if length >= 1
  1478. }
  1479. return non_zero_lengths
  1480. def _is_visible(idx_row, idx_col, lengths) -> bool:
  1481. """
  1482. Index -> {(idx_row, idx_col): bool}).
  1483. """
  1484. return (idx_col, idx_row) in lengths
  1485. def format_table_styles(styles: CSSStyles) -> CSSStyles:
  1486. """
  1487. looks for multiple CSS selectors and separates them:
  1488. [{'selector': 'td, th', 'props': 'a:v;'}]
  1489. ---> [{'selector': 'td', 'props': 'a:v;'},
  1490. {'selector': 'th', 'props': 'a:v;'}]
  1491. """
  1492. return [
  1493. {"selector": selector, "props": css_dict["props"]}
  1494. for css_dict in styles
  1495. for selector in css_dict["selector"].split(",")
  1496. ]
  1497. def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any:
  1498. """
  1499. Format the display of a value
  1500. Parameters
  1501. ----------
  1502. x : Any
  1503. Input variable to be formatted
  1504. precision : Int
  1505. Floating point precision used if ``x`` is float or complex.
  1506. thousands : bool, default False
  1507. Whether to group digits with thousands separated with ",".
  1508. Returns
  1509. -------
  1510. value : Any
  1511. Matches input type, or string if input is float or complex or int with sep.
  1512. """
  1513. if is_float(x) or is_complex(x):
  1514. return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}"
  1515. elif is_integer(x):
  1516. return f"{x:,}" if thousands else str(x)
  1517. return x
  1518. def _wrap_decimal_thousands(
  1519. formatter: Callable, decimal: str, thousands: str | None
  1520. ) -> Callable:
  1521. """
  1522. Takes a string formatting function and wraps logic to deal with thousands and
  1523. decimal parameters, in the case that they are non-standard and that the input
  1524. is a (float, complex, int).
  1525. """
  1526. def wrapper(x):
  1527. if is_float(x) or is_integer(x) or is_complex(x):
  1528. if decimal != "." and thousands is not None and thousands != ",":
  1529. return (
  1530. formatter(x)
  1531. .replace(",", "§_§-") # rare string to avoid "," <-> "." clash.
  1532. .replace(".", decimal)
  1533. .replace("§_§-", thousands)
  1534. )
  1535. elif decimal != "." and (thousands is None or thousands == ","):
  1536. return formatter(x).replace(".", decimal)
  1537. elif decimal == "." and thousands is not None and thousands != ",":
  1538. return formatter(x).replace(",", thousands)
  1539. return formatter(x)
  1540. return wrapper
  1541. def _str_escape(x, escape):
  1542. """if escaping: only use on str, else return input"""
  1543. if isinstance(x, str):
  1544. if escape == "html":
  1545. return escape_html(x)
  1546. elif escape == "latex":
  1547. return _escape_latex(x)
  1548. elif escape == "latex-math":
  1549. return _escape_latex_math(x)
  1550. else:
  1551. raise ValueError(
  1552. f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \
  1553. got {escape}"
  1554. )
  1555. return x
  1556. def _render_href(x, format):
  1557. """uses regex to detect a common URL pattern and converts to href tag in format."""
  1558. if isinstance(x, str):
  1559. if format == "html":
  1560. href = '<a href="{0}" target="_blank">{0}</a>'
  1561. elif format == "latex":
  1562. href = r"\href{{{0}}}{{{0}}}"
  1563. else:
  1564. raise ValueError("``hyperlinks`` format can only be 'html' or 'latex'")
  1565. pat = r"((http|ftp)s?:\/\/|www.)[\w/\-?=%.:@]+\.[\w/\-&?=%.,':;~!@#$*()\[\]]+"
  1566. return re.sub(pat, lambda m: href.format(m.group(0)), x)
  1567. return x
  1568. def _maybe_wrap_formatter(
  1569. formatter: BaseFormatter | None = None,
  1570. na_rep: str | None = None,
  1571. precision: int | None = None,
  1572. decimal: str = ".",
  1573. thousands: str | None = None,
  1574. escape: str | None = None,
  1575. hyperlinks: str | None = None,
  1576. ) -> Callable:
  1577. """
  1578. Allows formatters to be expressed as str, callable or None, where None returns
  1579. a default formatting function. wraps with na_rep, and precision where they are
  1580. available.
  1581. """
  1582. # Get initial func from input string, input callable, or from default factory
  1583. if isinstance(formatter, str):
  1584. func_0 = lambda x: formatter.format(x)
  1585. elif callable(formatter):
  1586. func_0 = formatter
  1587. elif formatter is None:
  1588. precision = (
  1589. get_option("styler.format.precision") if precision is None else precision
  1590. )
  1591. func_0 = partial(
  1592. _default_formatter, precision=precision, thousands=(thousands is not None)
  1593. )
  1594. else:
  1595. raise TypeError(f"'formatter' expected str or callable, got {type(formatter)}")
  1596. # Replace chars if escaping
  1597. if escape is not None:
  1598. func_1 = lambda x: func_0(_str_escape(x, escape=escape))
  1599. else:
  1600. func_1 = func_0
  1601. # Replace decimals and thousands if non-standard inputs detected
  1602. if decimal != "." or (thousands is not None and thousands != ","):
  1603. func_2 = _wrap_decimal_thousands(func_1, decimal=decimal, thousands=thousands)
  1604. else:
  1605. func_2 = func_1
  1606. # Render links
  1607. if hyperlinks is not None:
  1608. func_3 = lambda x: func_2(_render_href(x, format=hyperlinks))
  1609. else:
  1610. func_3 = func_2
  1611. # Replace missing values if na_rep
  1612. if na_rep is None:
  1613. return func_3
  1614. else:
  1615. return lambda x: na_rep if (isna(x) is True) else func_3(x)
  1616. def non_reducing_slice(slice_: Subset):
  1617. """
  1618. Ensure that a slice doesn't reduce to a Series or Scalar.
  1619. Any user-passed `subset` should have this called on it
  1620. to make sure we're always working with DataFrames.
  1621. """
  1622. # default to column slice, like DataFrame
  1623. # ['A', 'B'] -> IndexSlices[:, ['A', 'B']]
  1624. kinds = (ABCSeries, np.ndarray, Index, list, str)
  1625. if isinstance(slice_, kinds):
  1626. slice_ = IndexSlice[:, slice_]
  1627. def pred(part) -> bool:
  1628. """
  1629. Returns
  1630. -------
  1631. bool
  1632. True if slice does *not* reduce,
  1633. False if `part` is a tuple.
  1634. """
  1635. # true when slice does *not* reduce, False when part is a tuple,
  1636. # i.e. MultiIndex slice
  1637. if isinstance(part, tuple):
  1638. # GH#39421 check for sub-slice:
  1639. return any((isinstance(s, slice) or is_list_like(s)) for s in part)
  1640. else:
  1641. return isinstance(part, slice) or is_list_like(part)
  1642. if not is_list_like(slice_):
  1643. if not isinstance(slice_, slice):
  1644. # a 1-d slice, like df.loc[1]
  1645. slice_ = [[slice_]]
  1646. else:
  1647. # slice(a, b, c)
  1648. slice_ = [slice_] # to tuplize later
  1649. else:
  1650. # error: Item "slice" of "Union[slice, Sequence[Any]]" has no attribute
  1651. # "__iter__" (not iterable) -> is specifically list_like in conditional
  1652. slice_ = [p if pred(p) else [p] for p in slice_] # type: ignore[union-attr]
  1653. return tuple(slice_)
  1654. def maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList:
  1655. """
  1656. Convert css-string to sequence of tuples format if needed.
  1657. 'color:red; border:1px solid black;' -> [('color', 'red'),
  1658. ('border','1px solid red')]
  1659. """
  1660. if isinstance(style, str):
  1661. s = style.split(";")
  1662. try:
  1663. return [
  1664. (x.split(":")[0].strip(), x.split(":")[1].strip())
  1665. for x in s
  1666. if x.strip() != ""
  1667. ]
  1668. except IndexError:
  1669. raise ValueError(
  1670. "Styles supplied as string must follow CSS rule formats, "
  1671. f"for example 'attr: val;'. '{style}' was given."
  1672. )
  1673. return style
  1674. def refactor_levels(
  1675. level: Level | list[Level] | None,
  1676. obj: Index,
  1677. ) -> list[int]:
  1678. """
  1679. Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``.
  1680. Parameters
  1681. ----------
  1682. level : int, str, list
  1683. Original ``level`` arg supplied to above methods.
  1684. obj:
  1685. Either ``self.index`` or ``self.columns``
  1686. Returns
  1687. -------
  1688. list : refactored arg with a list of levels to hide
  1689. """
  1690. if level is None:
  1691. levels_: list[int] = list(range(obj.nlevels))
  1692. elif isinstance(level, int):
  1693. levels_ = [level]
  1694. elif isinstance(level, str):
  1695. levels_ = [obj._get_level_number(level)]
  1696. elif isinstance(level, list):
  1697. levels_ = [
  1698. obj._get_level_number(lev) if not isinstance(lev, int) else lev
  1699. for lev in level
  1700. ]
  1701. else:
  1702. raise ValueError("`level` must be of type `int`, `str` or list of such")
  1703. return levels_
  1704. class Tooltips:
  1705. """
  1706. An extension to ``Styler`` that allows for and manipulates tooltips on hover
  1707. of ``<td>`` cells in the HTML result.
  1708. Parameters
  1709. ----------
  1710. css_name: str, default "pd-t"
  1711. Name of the CSS class that controls visualisation of tooltips.
  1712. css_props: list-like, default; see Notes
  1713. List of (attr, value) tuples defining properties of the CSS class.
  1714. tooltips: DataFrame, default empty
  1715. DataFrame of strings aligned with underlying Styler data for tooltip
  1716. display.
  1717. Notes
  1718. -----
  1719. The default properties for the tooltip CSS class are:
  1720. - visibility: hidden
  1721. - position: absolute
  1722. - z-index: 1
  1723. - background-color: black
  1724. - color: white
  1725. - transform: translate(-20px, -20px)
  1726. Hidden visibility is a key prerequisite to the hover functionality, and should
  1727. always be included in any manual properties specification.
  1728. """
  1729. def __init__(
  1730. self,
  1731. css_props: CSSProperties = [
  1732. ("visibility", "hidden"),
  1733. ("position", "absolute"),
  1734. ("z-index", 1),
  1735. ("background-color", "black"),
  1736. ("color", "white"),
  1737. ("transform", "translate(-20px, -20px)"),
  1738. ],
  1739. css_name: str = "pd-t",
  1740. tooltips: DataFrame = DataFrame(),
  1741. ) -> None:
  1742. self.class_name = css_name
  1743. self.class_properties = css_props
  1744. self.tt_data = tooltips
  1745. self.table_styles: CSSStyles = []
  1746. @property
  1747. def _class_styles(self):
  1748. """
  1749. Combine the ``_Tooltips`` CSS class name and CSS properties to the format
  1750. required to extend the underlying ``Styler`` `table_styles` to allow
  1751. tooltips to render in HTML.
  1752. Returns
  1753. -------
  1754. styles : List
  1755. """
  1756. return [
  1757. {
  1758. "selector": f".{self.class_name}",
  1759. "props": maybe_convert_css_to_tuples(self.class_properties),
  1760. }
  1761. ]
  1762. def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str):
  1763. """
  1764. For every table data-cell that has a valid tooltip (not None, NaN or
  1765. empty string) must create two pseudo CSS entries for the specific
  1766. <td> element id which are added to overall table styles:
  1767. an on hover visibility change and a content change
  1768. dependent upon the user's chosen display string.
  1769. For example:
  1770. [{"selector": "T__row1_col1:hover .pd-t",
  1771. "props": [("visibility", "visible")]},
  1772. {"selector": "T__row1_col1 .pd-t::after",
  1773. "props": [("content", "Some Valid Text String")]}]
  1774. Parameters
  1775. ----------
  1776. uuid: str
  1777. The uuid of the Styler instance
  1778. name: str
  1779. The css-name of the class used for styling tooltips
  1780. row : int
  1781. The row index of the specified tooltip string data
  1782. col : int
  1783. The col index of the specified tooltip string data
  1784. text : str
  1785. The textual content of the tooltip to be displayed in HTML.
  1786. Returns
  1787. -------
  1788. pseudo_css : List
  1789. """
  1790. selector_id = "#T_" + uuid + "_row" + str(row) + "_col" + str(col)
  1791. return [
  1792. {
  1793. "selector": selector_id + f":hover .{name}",
  1794. "props": [("visibility", "visible")],
  1795. },
  1796. {
  1797. "selector": selector_id + f" .{name}::after",
  1798. "props": [("content", f'"{text}"')],
  1799. },
  1800. ]
  1801. def _translate(self, styler: StylerRenderer, d: dict):
  1802. """
  1803. Mutate the render dictionary to allow for tooltips:
  1804. - Add ``<span>`` HTML element to each data cells ``display_value``. Ignores
  1805. headers.
  1806. - Add table level CSS styles to control pseudo classes.
  1807. Parameters
  1808. ----------
  1809. styler_data : DataFrame
  1810. Underlying ``Styler`` DataFrame used for reindexing.
  1811. uuid : str
  1812. The underlying ``Styler`` uuid for CSS id.
  1813. d : dict
  1814. The dictionary prior to final render
  1815. Returns
  1816. -------
  1817. render_dict : Dict
  1818. """
  1819. self.tt_data = self.tt_data.reindex_like(styler.data)
  1820. if self.tt_data.empty:
  1821. return d
  1822. name = self.class_name
  1823. mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip
  1824. self.table_styles = [
  1825. style
  1826. for sublist in [
  1827. self._pseudo_css(styler.uuid, name, i, j, str(self.tt_data.iloc[i, j]))
  1828. for i in range(len(self.tt_data.index))
  1829. for j in range(len(self.tt_data.columns))
  1830. if not (
  1831. mask.iloc[i, j]
  1832. or i in styler.hidden_rows
  1833. or j in styler.hidden_columns
  1834. )
  1835. ]
  1836. for style in sublist
  1837. ]
  1838. if self.table_styles:
  1839. # add span class to every cell only if at least 1 non-empty tooltip
  1840. for row in d["body"]:
  1841. for item in row:
  1842. if item["type"] == "td":
  1843. item["display_value"] = (
  1844. str(item["display_value"])
  1845. + f'<span class="{self.class_name}"></span>'
  1846. )
  1847. d["table_styles"].extend(self._class_styles)
  1848. d["table_styles"].extend(self.table_styles)
  1849. return d
  1850. def _parse_latex_table_wrapping(table_styles: CSSStyles, caption: str | None) -> bool:
  1851. """
  1852. Indicate whether LaTeX {tabular} should be wrapped with a {table} environment.
  1853. Parses the `table_styles` and detects any selectors which must be included outside
  1854. of {tabular}, i.e. indicating that wrapping must occur, and therefore return True,
  1855. or if a caption exists and requires similar.
  1856. """
  1857. IGNORED_WRAPPERS = ["toprule", "midrule", "bottomrule", "column_format"]
  1858. # ignored selectors are included with {tabular} so do not need wrapping
  1859. return (
  1860. table_styles is not None
  1861. and any(d["selector"] not in IGNORED_WRAPPERS for d in table_styles)
  1862. ) or caption is not None
  1863. def _parse_latex_table_styles(table_styles: CSSStyles, selector: str) -> str | None:
  1864. """
  1865. Return the first 'props' 'value' from ``tables_styles`` identified by ``selector``.
  1866. Examples
  1867. --------
  1868. >>> table_styles = [{'selector': 'foo', 'props': [('attr','value')]},
  1869. ... {'selector': 'bar', 'props': [('attr', 'overwritten')]},
  1870. ... {'selector': 'bar', 'props': [('a1', 'baz'), ('a2', 'ignore')]}]
  1871. >>> _parse_latex_table_styles(table_styles, selector='bar')
  1872. 'baz'
  1873. Notes
  1874. -----
  1875. The replacement of "§" with ":" is to avoid the CSS problem where ":" has structural
  1876. significance and cannot be used in LaTeX labels, but is often required by them.
  1877. """
  1878. for style in table_styles[::-1]: # in reverse for most recently applied style
  1879. if style["selector"] == selector:
  1880. return str(style["props"][0][1]).replace("§", ":")
  1881. return None
  1882. def _parse_latex_cell_styles(
  1883. latex_styles: CSSList, display_value: str, convert_css: bool = False
  1884. ) -> str:
  1885. r"""
  1886. Mutate the ``display_value`` string including LaTeX commands from ``latex_styles``.
  1887. This method builds a recursive latex chain of commands based on the
  1888. CSSList input, nested around ``display_value``.
  1889. If a CSS style is given as ('<command>', '<options>') this is translated to
  1890. '\<command><options>{display_value}', and this value is treated as the
  1891. display value for the next iteration.
  1892. The most recent style forms the inner component, for example for styles:
  1893. `[('c1', 'o1'), ('c2', 'o2')]` this returns: `\c1o1{\c2o2{display_value}}`
  1894. Sometimes latex commands have to be wrapped with curly braces in different ways:
  1895. We create some parsing flags to identify the different behaviours:
  1896. - `--rwrap` : `\<command><options>{<display_value>}`
  1897. - `--wrap` : `{\<command><options> <display_value>}`
  1898. - `--nowrap` : `\<command><options> <display_value>`
  1899. - `--lwrap` : `{\<command><options>} <display_value>`
  1900. - `--dwrap` : `{\<command><options>}{<display_value>}`
  1901. For example for styles:
  1902. `[('c1', 'o1--wrap'), ('c2', 'o2')]` this returns: `{\c1o1 \c2o2{display_value}}
  1903. """
  1904. if convert_css:
  1905. latex_styles = _parse_latex_css_conversion(latex_styles)
  1906. for command, options in latex_styles[::-1]: # in reverse for most recent style
  1907. formatter = {
  1908. "--wrap": f"{{\\{command}--to_parse {display_value}}}",
  1909. "--nowrap": f"\\{command}--to_parse {display_value}",
  1910. "--lwrap": f"{{\\{command}--to_parse}} {display_value}",
  1911. "--rwrap": f"\\{command}--to_parse{{{display_value}}}",
  1912. "--dwrap": f"{{\\{command}--to_parse}}{{{display_value}}}",
  1913. }
  1914. display_value = f"\\{command}{options} {display_value}"
  1915. for arg in ["--nowrap", "--wrap", "--lwrap", "--rwrap", "--dwrap"]:
  1916. if arg in str(options):
  1917. display_value = formatter[arg].replace(
  1918. "--to_parse", _parse_latex_options_strip(value=options, arg=arg)
  1919. )
  1920. break # only ever one purposeful entry
  1921. return display_value
  1922. def _parse_latex_header_span(
  1923. cell: dict[str, Any],
  1924. multirow_align: str,
  1925. multicol_align: str,
  1926. wrap: bool = False,
  1927. convert_css: bool = False,
  1928. ) -> str:
  1929. r"""
  1930. Refactor the cell `display_value` if a 'colspan' or 'rowspan' attribute is present.
  1931. 'rowspan' and 'colspan' do not occur simultaneouly. If they are detected then
  1932. the `display_value` is altered to a LaTeX `multirow` or `multicol` command
  1933. respectively, with the appropriate cell-span.
  1934. ``wrap`` is used to enclose the `display_value` in braces which is needed for
  1935. column headers using an siunitx package.
  1936. Requires the package {multirow}, whereas multicol support is usually built in
  1937. to the {tabular} environment.
  1938. Examples
  1939. --------
  1940. >>> cell = {'cellstyle': '', 'display_value':'text', 'attributes': 'colspan="3"'}
  1941. >>> _parse_latex_header_span(cell, 't', 'c')
  1942. '\\multicolumn{3}{c}{text}'
  1943. """
  1944. display_val = _parse_latex_cell_styles(
  1945. cell["cellstyle"], cell["display_value"], convert_css
  1946. )
  1947. if "attributes" in cell:
  1948. attrs = cell["attributes"]
  1949. if 'colspan="' in attrs:
  1950. colspan = attrs[attrs.find('colspan="') + 9 :] # len('colspan="') = 9
  1951. colspan = int(colspan[: colspan.find('"')])
  1952. if "naive-l" == multicol_align:
  1953. out = f"{{{display_val}}}" if wrap else f"{display_val}"
  1954. blanks = " & {}" if wrap else " &"
  1955. return out + blanks * (colspan - 1)
  1956. elif "naive-r" == multicol_align:
  1957. out = f"{{{display_val}}}" if wrap else f"{display_val}"
  1958. blanks = "{} & " if wrap else "& "
  1959. return blanks * (colspan - 1) + out
  1960. return f"\\multicolumn{{{colspan}}}{{{multicol_align}}}{{{display_val}}}"
  1961. elif 'rowspan="' in attrs:
  1962. if multirow_align == "naive":
  1963. return display_val
  1964. rowspan = attrs[attrs.find('rowspan="') + 9 :]
  1965. rowspan = int(rowspan[: rowspan.find('"')])
  1966. return f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}{{{display_val}}}"
  1967. if wrap:
  1968. return f"{{{display_val}}}"
  1969. else:
  1970. return display_val
  1971. def _parse_latex_options_strip(value: str | float, arg: str) -> str:
  1972. """
  1973. Strip a css_value which may have latex wrapping arguments, css comment identifiers,
  1974. and whitespaces, to a valid string for latex options parsing.
  1975. For example: 'red /* --wrap */ ' --> 'red'
  1976. """
  1977. return str(value).replace(arg, "").replace("/*", "").replace("*/", "").strip()
  1978. def _parse_latex_css_conversion(styles: CSSList) -> CSSList:
  1979. """
  1980. Convert CSS (attribute,value) pairs to equivalent LaTeX (command,options) pairs.
  1981. Ignore conversion if tagged with `--latex` option, skipped if no conversion found.
  1982. """
  1983. def font_weight(value, arg):
  1984. if value in ("bold", "bolder"):
  1985. return "bfseries", f"{arg}"
  1986. return None
  1987. def font_style(value, arg):
  1988. if value == "italic":
  1989. return "itshape", f"{arg}"
  1990. if value == "oblique":
  1991. return "slshape", f"{arg}"
  1992. return None
  1993. def color(value, user_arg, command, comm_arg):
  1994. """
  1995. CSS colors have 5 formats to process:
  1996. - 6 digit hex code: "#ff23ee" --> [HTML]{FF23EE}
  1997. - 3 digit hex code: "#f0e" --> [HTML]{FF00EE}
  1998. - rgba: rgba(128, 255, 0, 0.5) --> [rgb]{0.502, 1.000, 0.000}
  1999. - rgb: rgb(128, 255, 0,) --> [rbg]{0.502, 1.000, 0.000}
  2000. - string: red --> {red}
  2001. Additionally rgb or rgba can be expressed in % which is also parsed.
  2002. """
  2003. arg = user_arg if user_arg != "" else comm_arg
  2004. if value[0] == "#" and len(value) == 7: # color is hex code
  2005. return command, f"[HTML]{{{value[1:].upper()}}}{arg}"
  2006. if value[0] == "#" and len(value) == 4: # color is short hex code
  2007. val = f"{value[1].upper()*2}{value[2].upper()*2}{value[3].upper()*2}"
  2008. return command, f"[HTML]{{{val}}}{arg}"
  2009. elif value[:3] == "rgb": # color is rgb or rgba
  2010. r = re.findall("(?<=\\()[0-9\\s%]+(?=,)", value)[0].strip()
  2011. r = float(r[:-1]) / 100 if "%" in r else int(r) / 255
  2012. g = re.findall("(?<=,)[0-9\\s%]+(?=,)", value)[0].strip()
  2013. g = float(g[:-1]) / 100 if "%" in g else int(g) / 255
  2014. if value[3] == "a": # color is rgba
  2015. b = re.findall("(?<=,)[0-9\\s%]+(?=,)", value)[1].strip()
  2016. else: # color is rgb
  2017. b = re.findall("(?<=,)[0-9\\s%]+(?=\\))", value)[0].strip()
  2018. b = float(b[:-1]) / 100 if "%" in b else int(b) / 255
  2019. return command, f"[rgb]{{{r:.3f}, {g:.3f}, {b:.3f}}}{arg}"
  2020. else:
  2021. return command, f"{{{value}}}{arg}" # color is likely string-named
  2022. CONVERTED_ATTRIBUTES: dict[str, Callable] = {
  2023. "font-weight": font_weight,
  2024. "background-color": partial(color, command="cellcolor", comm_arg="--lwrap"),
  2025. "color": partial(color, command="color", comm_arg=""),
  2026. "font-style": font_style,
  2027. }
  2028. latex_styles: CSSList = []
  2029. for attribute, value in styles:
  2030. if isinstance(value, str) and "--latex" in value:
  2031. # return the style without conversion but drop '--latex'
  2032. latex_styles.append((attribute, value.replace("--latex", "")))
  2033. if attribute in CONVERTED_ATTRIBUTES:
  2034. arg = ""
  2035. for x in ["--wrap", "--nowrap", "--lwrap", "--dwrap", "--rwrap"]:
  2036. if x in str(value):
  2037. arg, value = x, _parse_latex_options_strip(value, x)
  2038. break
  2039. latex_style = CONVERTED_ATTRIBUTES[attribute](value, arg)
  2040. if latex_style is not None:
  2041. latex_styles.extend([latex_style])
  2042. return latex_styles
  2043. def _escape_latex(s: str) -> str:
  2044. r"""
  2045. Replace the characters ``&``, ``%``, ``$``, ``#``, ``_``, ``{``, ``}``,
  2046. ``~``, ``^``, and ``\`` in the string with LaTeX-safe sequences.
  2047. Use this if you need to display text that might contain such characters in LaTeX.
  2048. Parameters
  2049. ----------
  2050. s : str
  2051. Input to be escaped
  2052. Return
  2053. ------
  2054. str :
  2055. Escaped string
  2056. """
  2057. return (
  2058. s.replace("\\", "ab2§=§8yz") # rare string for final conversion: avoid \\ clash
  2059. .replace("ab2§=§8yz ", "ab2§=§8yz\\space ") # since \backslash gobbles spaces
  2060. .replace("&", "\\&")
  2061. .replace("%", "\\%")
  2062. .replace("$", "\\$")
  2063. .replace("#", "\\#")
  2064. .replace("_", "\\_")
  2065. .replace("{", "\\{")
  2066. .replace("}", "\\}")
  2067. .replace("~ ", "~\\space ") # since \textasciitilde gobbles spaces
  2068. .replace("~", "\\textasciitilde ")
  2069. .replace("^ ", "^\\space ") # since \textasciicircum gobbles spaces
  2070. .replace("^", "\\textasciicircum ")
  2071. .replace("ab2§=§8yz", "\\textbackslash ")
  2072. )
  2073. def _math_mode_with_dollar(s: str) -> str:
  2074. r"""
  2075. All characters in LaTeX math mode are preserved.
  2076. The substrings in LaTeX math mode, which start with
  2077. the character ``$`` and end with ``$``, are preserved
  2078. without escaping. Otherwise regular LaTeX escaping applies.
  2079. Parameters
  2080. ----------
  2081. s : str
  2082. Input to be escaped
  2083. Return
  2084. ------
  2085. str :
  2086. Escaped string
  2087. """
  2088. s = s.replace(r"\$", r"rt8§=§7wz")
  2089. pattern = re.compile(r"\$.*?\$")
  2090. pos = 0
  2091. ps = pattern.search(s, pos)
  2092. res = []
  2093. while ps:
  2094. res.append(_escape_latex(s[pos : ps.span()[0]]))
  2095. res.append(ps.group())
  2096. pos = ps.span()[1]
  2097. ps = pattern.search(s, pos)
  2098. res.append(_escape_latex(s[pos : len(s)]))
  2099. return "".join(res).replace(r"rt8§=§7wz", r"\$")
  2100. def _math_mode_with_parentheses(s: str) -> str:
  2101. r"""
  2102. All characters in LaTeX math mode are preserved.
  2103. The substrings in LaTeX math mode, which start with
  2104. the character ``\(`` and end with ``\)``, are preserved
  2105. without escaping. Otherwise regular LaTeX escaping applies.
  2106. Parameters
  2107. ----------
  2108. s : str
  2109. Input to be escaped
  2110. Return
  2111. ------
  2112. str :
  2113. Escaped string
  2114. """
  2115. s = s.replace(r"\(", r"LEFT§=§6yzLEFT").replace(r"\)", r"RIGHTab5§=§RIGHT")
  2116. res = []
  2117. for item in re.split(r"LEFT§=§6yz|ab5§=§RIGHT", s):
  2118. if item.startswith("LEFT") and item.endswith("RIGHT"):
  2119. res.append(item.replace("LEFT", r"\(").replace("RIGHT", r"\)"))
  2120. elif "LEFT" in item and "RIGHT" in item:
  2121. res.append(
  2122. _escape_latex(item).replace("LEFT", r"\(").replace("RIGHT", r"\)")
  2123. )
  2124. else:
  2125. res.append(
  2126. _escape_latex(item)
  2127. .replace("LEFT", r"\textbackslash (")
  2128. .replace("RIGHT", r"\textbackslash )")
  2129. )
  2130. return "".join(res)
  2131. def _escape_latex_math(s: str) -> str:
  2132. r"""
  2133. All characters in LaTeX math mode are preserved.
  2134. The substrings in LaTeX math mode, which either are surrounded
  2135. by two characters ``$`` or start with the character ``\(`` and end with ``\)``,
  2136. are preserved without escaping. Otherwise regular LaTeX escaping applies.
  2137. Parameters
  2138. ----------
  2139. s : str
  2140. Input to be escaped
  2141. Return
  2142. ------
  2143. str :
  2144. Escaped string
  2145. """
  2146. s = s.replace(r"\$", r"rt8§=§7wz")
  2147. ps_d = re.compile(r"\$.*?\$").search(s, 0)
  2148. ps_p = re.compile(r"\(.*?\)").search(s, 0)
  2149. mode = []
  2150. if ps_d:
  2151. mode.append(ps_d.span()[0])
  2152. if ps_p:
  2153. mode.append(ps_p.span()[0])
  2154. if len(mode) == 0:
  2155. return _escape_latex(s.replace(r"rt8§=§7wz", r"\$"))
  2156. if s[mode[0]] == r"$":
  2157. return _math_mode_with_dollar(s.replace(r"rt8§=§7wz", r"\$"))
  2158. if s[mode[0] - 1 : mode[0] + 1] == r"\(":
  2159. return _math_mode_with_parentheses(s.replace(r"rt8§=§7wz", r"\$"))
  2160. else:
  2161. return _escape_latex(s.replace(r"rt8§=§7wz", r"\$"))