test_style.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576
  1. import contextlib
  2. import copy
  3. import re
  4. from textwrap import dedent
  5. import numpy as np
  6. import pytest
  7. from pandas import (
  8. DataFrame,
  9. IndexSlice,
  10. MultiIndex,
  11. Series,
  12. option_context,
  13. )
  14. import pandas._testing as tm
  15. jinja2 = pytest.importorskip("jinja2")
  16. from pandas.io.formats.style import ( # isort:skip
  17. Styler,
  18. )
  19. from pandas.io.formats.style_render import (
  20. _get_level_lengths,
  21. _get_trimming_maximums,
  22. maybe_convert_css_to_tuples,
  23. non_reducing_slice,
  24. )
  25. @pytest.fixture
  26. def mi_df():
  27. return DataFrame(
  28. [[1, 2], [3, 4]],
  29. index=MultiIndex.from_product([["i0"], ["i1_a", "i1_b"]]),
  30. columns=MultiIndex.from_product([["c0"], ["c1_a", "c1_b"]]),
  31. dtype=int,
  32. )
  33. @pytest.fixture
  34. def mi_styler(mi_df):
  35. return Styler(mi_df, uuid_len=0)
  36. @pytest.fixture
  37. def mi_styler_comp(mi_styler):
  38. # comprehensively add features to mi_styler
  39. mi_styler = mi_styler._copy(deepcopy=True)
  40. mi_styler.css = {**mi_styler.css, **{"row": "ROW", "col": "COL"}}
  41. mi_styler.uuid_len = 5
  42. mi_styler.uuid = "abcde"
  43. mi_styler.set_caption("capt")
  44. mi_styler.set_table_styles([{"selector": "a", "props": "a:v;"}])
  45. mi_styler.hide(axis="columns")
  46. mi_styler.hide([("c0", "c1_a")], axis="columns", names=True)
  47. mi_styler.hide(axis="index")
  48. mi_styler.hide([("i0", "i1_a")], axis="index", names=True)
  49. mi_styler.set_table_attributes('class="box"')
  50. other = mi_styler.data.agg(["mean"])
  51. other.index = MultiIndex.from_product([[""], other.index])
  52. mi_styler.concat(other.style)
  53. mi_styler.format(na_rep="MISSING", precision=3)
  54. mi_styler.format_index(precision=2, axis=0)
  55. mi_styler.format_index(precision=4, axis=1)
  56. mi_styler.highlight_max(axis=None)
  57. mi_styler.applymap_index(lambda x: "color: white;", axis=0)
  58. mi_styler.applymap_index(lambda x: "color: black;", axis=1)
  59. mi_styler.set_td_classes(
  60. DataFrame(
  61. [["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns
  62. )
  63. )
  64. mi_styler.set_tooltips(
  65. DataFrame(
  66. [["a2", "b2"], ["a2", "c2"]],
  67. index=mi_styler.index,
  68. columns=mi_styler.columns,
  69. )
  70. )
  71. return mi_styler
  72. @pytest.fixture
  73. def blank_value():
  74. return " "
  75. @pytest.fixture
  76. def df():
  77. np.random.seed(24)
  78. df = DataFrame({"A": [0, 1], "B": np.random.randn(2)})
  79. return df
  80. @pytest.fixture
  81. def styler(df):
  82. np.random.seed(24)
  83. df = DataFrame({"A": [0, 1], "B": np.random.randn(2)})
  84. return Styler(df)
  85. @pytest.mark.parametrize(
  86. "sparse_columns, exp_cols",
  87. [
  88. (
  89. True,
  90. [
  91. {"is_visible": True, "attributes": 'colspan="2"', "value": "c0"},
  92. {"is_visible": False, "attributes": "", "value": "c0"},
  93. ],
  94. ),
  95. (
  96. False,
  97. [
  98. {"is_visible": True, "attributes": "", "value": "c0"},
  99. {"is_visible": True, "attributes": "", "value": "c0"},
  100. ],
  101. ),
  102. ],
  103. )
  104. def test_mi_styler_sparsify_columns(mi_styler, sparse_columns, exp_cols):
  105. exp_l1_c0 = {"is_visible": True, "attributes": "", "display_value": "c1_a"}
  106. exp_l1_c1 = {"is_visible": True, "attributes": "", "display_value": "c1_b"}
  107. ctx = mi_styler._translate(True, sparse_columns)
  108. assert exp_cols[0].items() <= ctx["head"][0][2].items()
  109. assert exp_cols[1].items() <= ctx["head"][0][3].items()
  110. assert exp_l1_c0.items() <= ctx["head"][1][2].items()
  111. assert exp_l1_c1.items() <= ctx["head"][1][3].items()
  112. @pytest.mark.parametrize(
  113. "sparse_index, exp_rows",
  114. [
  115. (
  116. True,
  117. [
  118. {"is_visible": True, "attributes": 'rowspan="2"', "value": "i0"},
  119. {"is_visible": False, "attributes": "", "value": "i0"},
  120. ],
  121. ),
  122. (
  123. False,
  124. [
  125. {"is_visible": True, "attributes": "", "value": "i0"},
  126. {"is_visible": True, "attributes": "", "value": "i0"},
  127. ],
  128. ),
  129. ],
  130. )
  131. def test_mi_styler_sparsify_index(mi_styler, sparse_index, exp_rows):
  132. exp_l1_r0 = {"is_visible": True, "attributes": "", "display_value": "i1_a"}
  133. exp_l1_r1 = {"is_visible": True, "attributes": "", "display_value": "i1_b"}
  134. ctx = mi_styler._translate(sparse_index, True)
  135. assert exp_rows[0].items() <= ctx["body"][0][0].items()
  136. assert exp_rows[1].items() <= ctx["body"][1][0].items()
  137. assert exp_l1_r0.items() <= ctx["body"][0][1].items()
  138. assert exp_l1_r1.items() <= ctx["body"][1][1].items()
  139. def test_mi_styler_sparsify_options(mi_styler):
  140. with option_context("styler.sparse.index", False):
  141. html1 = mi_styler.to_html()
  142. with option_context("styler.sparse.index", True):
  143. html2 = mi_styler.to_html()
  144. assert html1 != html2
  145. with option_context("styler.sparse.columns", False):
  146. html1 = mi_styler.to_html()
  147. with option_context("styler.sparse.columns", True):
  148. html2 = mi_styler.to_html()
  149. assert html1 != html2
  150. @pytest.mark.parametrize(
  151. "rn, cn, max_els, max_rows, max_cols, exp_rn, exp_cn",
  152. [
  153. (100, 100, 100, None, None, 12, 6), # reduce to (12, 6) < 100 elements
  154. (1000, 3, 750, None, None, 250, 3), # dynamically reduce rows to 250, keep cols
  155. (4, 1000, 500, None, None, 4, 125), # dynamically reduce cols to 125, keep rows
  156. (1000, 3, 750, 10, None, 10, 3), # overwrite above dynamics with max_row
  157. (4, 1000, 500, None, 5, 4, 5), # overwrite above dynamics with max_col
  158. (100, 100, 700, 50, 50, 25, 25), # rows cols below given maxes so < 700 elmts
  159. ],
  160. )
  161. def test_trimming_maximum(rn, cn, max_els, max_rows, max_cols, exp_rn, exp_cn):
  162. rn, cn = _get_trimming_maximums(
  163. rn, cn, max_els, max_rows, max_cols, scaling_factor=0.5
  164. )
  165. assert (rn, cn) == (exp_rn, exp_cn)
  166. @pytest.mark.parametrize(
  167. "option, val",
  168. [
  169. ("styler.render.max_elements", 6),
  170. ("styler.render.max_rows", 3),
  171. ],
  172. )
  173. def test_render_trimming_rows(option, val):
  174. # test auto and specific trimming of rows
  175. df = DataFrame(np.arange(120).reshape(60, 2))
  176. with option_context(option, val):
  177. ctx = df.style._translate(True, True)
  178. assert len(ctx["head"][0]) == 3 # index + 2 data cols
  179. assert len(ctx["body"]) == 4 # 3 data rows + trimming row
  180. assert len(ctx["body"][0]) == 3 # index + 2 data cols
  181. @pytest.mark.parametrize(
  182. "option, val",
  183. [
  184. ("styler.render.max_elements", 6),
  185. ("styler.render.max_columns", 2),
  186. ],
  187. )
  188. def test_render_trimming_cols(option, val):
  189. # test auto and specific trimming of cols
  190. df = DataFrame(np.arange(30).reshape(3, 10))
  191. with option_context(option, val):
  192. ctx = df.style._translate(True, True)
  193. assert len(ctx["head"][0]) == 4 # index + 2 data cols + trimming col
  194. assert len(ctx["body"]) == 3 # 3 data rows
  195. assert len(ctx["body"][0]) == 4 # index + 2 data cols + trimming col
  196. def test_render_trimming_mi():
  197. midx = MultiIndex.from_product([[1, 2], [1, 2, 3]])
  198. df = DataFrame(np.arange(36).reshape(6, 6), columns=midx, index=midx)
  199. with option_context("styler.render.max_elements", 4):
  200. ctx = df.style._translate(True, True)
  201. assert len(ctx["body"][0]) == 5 # 2 indexes + 2 data cols + trimming row
  202. assert {"attributes": 'rowspan="2"'}.items() <= ctx["body"][0][0].items()
  203. assert {"class": "data row0 col_trim"}.items() <= ctx["body"][0][4].items()
  204. assert {"class": "data row_trim col_trim"}.items() <= ctx["body"][2][4].items()
  205. assert len(ctx["body"]) == 3 # 2 data rows + trimming row
  206. def test_render_empty_mi():
  207. # GH 43305
  208. df = DataFrame(index=MultiIndex.from_product([["A"], [0, 1]], names=[None, "one"]))
  209. expected = dedent(
  210. """\
  211. >
  212. <thead>
  213. <tr>
  214. <th class="index_name level0" >&nbsp;</th>
  215. <th class="index_name level1" >one</th>
  216. </tr>
  217. </thead>
  218. """
  219. )
  220. assert expected in df.style.to_html()
  221. @pytest.mark.parametrize("comprehensive", [True, False])
  222. @pytest.mark.parametrize("render", [True, False])
  223. @pytest.mark.parametrize("deepcopy", [True, False])
  224. def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp):
  225. styler = mi_styler_comp if comprehensive else mi_styler
  226. styler.uuid_len = 5
  227. s2 = copy.deepcopy(styler) if deepcopy else copy.copy(styler) # make copy and check
  228. assert s2 is not styler
  229. if render:
  230. styler.to_html()
  231. excl = [
  232. "cellstyle_map", # render time vars..
  233. "cellstyle_map_columns",
  234. "cellstyle_map_index",
  235. "template_latex", # render templates are class level
  236. "template_html",
  237. "template_html_style",
  238. "template_html_table",
  239. ]
  240. if not deepcopy: # check memory locations are equal for all included attributes
  241. for attr in [a for a in styler.__dict__ if (not callable(a) and a not in excl)]:
  242. assert id(getattr(s2, attr)) == id(getattr(styler, attr))
  243. else: # check memory locations are different for nested or mutable vars
  244. shallow = [
  245. "data",
  246. "columns",
  247. "index",
  248. "uuid_len",
  249. "uuid",
  250. "caption",
  251. "cell_ids",
  252. "hide_index_",
  253. "hide_columns_",
  254. "hide_index_names",
  255. "hide_column_names",
  256. "table_attributes",
  257. ]
  258. for attr in shallow:
  259. assert id(getattr(s2, attr)) == id(getattr(styler, attr))
  260. for attr in [
  261. a
  262. for a in styler.__dict__
  263. if (not callable(a) and a not in excl and a not in shallow)
  264. ]:
  265. if getattr(s2, attr) is None:
  266. assert id(getattr(s2, attr)) == id(getattr(styler, attr))
  267. else:
  268. assert id(getattr(s2, attr)) != id(getattr(styler, attr))
  269. def test_clear(mi_styler_comp):
  270. # NOTE: if this test fails for new features then 'mi_styler_comp' should be updated
  271. # to ensure proper testing of the 'copy', 'clear', 'export' methods with new feature
  272. # GH 40675
  273. styler = mi_styler_comp
  274. styler._compute() # execute applied methods
  275. clean_copy = Styler(styler.data, uuid=styler.uuid)
  276. excl = [
  277. "data",
  278. "index",
  279. "columns",
  280. "uuid",
  281. "uuid_len", # uuid is set to be the same on styler and clean_copy
  282. "cell_ids",
  283. "cellstyle_map", # execution time only
  284. "cellstyle_map_columns", # execution time only
  285. "cellstyle_map_index", # execution time only
  286. "template_latex", # render templates are class level
  287. "template_html",
  288. "template_html_style",
  289. "template_html_table",
  290. ]
  291. # tests vars are not same vals on obj and clean copy before clear (except for excl)
  292. for attr in [a for a in styler.__dict__ if not (callable(a) or a in excl)]:
  293. res = getattr(styler, attr) == getattr(clean_copy, attr)
  294. if hasattr(res, "__iter__") and len(res) > 0:
  295. assert not all(res) # some element in iterable differs
  296. elif hasattr(res, "__iter__") and len(res) == 0:
  297. pass # empty array
  298. else:
  299. assert not res # explicit var differs
  300. # test vars have same vales on obj and clean copy after clearing
  301. styler.clear()
  302. for attr in [a for a in styler.__dict__ if not callable(a)]:
  303. res = getattr(styler, attr) == getattr(clean_copy, attr)
  304. assert all(res) if hasattr(res, "__iter__") else res
  305. def test_export(mi_styler_comp, mi_styler):
  306. exp_attrs = [
  307. "_todo",
  308. "hide_index_",
  309. "hide_index_names",
  310. "hide_columns_",
  311. "hide_column_names",
  312. "table_attributes",
  313. "table_styles",
  314. "css",
  315. ]
  316. for attr in exp_attrs:
  317. check = getattr(mi_styler, attr) == getattr(mi_styler_comp, attr)
  318. assert not (
  319. all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check
  320. )
  321. export = mi_styler_comp.export()
  322. used = mi_styler.use(export)
  323. for attr in exp_attrs:
  324. check = getattr(used, attr) == getattr(mi_styler_comp, attr)
  325. assert all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check
  326. used.to_html()
  327. def test_hide_raises(mi_styler):
  328. msg = "`subset` and `level` cannot be passed simultaneously"
  329. with pytest.raises(ValueError, match=msg):
  330. mi_styler.hide(axis="index", subset="something", level="something else")
  331. msg = "`level` must be of type `int`, `str` or list of such"
  332. with pytest.raises(ValueError, match=msg):
  333. mi_styler.hide(axis="index", level={"bad": 1, "type": 2})
  334. @pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
  335. def test_hide_index_level(mi_styler, level):
  336. mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"]
  337. ctx = mi_styler.hide(axis="index", level=level)._translate(False, True)
  338. assert len(ctx["head"][0]) == 3
  339. assert len(ctx["head"][1]) == 3
  340. assert len(ctx["head"][2]) == 4
  341. assert ctx["head"][2][0]["is_visible"]
  342. assert not ctx["head"][2][1]["is_visible"]
  343. assert ctx["body"][0][0]["is_visible"]
  344. assert not ctx["body"][0][1]["is_visible"]
  345. assert ctx["body"][1][0]["is_visible"]
  346. assert not ctx["body"][1][1]["is_visible"]
  347. @pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
  348. @pytest.mark.parametrize("names", [True, False])
  349. def test_hide_columns_level(mi_styler, level, names):
  350. mi_styler.columns.names = ["zero", "one"]
  351. if names:
  352. mi_styler.index.names = ["zero", "one"]
  353. ctx = mi_styler.hide(axis="columns", level=level)._translate(True, False)
  354. assert len(ctx["head"]) == (2 if names else 1)
  355. @pytest.mark.parametrize("method", ["applymap", "apply"])
  356. @pytest.mark.parametrize("axis", ["index", "columns"])
  357. def test_apply_map_header(method, axis):
  358. # GH 41893
  359. df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"])
  360. func = {
  361. "apply": lambda s: ["attr: val" if ("A" in v or "C" in v) else "" for v in s],
  362. "applymap": lambda v: "attr: val" if ("A" in v or "C" in v) else "",
  363. }
  364. # test execution added to todo
  365. result = getattr(df.style, f"{method}_index")(func[method], axis=axis)
  366. assert len(result._todo) == 1
  367. assert len(getattr(result, f"ctx_{axis}")) == 0
  368. # test ctx object on compute
  369. result._compute()
  370. expected = {
  371. (0, 0): [("attr", "val")],
  372. }
  373. assert getattr(result, f"ctx_{axis}") == expected
  374. @pytest.mark.parametrize("method", ["apply", "applymap"])
  375. @pytest.mark.parametrize("axis", ["index", "columns"])
  376. def test_apply_map_header_mi(mi_styler, method, axis):
  377. # GH 41893
  378. func = {
  379. "apply": lambda s: ["attr: val;" if "b" in v else "" for v in s],
  380. "applymap": lambda v: "attr: val" if "b" in v else "",
  381. }
  382. result = getattr(mi_styler, f"{method}_index")(func[method], axis=axis)._compute()
  383. expected = {(1, 1): [("attr", "val")]}
  384. assert getattr(result, f"ctx_{axis}") == expected
  385. def test_apply_map_header_raises(mi_styler):
  386. # GH 41893
  387. with pytest.raises(ValueError, match="No axis named bad for object type DataFrame"):
  388. mi_styler.applymap_index(lambda v: "attr: val;", axis="bad")._compute()
  389. class TestStyler:
  390. def test_init_non_pandas(self):
  391. msg = "``data`` must be a Series or DataFrame"
  392. with pytest.raises(TypeError, match=msg):
  393. Styler([1, 2, 3])
  394. def test_init_series(self):
  395. result = Styler(Series([1, 2]))
  396. assert result.data.ndim == 2
  397. def test_repr_html_ok(self, styler):
  398. styler._repr_html_()
  399. def test_repr_html_mathjax(self, styler):
  400. # gh-19824 / 41395
  401. assert "tex2jax_ignore" not in styler._repr_html_()
  402. with option_context("styler.html.mathjax", False):
  403. assert "tex2jax_ignore" in styler._repr_html_()
  404. def test_update_ctx(self, styler):
  405. styler._update_ctx(DataFrame({"A": ["color: red", "color: blue"]}))
  406. expected = {(0, 0): [("color", "red")], (1, 0): [("color", "blue")]}
  407. assert styler.ctx == expected
  408. def test_update_ctx_flatten_multi_and_trailing_semi(self, styler):
  409. attrs = DataFrame({"A": ["color: red; foo: bar", "color:blue ; foo: baz;"]})
  410. styler._update_ctx(attrs)
  411. expected = {
  412. (0, 0): [("color", "red"), ("foo", "bar")],
  413. (1, 0): [("color", "blue"), ("foo", "baz")],
  414. }
  415. assert styler.ctx == expected
  416. def test_render(self):
  417. df = DataFrame({"A": [0, 1]})
  418. style = lambda x: Series(["color: red", "color: blue"], name=x.name)
  419. s = Styler(df, uuid="AB").apply(style)
  420. s.to_html()
  421. # it worked?
  422. def test_multiple_render(self, df):
  423. # GH 39396
  424. s = Styler(df, uuid_len=0).applymap(lambda x: "color: red;", subset=["A"])
  425. s.to_html() # do 2 renders to ensure css styles not duplicated
  426. assert (
  427. '<style type="text/css">\n#T__row0_col0, #T__row1_col0 {\n'
  428. " color: red;\n}\n</style>" in s.to_html()
  429. )
  430. def test_render_empty_dfs(self):
  431. empty_df = DataFrame()
  432. es = Styler(empty_df)
  433. es.to_html()
  434. # An index but no columns
  435. DataFrame(columns=["a"]).style.to_html()
  436. # A column but no index
  437. DataFrame(index=["a"]).style.to_html()
  438. # No IndexError raised?
  439. def test_render_double(self):
  440. df = DataFrame({"A": [0, 1]})
  441. style = lambda x: Series(
  442. ["color: red; border: 1px", "color: blue; border: 2px"], name=x.name
  443. )
  444. s = Styler(df, uuid="AB").apply(style)
  445. s.to_html()
  446. # it worked?
  447. def test_set_properties(self):
  448. df = DataFrame({"A": [0, 1]})
  449. result = df.style.set_properties(color="white", size="10px")._compute().ctx
  450. # order is deterministic
  451. v = [("color", "white"), ("size", "10px")]
  452. expected = {(0, 0): v, (1, 0): v}
  453. assert result.keys() == expected.keys()
  454. for v1, v2 in zip(result.values(), expected.values()):
  455. assert sorted(v1) == sorted(v2)
  456. def test_set_properties_subset(self):
  457. df = DataFrame({"A": [0, 1]})
  458. result = (
  459. df.style.set_properties(subset=IndexSlice[0, "A"], color="white")
  460. ._compute()
  461. .ctx
  462. )
  463. expected = {(0, 0): [("color", "white")]}
  464. assert result == expected
  465. def test_empty_index_name_doesnt_display(self, blank_value):
  466. # https://github.com/pandas-dev/pandas/pull/12090#issuecomment-180695902
  467. df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
  468. result = df.style._translate(True, True)
  469. assert len(result["head"]) == 1
  470. expected = {
  471. "class": "blank level0",
  472. "type": "th",
  473. "value": blank_value,
  474. "is_visible": True,
  475. "display_value": blank_value,
  476. }
  477. assert expected.items() <= result["head"][0][0].items()
  478. def test_index_name(self):
  479. # https://github.com/pandas-dev/pandas/issues/11655
  480. df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
  481. result = df.set_index("A").style._translate(True, True)
  482. expected = {
  483. "class": "index_name level0",
  484. "type": "th",
  485. "value": "A",
  486. "is_visible": True,
  487. "display_value": "A",
  488. }
  489. assert expected.items() <= result["head"][1][0].items()
  490. def test_numeric_columns(self):
  491. # https://github.com/pandas-dev/pandas/issues/12125
  492. # smoke test for _translate
  493. df = DataFrame({0: [1, 2, 3]})
  494. df.style._translate(True, True)
  495. def test_apply_axis(self):
  496. df = DataFrame({"A": [0, 0], "B": [1, 1]})
  497. f = lambda x: [f"val: {x.max()}" for v in x]
  498. result = df.style.apply(f, axis=1)
  499. assert len(result._todo) == 1
  500. assert len(result.ctx) == 0
  501. result._compute()
  502. expected = {
  503. (0, 0): [("val", "1")],
  504. (0, 1): [("val", "1")],
  505. (1, 0): [("val", "1")],
  506. (1, 1): [("val", "1")],
  507. }
  508. assert result.ctx == expected
  509. result = df.style.apply(f, axis=0)
  510. expected = {
  511. (0, 0): [("val", "0")],
  512. (0, 1): [("val", "1")],
  513. (1, 0): [("val", "0")],
  514. (1, 1): [("val", "1")],
  515. }
  516. result._compute()
  517. assert result.ctx == expected
  518. result = df.style.apply(f) # default
  519. result._compute()
  520. assert result.ctx == expected
  521. @pytest.mark.parametrize("axis", [0, 1])
  522. def test_apply_series_return(self, axis):
  523. # GH 42014
  524. df = DataFrame([[1, 2], [3, 4]], index=["X", "Y"], columns=["X", "Y"])
  525. # test Series return where len(Series) < df.index or df.columns but labels OK
  526. func = lambda s: Series(["color: red;"], index=["Y"])
  527. result = df.style.apply(func, axis=axis)._compute().ctx
  528. assert result[(1, 1)] == [("color", "red")]
  529. assert result[(1 - axis, axis)] == [("color", "red")]
  530. # test Series return where labels align but different order
  531. func = lambda s: Series(["color: red;", "color: blue;"], index=["Y", "X"])
  532. result = df.style.apply(func, axis=axis)._compute().ctx
  533. assert result[(0, 0)] == [("color", "blue")]
  534. assert result[(1, 1)] == [("color", "red")]
  535. assert result[(1 - axis, axis)] == [("color", "red")]
  536. assert result[(axis, 1 - axis)] == [("color", "blue")]
  537. @pytest.mark.parametrize("index", [False, True])
  538. @pytest.mark.parametrize("columns", [False, True])
  539. def test_apply_dataframe_return(self, index, columns):
  540. # GH 42014
  541. df = DataFrame([[1, 2], [3, 4]], index=["X", "Y"], columns=["X", "Y"])
  542. idxs = ["X", "Y"] if index else ["Y"]
  543. cols = ["X", "Y"] if columns else ["Y"]
  544. df_styles = DataFrame("color: red;", index=idxs, columns=cols)
  545. result = df.style.apply(lambda x: df_styles, axis=None)._compute().ctx
  546. assert result[(1, 1)] == [("color", "red")] # (Y,Y) styles always present
  547. assert (result[(0, 1)] == [("color", "red")]) is index # (X,Y) only if index
  548. assert (result[(1, 0)] == [("color", "red")]) is columns # (Y,X) only if cols
  549. assert (result[(0, 0)] == [("color", "red")]) is (index and columns) # (X,X)
  550. @pytest.mark.parametrize(
  551. "slice_",
  552. [
  553. IndexSlice[:],
  554. IndexSlice[:, ["A"]],
  555. IndexSlice[[1], :],
  556. IndexSlice[[1], ["A"]],
  557. IndexSlice[:2, ["A", "B"]],
  558. ],
  559. )
  560. @pytest.mark.parametrize("axis", [0, 1])
  561. def test_apply_subset(self, slice_, axis, df):
  562. def h(x, color="bar"):
  563. return Series(f"color: {color}", index=x.index, name=x.name)
  564. result = df.style.apply(h, axis=axis, subset=slice_, color="baz")._compute().ctx
  565. expected = {
  566. (r, c): [("color", "baz")]
  567. for r, row in enumerate(df.index)
  568. for c, col in enumerate(df.columns)
  569. if row in df.loc[slice_].index and col in df.loc[slice_].columns
  570. }
  571. assert result == expected
  572. @pytest.mark.parametrize(
  573. "slice_",
  574. [
  575. IndexSlice[:],
  576. IndexSlice[:, ["A"]],
  577. IndexSlice[[1], :],
  578. IndexSlice[[1], ["A"]],
  579. IndexSlice[:2, ["A", "B"]],
  580. ],
  581. )
  582. def test_applymap_subset(self, slice_, df):
  583. result = df.style.applymap(lambda x: "color:baz;", subset=slice_)._compute().ctx
  584. expected = {
  585. (r, c): [("color", "baz")]
  586. for r, row in enumerate(df.index)
  587. for c, col in enumerate(df.columns)
  588. if row in df.loc[slice_].index and col in df.loc[slice_].columns
  589. }
  590. assert result == expected
  591. @pytest.mark.parametrize(
  592. "slice_",
  593. [
  594. IndexSlice[:, IndexSlice["x", "A"]],
  595. IndexSlice[:, IndexSlice[:, "A"]],
  596. IndexSlice[:, IndexSlice[:, ["A", "C"]]], # missing col element
  597. IndexSlice[IndexSlice["a", 1], :],
  598. IndexSlice[IndexSlice[:, 1], :],
  599. IndexSlice[IndexSlice[:, [1, 3]], :], # missing row element
  600. IndexSlice[:, ("x", "A")],
  601. IndexSlice[("a", 1), :],
  602. ],
  603. )
  604. def test_applymap_subset_multiindex(self, slice_):
  605. # GH 19861
  606. # edited for GH 33562
  607. if (
  608. isinstance(slice_[-1], tuple)
  609. and isinstance(slice_[-1][-1], list)
  610. and "C" in slice_[-1][-1]
  611. ):
  612. ctx = pytest.raises(KeyError, match="C")
  613. elif (
  614. isinstance(slice_[0], tuple)
  615. and isinstance(slice_[0][1], list)
  616. and 3 in slice_[0][1]
  617. ):
  618. ctx = pytest.raises(KeyError, match="3")
  619. else:
  620. ctx = contextlib.nullcontext()
  621. idx = MultiIndex.from_product([["a", "b"], [1, 2]])
  622. col = MultiIndex.from_product([["x", "y"], ["A", "B"]])
  623. df = DataFrame(np.random.rand(4, 4), columns=col, index=idx)
  624. with ctx:
  625. df.style.applymap(lambda x: "color: red;", subset=slice_).to_html()
  626. def test_applymap_subset_multiindex_code(self):
  627. # https://github.com/pandas-dev/pandas/issues/25858
  628. # Checks styler.applymap works with multindex when codes are provided
  629. codes = np.array([[0, 0, 1, 1], [0, 1, 0, 1]])
  630. columns = MultiIndex(
  631. levels=[["a", "b"], ["%", "#"]], codes=codes, names=["", ""]
  632. )
  633. df = DataFrame(
  634. [[1, -1, 1, 1], [-1, 1, 1, 1]], index=["hello", "world"], columns=columns
  635. )
  636. pct_subset = IndexSlice[:, IndexSlice[:, "%":"%"]]
  637. def color_negative_red(val):
  638. color = "red" if val < 0 else "black"
  639. return f"color: {color}"
  640. df.loc[pct_subset]
  641. df.style.applymap(color_negative_red, subset=pct_subset)
  642. @pytest.mark.parametrize(
  643. "stylefunc", ["background_gradient", "bar", "text_gradient"]
  644. )
  645. def test_subset_for_boolean_cols(self, stylefunc):
  646. # GH47838
  647. df = DataFrame(
  648. [
  649. [1, 2],
  650. [3, 4],
  651. ],
  652. columns=[False, True],
  653. )
  654. styled = getattr(df.style, stylefunc)()
  655. styled._compute()
  656. assert set(styled.ctx) == {(0, 0), (0, 1), (1, 0), (1, 1)}
  657. def test_empty(self):
  658. df = DataFrame({"A": [1, 0]})
  659. s = df.style
  660. s.ctx = {(0, 0): [("color", "red")], (1, 0): [("", "")]}
  661. result = s._translate(True, True)["cellstyle"]
  662. expected = [
  663. {"props": [("color", "red")], "selectors": ["row0_col0"]},
  664. {"props": [("", "")], "selectors": ["row1_col0"]},
  665. ]
  666. assert result == expected
  667. def test_duplicate(self):
  668. df = DataFrame({"A": [1, 0]})
  669. s = df.style
  670. s.ctx = {(0, 0): [("color", "red")], (1, 0): [("color", "red")]}
  671. result = s._translate(True, True)["cellstyle"]
  672. expected = [
  673. {"props": [("color", "red")], "selectors": ["row0_col0", "row1_col0"]}
  674. ]
  675. assert result == expected
  676. def test_init_with_na_rep(self):
  677. # GH 21527 28358
  678. df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"])
  679. ctx = Styler(df, na_rep="NA")._translate(True, True)
  680. assert ctx["body"][0][1]["display_value"] == "NA"
  681. assert ctx["body"][0][2]["display_value"] == "NA"
  682. def test_caption(self, df):
  683. styler = Styler(df, caption="foo")
  684. result = styler.to_html()
  685. assert all(["caption" in result, "foo" in result])
  686. styler = df.style
  687. result = styler.set_caption("baz")
  688. assert styler is result
  689. assert styler.caption == "baz"
  690. def test_uuid(self, df):
  691. styler = Styler(df, uuid="abc123")
  692. result = styler.to_html()
  693. assert "abc123" in result
  694. styler = df.style
  695. result = styler.set_uuid("aaa")
  696. assert result is styler
  697. assert result.uuid == "aaa"
  698. def test_unique_id(self):
  699. # See https://github.com/pandas-dev/pandas/issues/16780
  700. df = DataFrame({"a": [1, 3, 5, 6], "b": [2, 4, 12, 21]})
  701. result = df.style.to_html(uuid="test")
  702. assert "test" in result
  703. ids = re.findall('id="(.*?)"', result)
  704. assert np.unique(ids).size == len(ids)
  705. def test_table_styles(self, df):
  706. style = [{"selector": "th", "props": [("foo", "bar")]}] # default format
  707. styler = Styler(df, table_styles=style)
  708. result = " ".join(styler.to_html().split())
  709. assert "th { foo: bar; }" in result
  710. styler = df.style
  711. result = styler.set_table_styles(style)
  712. assert styler is result
  713. assert styler.table_styles == style
  714. # GH 39563
  715. style = [{"selector": "th", "props": "foo:bar;"}] # css string format
  716. styler = df.style.set_table_styles(style)
  717. result = " ".join(styler.to_html().split())
  718. assert "th { foo: bar; }" in result
  719. def test_table_styles_multiple(self, df):
  720. ctx = df.style.set_table_styles(
  721. [
  722. {"selector": "th,td", "props": "color:red;"},
  723. {"selector": "tr", "props": "color:green;"},
  724. ]
  725. )._translate(True, True)["table_styles"]
  726. assert ctx == [
  727. {"selector": "th", "props": [("color", "red")]},
  728. {"selector": "td", "props": [("color", "red")]},
  729. {"selector": "tr", "props": [("color", "green")]},
  730. ]
  731. def test_table_styles_dict_multiple_selectors(self, df):
  732. # GH 44011
  733. result = df.style.set_table_styles(
  734. {
  735. "B": [
  736. {"selector": "th,td", "props": [("border-left", "2px solid black")]}
  737. ]
  738. }
  739. )._translate(True, True)["table_styles"]
  740. expected = [
  741. {"selector": "th.col1", "props": [("border-left", "2px solid black")]},
  742. {"selector": "td.col1", "props": [("border-left", "2px solid black")]},
  743. ]
  744. assert result == expected
  745. def test_maybe_convert_css_to_tuples(self):
  746. expected = [("a", "b"), ("c", "d e")]
  747. assert maybe_convert_css_to_tuples("a:b;c:d e;") == expected
  748. assert maybe_convert_css_to_tuples("a: b ;c: d e ") == expected
  749. expected = []
  750. assert maybe_convert_css_to_tuples("") == expected
  751. def test_maybe_convert_css_to_tuples_err(self):
  752. msg = "Styles supplied as string must follow CSS rule formats"
  753. with pytest.raises(ValueError, match=msg):
  754. maybe_convert_css_to_tuples("err")
  755. def test_table_attributes(self, df):
  756. attributes = 'class="foo" data-bar'
  757. styler = Styler(df, table_attributes=attributes)
  758. result = styler.to_html()
  759. assert 'class="foo" data-bar' in result
  760. result = df.style.set_table_attributes(attributes).to_html()
  761. assert 'class="foo" data-bar' in result
  762. def test_apply_none(self):
  763. def f(x):
  764. return DataFrame(
  765. np.where(x == x.max(), "color: red", ""),
  766. index=x.index,
  767. columns=x.columns,
  768. )
  769. result = DataFrame([[1, 2], [3, 4]]).style.apply(f, axis=None)._compute().ctx
  770. assert result[(1, 1)] == [("color", "red")]
  771. def test_trim(self, df):
  772. result = df.style.to_html() # trim=True
  773. assert result.count("#") == 0
  774. result = df.style.highlight_max().to_html()
  775. assert result.count("#") == len(df.columns)
  776. def test_export(self, df, styler):
  777. f = lambda x: "color: red" if x > 0 else "color: blue"
  778. g = lambda x, z: f"color: {z}" if x > 0 else f"color: {z}"
  779. style1 = styler
  780. style1.applymap(f).applymap(g, z="b").highlight_max()._compute() # = render
  781. result = style1.export()
  782. style2 = df.style
  783. style2.use(result)
  784. assert style1._todo == style2._todo
  785. style2.to_html()
  786. def test_bad_apply_shape(self):
  787. df = DataFrame([[1, 2], [3, 4]], index=["A", "B"], columns=["X", "Y"])
  788. msg = "resulted in the apply method collapsing to a Series."
  789. with pytest.raises(ValueError, match=msg):
  790. df.style._apply(lambda x: "x")
  791. msg = "created invalid {} labels"
  792. with pytest.raises(ValueError, match=msg.format("index")):
  793. df.style._apply(lambda x: [""])
  794. with pytest.raises(ValueError, match=msg.format("index")):
  795. df.style._apply(lambda x: ["", "", "", ""])
  796. with pytest.raises(ValueError, match=msg.format("index")):
  797. df.style._apply(lambda x: Series(["a:v;", ""], index=["A", "C"]), axis=0)
  798. with pytest.raises(ValueError, match=msg.format("columns")):
  799. df.style._apply(lambda x: ["", "", ""], axis=1)
  800. with pytest.raises(ValueError, match=msg.format("columns")):
  801. df.style._apply(lambda x: Series(["a:v;", ""], index=["X", "Z"]), axis=1)
  802. msg = "returned ndarray with wrong shape"
  803. with pytest.raises(ValueError, match=msg):
  804. df.style._apply(lambda x: np.array([[""], [""]]), axis=None)
  805. def test_apply_bad_return(self):
  806. def f(x):
  807. return ""
  808. df = DataFrame([[1, 2], [3, 4]])
  809. msg = (
  810. "must return a DataFrame or ndarray when passed to `Styler.apply` "
  811. "with axis=None"
  812. )
  813. with pytest.raises(TypeError, match=msg):
  814. df.style._apply(f, axis=None)
  815. @pytest.mark.parametrize("axis", ["index", "columns"])
  816. def test_apply_bad_labels(self, axis):
  817. def f(x):
  818. return DataFrame(**{axis: ["bad", "labels"]})
  819. df = DataFrame([[1, 2], [3, 4]])
  820. msg = f"created invalid {axis} labels."
  821. with pytest.raises(ValueError, match=msg):
  822. df.style._apply(f, axis=None)
  823. def test_get_level_lengths(self):
  824. index = MultiIndex.from_product([["a", "b"], [0, 1, 2]])
  825. expected = {
  826. (0, 0): 3,
  827. (0, 3): 3,
  828. (1, 0): 1,
  829. (1, 1): 1,
  830. (1, 2): 1,
  831. (1, 3): 1,
  832. (1, 4): 1,
  833. (1, 5): 1,
  834. }
  835. result = _get_level_lengths(index, sparsify=True, max_index=100)
  836. tm.assert_dict_equal(result, expected)
  837. expected = {
  838. (0, 0): 1,
  839. (0, 1): 1,
  840. (0, 2): 1,
  841. (0, 3): 1,
  842. (0, 4): 1,
  843. (0, 5): 1,
  844. (1, 0): 1,
  845. (1, 1): 1,
  846. (1, 2): 1,
  847. (1, 3): 1,
  848. (1, 4): 1,
  849. (1, 5): 1,
  850. }
  851. result = _get_level_lengths(index, sparsify=False, max_index=100)
  852. tm.assert_dict_equal(result, expected)
  853. def test_get_level_lengths_un_sorted(self):
  854. index = MultiIndex.from_arrays([[1, 1, 2, 1], ["a", "b", "b", "d"]])
  855. expected = {
  856. (0, 0): 2,
  857. (0, 2): 1,
  858. (0, 3): 1,
  859. (1, 0): 1,
  860. (1, 1): 1,
  861. (1, 2): 1,
  862. (1, 3): 1,
  863. }
  864. result = _get_level_lengths(index, sparsify=True, max_index=100)
  865. tm.assert_dict_equal(result, expected)
  866. expected = {
  867. (0, 0): 1,
  868. (0, 1): 1,
  869. (0, 2): 1,
  870. (0, 3): 1,
  871. (1, 0): 1,
  872. (1, 1): 1,
  873. (1, 2): 1,
  874. (1, 3): 1,
  875. }
  876. result = _get_level_lengths(index, sparsify=False, max_index=100)
  877. tm.assert_dict_equal(result, expected)
  878. def test_mi_sparse_index_names(self, blank_value):
  879. # Test the class names and displayed value are correct on rendering MI names
  880. df = DataFrame(
  881. {"A": [1, 2]},
  882. index=MultiIndex.from_arrays(
  883. [["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
  884. ),
  885. )
  886. result = df.style._translate(True, True)
  887. head = result["head"][1]
  888. expected = [
  889. {
  890. "class": "index_name level0",
  891. "display_value": "idx_level_0",
  892. "is_visible": True,
  893. },
  894. {
  895. "class": "index_name level1",
  896. "display_value": "idx_level_1",
  897. "is_visible": True,
  898. },
  899. {
  900. "class": "blank col0",
  901. "display_value": blank_value,
  902. "is_visible": True,
  903. },
  904. ]
  905. for i, expected_dict in enumerate(expected):
  906. assert expected_dict.items() <= head[i].items()
  907. def test_mi_sparse_column_names(self, blank_value):
  908. df = DataFrame(
  909. np.arange(16).reshape(4, 4),
  910. index=MultiIndex.from_arrays(
  911. [["a", "a", "b", "a"], [0, 1, 1, 2]],
  912. names=["idx_level_0", "idx_level_1"],
  913. ),
  914. columns=MultiIndex.from_arrays(
  915. [["C1", "C1", "C2", "C2"], [1, 0, 1, 0]], names=["colnam_0", "colnam_1"]
  916. ),
  917. )
  918. result = Styler(df, cell_ids=False)._translate(True, True)
  919. for level in [0, 1]:
  920. head = result["head"][level]
  921. expected = [
  922. {
  923. "class": "blank",
  924. "display_value": blank_value,
  925. "is_visible": True,
  926. },
  927. {
  928. "class": f"index_name level{level}",
  929. "display_value": f"colnam_{level}",
  930. "is_visible": True,
  931. },
  932. ]
  933. for i, expected_dict in enumerate(expected):
  934. assert expected_dict.items() <= head[i].items()
  935. def test_hide_column_headers(self, df, styler):
  936. ctx = styler.hide(axis="columns")._translate(True, True)
  937. assert len(ctx["head"]) == 0 # no header entries with an unnamed index
  938. df.index.name = "some_name"
  939. ctx = df.style.hide(axis="columns")._translate(True, True)
  940. assert len(ctx["head"]) == 1
  941. # index names still visible, changed in #42101, reverted in 43404
  942. def test_hide_single_index(self, df):
  943. # GH 14194
  944. # single unnamed index
  945. ctx = df.style._translate(True, True)
  946. assert ctx["body"][0][0]["is_visible"]
  947. assert ctx["head"][0][0]["is_visible"]
  948. ctx2 = df.style.hide(axis="index")._translate(True, True)
  949. assert not ctx2["body"][0][0]["is_visible"]
  950. assert not ctx2["head"][0][0]["is_visible"]
  951. # single named index
  952. ctx3 = df.set_index("A").style._translate(True, True)
  953. assert ctx3["body"][0][0]["is_visible"]
  954. assert len(ctx3["head"]) == 2 # 2 header levels
  955. assert ctx3["head"][0][0]["is_visible"]
  956. ctx4 = df.set_index("A").style.hide(axis="index")._translate(True, True)
  957. assert not ctx4["body"][0][0]["is_visible"]
  958. assert len(ctx4["head"]) == 1 # only 1 header levels
  959. assert not ctx4["head"][0][0]["is_visible"]
  960. def test_hide_multiindex(self):
  961. # GH 14194
  962. df = DataFrame(
  963. {"A": [1, 2], "B": [1, 2]},
  964. index=MultiIndex.from_arrays(
  965. [["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
  966. ),
  967. )
  968. ctx1 = df.style._translate(True, True)
  969. # tests for 'a' and '0'
  970. assert ctx1["body"][0][0]["is_visible"]
  971. assert ctx1["body"][0][1]["is_visible"]
  972. # check for blank header rows
  973. assert len(ctx1["head"][0]) == 4 # two visible indexes and two data columns
  974. ctx2 = df.style.hide(axis="index")._translate(True, True)
  975. # tests for 'a' and '0'
  976. assert not ctx2["body"][0][0]["is_visible"]
  977. assert not ctx2["body"][0][1]["is_visible"]
  978. # check for blank header rows
  979. assert len(ctx2["head"][0]) == 3 # one hidden (col name) and two data columns
  980. assert not ctx2["head"][0][0]["is_visible"]
  981. def test_hide_columns_single_level(self, df):
  982. # GH 14194
  983. # test hiding single column
  984. ctx = df.style._translate(True, True)
  985. assert ctx["head"][0][1]["is_visible"]
  986. assert ctx["head"][0][1]["display_value"] == "A"
  987. assert ctx["head"][0][2]["is_visible"]
  988. assert ctx["head"][0][2]["display_value"] == "B"
  989. assert ctx["body"][0][1]["is_visible"] # col A, row 1
  990. assert ctx["body"][1][2]["is_visible"] # col B, row 1
  991. ctx = df.style.hide("A", axis="columns")._translate(True, True)
  992. assert not ctx["head"][0][1]["is_visible"]
  993. assert not ctx["body"][0][1]["is_visible"] # col A, row 1
  994. assert ctx["body"][1][2]["is_visible"] # col B, row 1
  995. # test hiding mulitiple columns
  996. ctx = df.style.hide(["A", "B"], axis="columns")._translate(True, True)
  997. assert not ctx["head"][0][1]["is_visible"]
  998. assert not ctx["head"][0][2]["is_visible"]
  999. assert not ctx["body"][0][1]["is_visible"] # col A, row 1
  1000. assert not ctx["body"][1][2]["is_visible"] # col B, row 1
  1001. def test_hide_columns_index_mult_levels(self):
  1002. # GH 14194
  1003. # setup dataframe with multiple column levels and indices
  1004. i1 = MultiIndex.from_arrays(
  1005. [["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
  1006. )
  1007. i2 = MultiIndex.from_arrays(
  1008. [["b", "b"], [0, 1]], names=["col_level_0", "col_level_1"]
  1009. )
  1010. df = DataFrame([[1, 2], [3, 4]], index=i1, columns=i2)
  1011. ctx = df.style._translate(True, True)
  1012. # column headers
  1013. assert ctx["head"][0][2]["is_visible"]
  1014. assert ctx["head"][1][2]["is_visible"]
  1015. assert ctx["head"][1][3]["display_value"] == "1"
  1016. # indices
  1017. assert ctx["body"][0][0]["is_visible"]
  1018. # data
  1019. assert ctx["body"][1][2]["is_visible"]
  1020. assert ctx["body"][1][2]["display_value"] == "3"
  1021. assert ctx["body"][1][3]["is_visible"]
  1022. assert ctx["body"][1][3]["display_value"] == "4"
  1023. # hide top column level, which hides both columns
  1024. ctx = df.style.hide("b", axis="columns")._translate(True, True)
  1025. assert not ctx["head"][0][2]["is_visible"] # b
  1026. assert not ctx["head"][1][2]["is_visible"] # 0
  1027. assert not ctx["body"][1][2]["is_visible"] # 3
  1028. assert ctx["body"][0][0]["is_visible"] # index
  1029. # hide first column only
  1030. ctx = df.style.hide([("b", 0)], axis="columns")._translate(True, True)
  1031. assert not ctx["head"][0][2]["is_visible"] # b
  1032. assert ctx["head"][0][3]["is_visible"] # b
  1033. assert not ctx["head"][1][2]["is_visible"] # 0
  1034. assert not ctx["body"][1][2]["is_visible"] # 3
  1035. assert ctx["body"][1][3]["is_visible"]
  1036. assert ctx["body"][1][3]["display_value"] == "4"
  1037. # hide second column and index
  1038. ctx = df.style.hide([("b", 1)], axis=1).hide(axis=0)._translate(True, True)
  1039. assert not ctx["body"][0][0]["is_visible"] # index
  1040. assert len(ctx["head"][0]) == 3
  1041. assert ctx["head"][0][1]["is_visible"] # b
  1042. assert ctx["head"][1][1]["is_visible"] # 0
  1043. assert not ctx["head"][1][2]["is_visible"] # 1
  1044. assert not ctx["body"][1][3]["is_visible"] # 4
  1045. assert ctx["body"][1][2]["is_visible"]
  1046. assert ctx["body"][1][2]["display_value"] == "3"
  1047. # hide top row level, which hides both rows so body empty
  1048. ctx = df.style.hide("a", axis="index")._translate(True, True)
  1049. assert ctx["body"] == []
  1050. # hide first row only
  1051. ctx = df.style.hide(("a", 0), axis="index")._translate(True, True)
  1052. for i in [0, 1, 2, 3]:
  1053. assert "row1" in ctx["body"][0][i]["class"] # row0 not included in body
  1054. assert ctx["body"][0][i]["is_visible"]
  1055. def test_pipe(self, df):
  1056. def set_caption_from_template(styler, a, b):
  1057. return styler.set_caption(f"Dataframe with a = {a} and b = {b}")
  1058. styler = df.style.pipe(set_caption_from_template, "A", b="B")
  1059. assert "Dataframe with a = A and b = B" in styler.to_html()
  1060. # Test with an argument that is a (callable, keyword_name) pair.
  1061. def f(a, b, styler):
  1062. return (a, b, styler)
  1063. styler = df.style
  1064. result = styler.pipe((f, "styler"), a=1, b=2)
  1065. assert result == (1, 2, styler)
  1066. def test_no_cell_ids(self):
  1067. # GH 35588
  1068. # GH 35663
  1069. df = DataFrame(data=[[0]])
  1070. styler = Styler(df, uuid="_", cell_ids=False)
  1071. styler.to_html()
  1072. s = styler.to_html() # render twice to ensure ctx is not updated
  1073. assert s.find('<td class="data row0 col0" >') != -1
  1074. @pytest.mark.parametrize(
  1075. "classes",
  1076. [
  1077. DataFrame(
  1078. data=[["", "test-class"], [np.nan, None]],
  1079. columns=["A", "B"],
  1080. index=["a", "b"],
  1081. ),
  1082. DataFrame(data=[["test-class"]], columns=["B"], index=["a"]),
  1083. DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]),
  1084. ],
  1085. )
  1086. def test_set_data_classes(self, classes):
  1087. # GH 36159
  1088. df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"])
  1089. s = Styler(df, uuid_len=0, cell_ids=False).set_td_classes(classes).to_html()
  1090. assert '<td class="data row0 col0" >0</td>' in s
  1091. assert '<td class="data row0 col1 test-class" >1</td>' in s
  1092. assert '<td class="data row1 col0" >2</td>' in s
  1093. assert '<td class="data row1 col1" >3</td>' in s
  1094. # GH 39317
  1095. s = Styler(df, uuid_len=0, cell_ids=True).set_td_classes(classes).to_html()
  1096. assert '<td id="T__row0_col0" class="data row0 col0" >0</td>' in s
  1097. assert '<td id="T__row0_col1" class="data row0 col1 test-class" >1</td>' in s
  1098. assert '<td id="T__row1_col0" class="data row1 col0" >2</td>' in s
  1099. assert '<td id="T__row1_col1" class="data row1 col1" >3</td>' in s
  1100. def test_set_data_classes_reindex(self):
  1101. # GH 39317
  1102. df = DataFrame(
  1103. data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]], columns=[0, 1, 2], index=[0, 1, 2]
  1104. )
  1105. classes = DataFrame(
  1106. data=[["mi", "ma"], ["mu", "mo"]],
  1107. columns=[0, 2],
  1108. index=[0, 2],
  1109. )
  1110. s = Styler(df, uuid_len=0).set_td_classes(classes).to_html()
  1111. assert '<td id="T__row0_col0" class="data row0 col0 mi" >0</td>' in s
  1112. assert '<td id="T__row0_col2" class="data row0 col2 ma" >2</td>' in s
  1113. assert '<td id="T__row1_col1" class="data row1 col1" >4</td>' in s
  1114. assert '<td id="T__row2_col0" class="data row2 col0 mu" >6</td>' in s
  1115. assert '<td id="T__row2_col2" class="data row2 col2 mo" >8</td>' in s
  1116. def test_chaining_table_styles(self):
  1117. # GH 35607
  1118. df = DataFrame(data=[[0, 1], [1, 2]], columns=["A", "B"])
  1119. styler = df.style.set_table_styles(
  1120. [{"selector": "", "props": [("background-color", "yellow")]}]
  1121. ).set_table_styles(
  1122. [{"selector": ".col0", "props": [("background-color", "blue")]}],
  1123. overwrite=False,
  1124. )
  1125. assert len(styler.table_styles) == 2
  1126. def test_column_and_row_styling(self):
  1127. # GH 35607
  1128. df = DataFrame(data=[[0, 1], [1, 2]], columns=["A", "B"])
  1129. s = Styler(df, uuid_len=0)
  1130. s = s.set_table_styles({"A": [{"selector": "", "props": [("color", "blue")]}]})
  1131. assert "#T_ .col0 {\n color: blue;\n}" in s.to_html()
  1132. s = s.set_table_styles(
  1133. {0: [{"selector": "", "props": [("color", "blue")]}]}, axis=1
  1134. )
  1135. assert "#T_ .row0 {\n color: blue;\n}" in s.to_html()
  1136. @pytest.mark.parametrize("len_", [1, 5, 32, 33, 100])
  1137. def test_uuid_len(self, len_):
  1138. # GH 36345
  1139. df = DataFrame(data=[["A"]])
  1140. s = Styler(df, uuid_len=len_, cell_ids=False).to_html()
  1141. strt = s.find('id="T_')
  1142. end = s[strt + 6 :].find('"')
  1143. if len_ > 32:
  1144. assert end == 32
  1145. else:
  1146. assert end == len_
  1147. @pytest.mark.parametrize("len_", [-2, "bad", None])
  1148. def test_uuid_len_raises(self, len_):
  1149. # GH 36345
  1150. df = DataFrame(data=[["A"]])
  1151. msg = "``uuid_len`` must be an integer in range \\[0, 32\\]."
  1152. with pytest.raises(TypeError, match=msg):
  1153. Styler(df, uuid_len=len_, cell_ids=False).to_html()
  1154. @pytest.mark.parametrize(
  1155. "slc",
  1156. [
  1157. IndexSlice[:, :],
  1158. IndexSlice[:, 1],
  1159. IndexSlice[1, :],
  1160. IndexSlice[[1], [1]],
  1161. IndexSlice[1, [1]],
  1162. IndexSlice[[1], 1],
  1163. IndexSlice[1],
  1164. IndexSlice[1, 1],
  1165. slice(None, None, None),
  1166. [0, 1],
  1167. np.array([0, 1]),
  1168. Series([0, 1]),
  1169. ],
  1170. )
  1171. def test_non_reducing_slice(self, slc):
  1172. df = DataFrame([[0, 1], [2, 3]])
  1173. tslice_ = non_reducing_slice(slc)
  1174. assert isinstance(df.loc[tslice_], DataFrame)
  1175. @pytest.mark.parametrize("box", [list, Series, np.array])
  1176. def test_list_slice(self, box):
  1177. # like dataframe getitem
  1178. subset = box(["A"])
  1179. df = DataFrame({"A": [1, 2], "B": [3, 4]}, index=["A", "B"])
  1180. expected = IndexSlice[:, ["A"]]
  1181. result = non_reducing_slice(subset)
  1182. tm.assert_frame_equal(df.loc[result], df.loc[expected])
  1183. def test_non_reducing_slice_on_multiindex(self):
  1184. # GH 19861
  1185. dic = {
  1186. ("a", "d"): [1, 4],
  1187. ("a", "c"): [2, 3],
  1188. ("b", "c"): [3, 2],
  1189. ("b", "d"): [4, 1],
  1190. }
  1191. df = DataFrame(dic, index=[0, 1])
  1192. idx = IndexSlice
  1193. slice_ = idx[:, idx["b", "d"]]
  1194. tslice_ = non_reducing_slice(slice_)
  1195. result = df.loc[tslice_]
  1196. expected = DataFrame({("b", "d"): [4, 1]})
  1197. tm.assert_frame_equal(result, expected)
  1198. @pytest.mark.parametrize(
  1199. "slice_",
  1200. [
  1201. IndexSlice[:, :],
  1202. # check cols
  1203. IndexSlice[:, IndexSlice[["a"]]], # inferred deeper need list
  1204. IndexSlice[:, IndexSlice[["a"], ["c"]]], # inferred deeper need list
  1205. IndexSlice[:, IndexSlice["a", "c", :]],
  1206. IndexSlice[:, IndexSlice["a", :, "e"]],
  1207. IndexSlice[:, IndexSlice[:, "c", "e"]],
  1208. IndexSlice[:, IndexSlice["a", ["c", "d"], :]], # check list
  1209. IndexSlice[:, IndexSlice["a", ["c", "d", "-"], :]], # don't allow missing
  1210. IndexSlice[:, IndexSlice["a", ["c", "d", "-"], "e"]], # no slice
  1211. # check rows
  1212. IndexSlice[IndexSlice[["U"]], :], # inferred deeper need list
  1213. IndexSlice[IndexSlice[["U"], ["W"]], :], # inferred deeper need list
  1214. IndexSlice[IndexSlice["U", "W", :], :],
  1215. IndexSlice[IndexSlice["U", :, "Y"], :],
  1216. IndexSlice[IndexSlice[:, "W", "Y"], :],
  1217. IndexSlice[IndexSlice[:, "W", ["Y", "Z"]], :], # check list
  1218. IndexSlice[IndexSlice[:, "W", ["Y", "Z", "-"]], :], # don't allow missing
  1219. IndexSlice[IndexSlice["U", "W", ["Y", "Z", "-"]], :], # no slice
  1220. # check simultaneous
  1221. IndexSlice[IndexSlice[:, "W", "Y"], IndexSlice["a", "c", :]],
  1222. ],
  1223. )
  1224. def test_non_reducing_multi_slice_on_multiindex(self, slice_):
  1225. # GH 33562
  1226. cols = MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]])
  1227. idxs = MultiIndex.from_product([["U", "V"], ["W", "X"], ["Y", "Z"]])
  1228. df = DataFrame(np.arange(64).reshape(8, 8), columns=cols, index=idxs)
  1229. for lvl in [0, 1]:
  1230. key = slice_[lvl]
  1231. if isinstance(key, tuple):
  1232. for subkey in key:
  1233. if isinstance(subkey, list) and "-" in subkey:
  1234. # not present in the index level, raises KeyError since 2.0
  1235. with pytest.raises(KeyError, match="-"):
  1236. df.loc[slice_]
  1237. return
  1238. expected = df.loc[slice_]
  1239. result = df.loc[non_reducing_slice(slice_)]
  1240. tm.assert_frame_equal(result, expected)
  1241. def test_hidden_index_names(mi_df):
  1242. mi_df.index.names = ["Lev0", "Lev1"]
  1243. mi_styler = mi_df.style
  1244. ctx = mi_styler._translate(True, True)
  1245. assert len(ctx["head"]) == 3 # 2 column index levels + 1 index names row
  1246. mi_styler.hide(axis="index", names=True)
  1247. ctx = mi_styler._translate(True, True)
  1248. assert len(ctx["head"]) == 2 # index names row is unparsed
  1249. for i in range(4):
  1250. assert ctx["body"][0][i]["is_visible"] # 2 index levels + 2 data values visible
  1251. mi_styler.hide(axis="index", level=1)
  1252. ctx = mi_styler._translate(True, True)
  1253. assert len(ctx["head"]) == 2 # index names row is still hidden
  1254. assert ctx["body"][0][0]["is_visible"] is True
  1255. assert ctx["body"][0][1]["is_visible"] is False
  1256. def test_hidden_column_names(mi_df):
  1257. mi_df.columns.names = ["Lev0", "Lev1"]
  1258. mi_styler = mi_df.style
  1259. ctx = mi_styler._translate(True, True)
  1260. assert ctx["head"][0][1]["display_value"] == "Lev0"
  1261. assert ctx["head"][1][1]["display_value"] == "Lev1"
  1262. mi_styler.hide(names=True, axis="columns")
  1263. ctx = mi_styler._translate(True, True)
  1264. assert ctx["head"][0][1]["display_value"] == "&nbsp;"
  1265. assert ctx["head"][1][1]["display_value"] == "&nbsp;"
  1266. mi_styler.hide(level=0, axis="columns")
  1267. ctx = mi_styler._translate(True, True)
  1268. assert len(ctx["head"]) == 1 # no index names and only one visible column headers
  1269. assert ctx["head"][0][1]["display_value"] == "&nbsp;"
  1270. @pytest.mark.parametrize("caption", [1, ("a", "b", "c"), (1, "s")])
  1271. def test_caption_raises(mi_styler, caption):
  1272. msg = "`caption` must be either a string or 2-tuple of strings."
  1273. with pytest.raises(ValueError, match=msg):
  1274. mi_styler.set_caption(caption)
  1275. def test_hiding_headers_over_index_no_sparsify():
  1276. # GH 43464
  1277. midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
  1278. df = DataFrame(9, index=midx, columns=[0])
  1279. ctx = df.style._translate(False, False)
  1280. assert len(ctx["body"]) == 6
  1281. ctx = df.style.hide((1, "a"), axis=0)._translate(False, False)
  1282. assert len(ctx["body"]) == 4
  1283. assert "row2" in ctx["body"][0][0]["class"]
  1284. def test_hiding_headers_over_columns_no_sparsify():
  1285. # GH 43464
  1286. midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
  1287. df = DataFrame(9, columns=midx, index=[0])
  1288. ctx = df.style._translate(False, False)
  1289. for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
  1290. assert ctx["head"][ix[0]][ix[1]]["is_visible"] is True
  1291. ctx = df.style.hide((1, "a"), axis="columns")._translate(False, False)
  1292. for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
  1293. assert ctx["head"][ix[0]][ix[1]]["is_visible"] is False
  1294. def test_get_level_lengths_mi_hidden():
  1295. # GH 43464
  1296. index = MultiIndex.from_arrays([[1, 1, 1, 2, 2, 2], ["a", "a", "b", "a", "a", "b"]])
  1297. expected = {
  1298. (0, 2): 1,
  1299. (0, 3): 1,
  1300. (0, 4): 1,
  1301. (0, 5): 1,
  1302. (1, 2): 1,
  1303. (1, 3): 1,
  1304. (1, 4): 1,
  1305. (1, 5): 1,
  1306. }
  1307. result = _get_level_lengths(
  1308. index,
  1309. sparsify=False,
  1310. max_index=100,
  1311. hidden_elements=[0, 1, 0, 1], # hidden element can repeat if duplicated index
  1312. )
  1313. tm.assert_dict_equal(result, expected)
  1314. def test_row_trimming_hide_index():
  1315. # gh 43703
  1316. df = DataFrame([[1], [2], [3], [4], [5]])
  1317. with option_context("styler.render.max_rows", 2):
  1318. ctx = df.style.hide([0, 1], axis="index")._translate(True, True)
  1319. assert len(ctx["body"]) == 3
  1320. for r, val in enumerate(["3", "4", "..."]):
  1321. assert ctx["body"][r][1]["display_value"] == val
  1322. def test_row_trimming_hide_index_mi():
  1323. # gh 44247
  1324. df = DataFrame([[1], [2], [3], [4], [5]])
  1325. df.index = MultiIndex.from_product([[0], [0, 1, 2, 3, 4]])
  1326. with option_context("styler.render.max_rows", 2):
  1327. ctx = df.style.hide([(0, 0), (0, 1)], axis="index")._translate(True, True)
  1328. assert len(ctx["body"]) == 3
  1329. # level 0 index headers (sparsified)
  1330. assert {"value": 0, "attributes": 'rowspan="2"', "is_visible": True}.items() <= ctx[
  1331. "body"
  1332. ][0][0].items()
  1333. assert {"value": 0, "attributes": "", "is_visible": False}.items() <= ctx["body"][
  1334. 1
  1335. ][0].items()
  1336. assert {"value": "...", "is_visible": True}.items() <= ctx["body"][2][0].items()
  1337. for r, val in enumerate(["2", "3", "..."]):
  1338. assert ctx["body"][r][1]["display_value"] == val # level 1 index headers
  1339. for r, val in enumerate(["3", "4", "..."]):
  1340. assert ctx["body"][r][2]["display_value"] == val # data values
  1341. def test_col_trimming_hide_columns():
  1342. # gh 44272
  1343. df = DataFrame([[1, 2, 3, 4, 5]])
  1344. with option_context("styler.render.max_columns", 2):
  1345. ctx = df.style.hide([0, 1], axis="columns")._translate(True, True)
  1346. assert len(ctx["head"][0]) == 6 # blank, [0, 1 (hidden)], [2 ,3 (visible)], + trim
  1347. for c, vals in enumerate([(1, False), (2, True), (3, True), ("...", True)]):
  1348. assert ctx["head"][0][c + 2]["value"] == vals[0]
  1349. assert ctx["head"][0][c + 2]["is_visible"] == vals[1]
  1350. assert len(ctx["body"][0]) == 6 # index + 2 hidden + 2 visible + trimming col
  1351. def test_no_empty_apply(mi_styler):
  1352. # 45313
  1353. mi_styler.apply(lambda s: ["a:v;"] * 2, subset=[False, False])
  1354. mi_styler._compute()
  1355. @pytest.mark.parametrize("format", ["html", "latex", "string"])
  1356. def test_output_buffer(mi_styler, format):
  1357. # gh 47053
  1358. with tm.ensure_clean(f"delete_me.{format}") as f:
  1359. getattr(mi_styler, f"to_{format}")(f)