test_html.py 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. from textwrap import (
  2. dedent,
  3. indent,
  4. )
  5. import numpy as np
  6. import pytest
  7. from pandas import (
  8. DataFrame,
  9. MultiIndex,
  10. option_context,
  11. )
  12. jinja2 = pytest.importorskip("jinja2")
  13. from pandas.io.formats.style import Styler
  14. loader = jinja2.PackageLoader("pandas", "io/formats/templates")
  15. env = jinja2.Environment(loader=loader, trim_blocks=True)
  16. @pytest.fixture
  17. def styler():
  18. return Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]))
  19. @pytest.fixture
  20. def styler_mi():
  21. midx = MultiIndex.from_product([["a", "b"], ["c", "d"]])
  22. return Styler(DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=midx))
  23. @pytest.fixture
  24. def tpl_style():
  25. return env.get_template("html_style.tpl")
  26. @pytest.fixture
  27. def tpl_table():
  28. return env.get_template("html_table.tpl")
  29. def test_html_template_extends_options():
  30. # make sure if templates are edited tests are updated as are setup fixtures
  31. # to understand the dependency
  32. with open("pandas/io/formats/templates/html.tpl") as file:
  33. result = file.read()
  34. assert "{% include html_style_tpl %}" in result
  35. assert "{% include html_table_tpl %}" in result
  36. def test_exclude_styles(styler):
  37. result = styler.to_html(exclude_styles=True, doctype_html=True)
  38. expected = dedent(
  39. """\
  40. <!DOCTYPE html>
  41. <html>
  42. <head>
  43. <meta charset="utf-8">
  44. </head>
  45. <body>
  46. <table>
  47. <thead>
  48. <tr>
  49. <th >&nbsp;</th>
  50. <th >A</th>
  51. </tr>
  52. </thead>
  53. <tbody>
  54. <tr>
  55. <th >a</th>
  56. <td >2.610000</td>
  57. </tr>
  58. <tr>
  59. <th >b</th>
  60. <td >2.690000</td>
  61. </tr>
  62. </tbody>
  63. </table>
  64. </body>
  65. </html>
  66. """
  67. )
  68. assert result == expected
  69. def test_w3_html_format(styler):
  70. styler.set_uuid("").set_table_styles(
  71. [{"selector": "th", "props": "att2:v2;"}]
  72. ).applymap(lambda x: "att1:v1;").set_table_attributes(
  73. 'class="my-cls1" style="attr3:v3;"'
  74. ).set_td_classes(
  75. DataFrame(["my-cls2"], index=["a"], columns=["A"])
  76. ).format(
  77. "{:.1f}"
  78. ).set_caption(
  79. "A comprehensive test"
  80. )
  81. expected = dedent(
  82. """\
  83. <style type="text/css">
  84. #T_ th {
  85. att2: v2;
  86. }
  87. #T__row0_col0, #T__row1_col0 {
  88. att1: v1;
  89. }
  90. </style>
  91. <table id="T_" class="my-cls1" style="attr3:v3;">
  92. <caption>A comprehensive test</caption>
  93. <thead>
  94. <tr>
  95. <th class="blank level0" >&nbsp;</th>
  96. <th id="T__level0_col0" class="col_heading level0 col0" >A</th>
  97. </tr>
  98. </thead>
  99. <tbody>
  100. <tr>
  101. <th id="T__level0_row0" class="row_heading level0 row0" >a</th>
  102. <td id="T__row0_col0" class="data row0 col0 my-cls2" >2.6</td>
  103. </tr>
  104. <tr>
  105. <th id="T__level0_row1" class="row_heading level0 row1" >b</th>
  106. <td id="T__row1_col0" class="data row1 col0" >2.7</td>
  107. </tr>
  108. </tbody>
  109. </table>
  110. """
  111. )
  112. assert expected == styler.to_html()
  113. def test_colspan_w3():
  114. # GH 36223
  115. df = DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]])
  116. styler = Styler(df, uuid="_", cell_ids=False)
  117. assert '<th class="col_heading level0 col0" colspan="2">l0</th>' in styler.to_html()
  118. def test_rowspan_w3():
  119. # GH 38533
  120. df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]])
  121. styler = Styler(df, uuid="_", cell_ids=False)
  122. assert '<th class="row_heading level0 row0" rowspan="2">l0</th>' in styler.to_html()
  123. def test_styles(styler):
  124. styler.set_uuid("abc")
  125. styler.set_table_styles([{"selector": "td", "props": "color: red;"}])
  126. result = styler.to_html(doctype_html=True)
  127. expected = dedent(
  128. """\
  129. <!DOCTYPE html>
  130. <html>
  131. <head>
  132. <meta charset="utf-8">
  133. <style type="text/css">
  134. #T_abc td {
  135. color: red;
  136. }
  137. </style>
  138. </head>
  139. <body>
  140. <table id="T_abc">
  141. <thead>
  142. <tr>
  143. <th class="blank level0" >&nbsp;</th>
  144. <th id="T_abc_level0_col0" class="col_heading level0 col0" >A</th>
  145. </tr>
  146. </thead>
  147. <tbody>
  148. <tr>
  149. <th id="T_abc_level0_row0" class="row_heading level0 row0" >a</th>
  150. <td id="T_abc_row0_col0" class="data row0 col0" >2.610000</td>
  151. </tr>
  152. <tr>
  153. <th id="T_abc_level0_row1" class="row_heading level0 row1" >b</th>
  154. <td id="T_abc_row1_col0" class="data row1 col0" >2.690000</td>
  155. </tr>
  156. </tbody>
  157. </table>
  158. </body>
  159. </html>
  160. """
  161. )
  162. assert result == expected
  163. def test_doctype(styler):
  164. result = styler.to_html(doctype_html=False)
  165. assert "<html>" not in result
  166. assert "<body>" not in result
  167. assert "<!DOCTYPE html>" not in result
  168. assert "<head>" not in result
  169. def test_doctype_encoding(styler):
  170. with option_context("styler.render.encoding", "ASCII"):
  171. result = styler.to_html(doctype_html=True)
  172. assert '<meta charset="ASCII">' in result
  173. result = styler.to_html(doctype_html=True, encoding="ANSI")
  174. assert '<meta charset="ANSI">' in result
  175. def test_bold_headers_arg(styler):
  176. result = styler.to_html(bold_headers=True)
  177. assert "th {\n font-weight: bold;\n}" in result
  178. result = styler.to_html()
  179. assert "th {\n font-weight: bold;\n}" not in result
  180. def test_caption_arg(styler):
  181. result = styler.to_html(caption="foo bar")
  182. assert "<caption>foo bar</caption>" in result
  183. result = styler.to_html()
  184. assert "<caption>foo bar</caption>" not in result
  185. def test_block_names(tpl_style, tpl_table):
  186. # catch accidental removal of a block
  187. expected_style = {
  188. "before_style",
  189. "style",
  190. "table_styles",
  191. "before_cellstyle",
  192. "cellstyle",
  193. }
  194. expected_table = {
  195. "before_table",
  196. "table",
  197. "caption",
  198. "thead",
  199. "tbody",
  200. "after_table",
  201. "before_head_rows",
  202. "head_tr",
  203. "after_head_rows",
  204. "before_rows",
  205. "tr",
  206. "after_rows",
  207. }
  208. result1 = set(tpl_style.blocks)
  209. assert result1 == expected_style
  210. result2 = set(tpl_table.blocks)
  211. assert result2 == expected_table
  212. def test_from_custom_template_table(tmpdir):
  213. p = tmpdir.mkdir("tpl").join("myhtml_table.tpl")
  214. p.write(
  215. dedent(
  216. """\
  217. {% extends "html_table.tpl" %}
  218. {% block table %}
  219. <h1>{{custom_title}}</h1>
  220. {{ super() }}
  221. {% endblock table %}"""
  222. )
  223. )
  224. result = Styler.from_custom_template(str(tmpdir.join("tpl")), "myhtml_table.tpl")
  225. assert issubclass(result, Styler)
  226. assert result.env is not Styler.env
  227. assert result.template_html_table is not Styler.template_html_table
  228. styler = result(DataFrame({"A": [1, 2]}))
  229. assert "<h1>My Title</h1>\n\n\n<table" in styler.to_html(custom_title="My Title")
  230. def test_from_custom_template_style(tmpdir):
  231. p = tmpdir.mkdir("tpl").join("myhtml_style.tpl")
  232. p.write(
  233. dedent(
  234. """\
  235. {% extends "html_style.tpl" %}
  236. {% block style %}
  237. <link rel="stylesheet" href="mystyle.css">
  238. {{ super() }}
  239. {% endblock style %}"""
  240. )
  241. )
  242. result = Styler.from_custom_template(
  243. str(tmpdir.join("tpl")), html_style="myhtml_style.tpl"
  244. )
  245. assert issubclass(result, Styler)
  246. assert result.env is not Styler.env
  247. assert result.template_html_style is not Styler.template_html_style
  248. styler = result(DataFrame({"A": [1, 2]}))
  249. assert '<link rel="stylesheet" href="mystyle.css">\n\n<style' in styler.to_html()
  250. def test_caption_as_sequence(styler):
  251. styler.set_caption(("full cap", "short cap"))
  252. assert "<caption>full cap</caption>" in styler.to_html()
  253. @pytest.mark.parametrize("index", [False, True])
  254. @pytest.mark.parametrize("columns", [False, True])
  255. @pytest.mark.parametrize("index_name", [True, False])
  256. def test_sticky_basic(styler, index, columns, index_name):
  257. if index_name:
  258. styler.index.name = "some text"
  259. if index:
  260. styler.set_sticky(axis=0)
  261. if columns:
  262. styler.set_sticky(axis=1)
  263. left_css = (
  264. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  265. " left: 0px;\n z-index: {1};\n}}"
  266. )
  267. top_css = (
  268. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  269. " top: {1}px;\n z-index: {2};\n{3}}}"
  270. )
  271. res = styler.set_uuid("").to_html()
  272. # test index stickys over thead and tbody
  273. assert (left_css.format("thead tr th:nth-child(1)", "3 !important") in res) is index
  274. assert (left_css.format("tbody tr th:nth-child(1)", "1") in res) is index
  275. # test column stickys including if name row
  276. assert (
  277. top_css.format("thead tr:nth-child(1) th", "0", "2", " height: 25px;\n") in res
  278. ) is (columns and index_name)
  279. assert (
  280. top_css.format("thead tr:nth-child(2) th", "25", "2", " height: 25px;\n")
  281. in res
  282. ) is (columns and index_name)
  283. assert (top_css.format("thead tr:nth-child(1) th", "0", "2", "") in res) is (
  284. columns and not index_name
  285. )
  286. @pytest.mark.parametrize("index", [False, True])
  287. @pytest.mark.parametrize("columns", [False, True])
  288. def test_sticky_mi(styler_mi, index, columns):
  289. if index:
  290. styler_mi.set_sticky(axis=0)
  291. if columns:
  292. styler_mi.set_sticky(axis=1)
  293. left_css = (
  294. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  295. " left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
  296. )
  297. top_css = (
  298. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  299. " top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
  300. )
  301. res = styler_mi.set_uuid("").to_html()
  302. # test the index stickys for thead and tbody over both levels
  303. assert (
  304. left_css.format("thead tr th:nth-child(1)", "0", "3 !important") in res
  305. ) is index
  306. assert (left_css.format("tbody tr th.level0", "0", "1") in res) is index
  307. assert (
  308. left_css.format("thead tr th:nth-child(2)", "75", "3 !important") in res
  309. ) is index
  310. assert (left_css.format("tbody tr th.level1", "75", "1") in res) is index
  311. # test the column stickys for each level row
  312. assert (top_css.format("thead tr:nth-child(1) th", "0", "2") in res) is columns
  313. assert (top_css.format("thead tr:nth-child(2) th", "25", "2") in res) is columns
  314. @pytest.mark.parametrize("index", [False, True])
  315. @pytest.mark.parametrize("columns", [False, True])
  316. @pytest.mark.parametrize("levels", [[1], ["one"], "one"])
  317. def test_sticky_levels(styler_mi, index, columns, levels):
  318. styler_mi.index.names, styler_mi.columns.names = ["zero", "one"], ["zero", "one"]
  319. if index:
  320. styler_mi.set_sticky(axis=0, levels=levels)
  321. if columns:
  322. styler_mi.set_sticky(axis=1, levels=levels)
  323. left_css = (
  324. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  325. " left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
  326. )
  327. top_css = (
  328. "#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
  329. " top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
  330. )
  331. res = styler_mi.set_uuid("").to_html()
  332. # test no sticking of level0
  333. assert "#T_ thead tr th:nth-child(1)" not in res
  334. assert "#T_ tbody tr th.level0" not in res
  335. assert "#T_ thead tr:nth-child(1) th" not in res
  336. # test sticking level1
  337. assert (
  338. left_css.format("thead tr th:nth-child(2)", "0", "3 !important") in res
  339. ) is index
  340. assert (left_css.format("tbody tr th.level1", "0", "1") in res) is index
  341. assert (top_css.format("thead tr:nth-child(2) th", "0", "2") in res) is columns
  342. def test_sticky_raises(styler):
  343. with pytest.raises(ValueError, match="No axis named bad for object type DataFrame"):
  344. styler.set_sticky(axis="bad")
  345. @pytest.mark.parametrize(
  346. "sparse_index, sparse_columns",
  347. [(True, True), (True, False), (False, True), (False, False)],
  348. )
  349. def test_sparse_options(sparse_index, sparse_columns):
  350. cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
  351. ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
  352. df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], index=ridx, columns=cidx)
  353. styler = df.style
  354. default_html = styler.to_html() # defaults under pd.options to (True , True)
  355. with option_context(
  356. "styler.sparse.index", sparse_index, "styler.sparse.columns", sparse_columns
  357. ):
  358. html1 = styler.to_html()
  359. assert (html1 == default_html) is (sparse_index and sparse_columns)
  360. html2 = styler.to_html(sparse_index=sparse_index, sparse_columns=sparse_columns)
  361. assert html1 == html2
  362. @pytest.mark.parametrize("index", [True, False])
  363. @pytest.mark.parametrize("columns", [True, False])
  364. def test_applymap_header_cell_ids(styler, index, columns):
  365. # GH 41893
  366. func = lambda v: "attr: val;"
  367. styler.uuid, styler.cell_ids = "", False
  368. if index:
  369. styler.applymap_index(func, axis="index")
  370. if columns:
  371. styler.applymap_index(func, axis="columns")
  372. result = styler.to_html()
  373. # test no data cell ids
  374. assert '<td class="data row0 col0" >2.610000</td>' in result
  375. assert '<td class="data row1 col0" >2.690000</td>' in result
  376. # test index header ids where needed and css styles
  377. assert (
  378. '<th id="T__level0_row0" class="row_heading level0 row0" >a</th>' in result
  379. ) is index
  380. assert (
  381. '<th id="T__level0_row1" class="row_heading level0 row1" >b</th>' in result
  382. ) is index
  383. assert ("#T__level0_row0, #T__level0_row1 {\n attr: val;\n}" in result) is index
  384. # test column header ids where needed and css styles
  385. assert (
  386. '<th id="T__level0_col0" class="col_heading level0 col0" >A</th>' in result
  387. ) is columns
  388. assert ("#T__level0_col0 {\n attr: val;\n}" in result) is columns
  389. @pytest.mark.parametrize("rows", [True, False])
  390. @pytest.mark.parametrize("cols", [True, False])
  391. def test_maximums(styler_mi, rows, cols):
  392. result = styler_mi.to_html(
  393. max_rows=2 if rows else None,
  394. max_columns=2 if cols else None,
  395. )
  396. assert ">5</td>" in result # [[0,1], [4,5]] always visible
  397. assert (">8</td>" in result) is not rows # first trimmed vertical element
  398. assert (">2</td>" in result) is not cols # first trimmed horizontal element
  399. def test_replaced_css_class_names():
  400. css = {
  401. "row_heading": "ROWHEAD",
  402. # "col_heading": "COLHEAD",
  403. "index_name": "IDXNAME",
  404. # "col": "COL",
  405. "row": "ROW",
  406. # "col_trim": "COLTRIM",
  407. "row_trim": "ROWTRIM",
  408. "level": "LEVEL",
  409. "data": "DATA",
  410. "blank": "BLANK",
  411. }
  412. midx = MultiIndex.from_product([["a", "b"], ["c", "d"]])
  413. styler_mi = Styler(
  414. DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=midx),
  415. uuid_len=0,
  416. ).set_table_styles(css_class_names=css)
  417. styler_mi.index.names = ["n1", "n2"]
  418. styler_mi.hide(styler_mi.index[1:], axis=0)
  419. styler_mi.hide(styler_mi.columns[1:], axis=1)
  420. styler_mi.applymap_index(lambda v: "color: red;", axis=0)
  421. styler_mi.applymap_index(lambda v: "color: green;", axis=1)
  422. styler_mi.applymap(lambda v: "color: blue;")
  423. expected = dedent(
  424. """\
  425. <style type="text/css">
  426. #T__ROW0_col0 {
  427. color: blue;
  428. }
  429. #T__LEVEL0_ROW0, #T__LEVEL1_ROW0 {
  430. color: red;
  431. }
  432. #T__LEVEL0_col0, #T__LEVEL1_col0 {
  433. color: green;
  434. }
  435. </style>
  436. <table id="T_">
  437. <thead>
  438. <tr>
  439. <th class="BLANK" >&nbsp;</th>
  440. <th class="IDXNAME LEVEL0" >n1</th>
  441. <th id="T__LEVEL0_col0" class="col_heading LEVEL0 col0" >a</th>
  442. </tr>
  443. <tr>
  444. <th class="BLANK" >&nbsp;</th>
  445. <th class="IDXNAME LEVEL1" >n2</th>
  446. <th id="T__LEVEL1_col0" class="col_heading LEVEL1 col0" >c</th>
  447. </tr>
  448. <tr>
  449. <th class="IDXNAME LEVEL0" >n1</th>
  450. <th class="IDXNAME LEVEL1" >n2</th>
  451. <th class="BLANK col0" >&nbsp;</th>
  452. </tr>
  453. </thead>
  454. <tbody>
  455. <tr>
  456. <th id="T__LEVEL0_ROW0" class="ROWHEAD LEVEL0 ROW0" >a</th>
  457. <th id="T__LEVEL1_ROW0" class="ROWHEAD LEVEL1 ROW0" >c</th>
  458. <td id="T__ROW0_col0" class="DATA ROW0 col0" >0</td>
  459. </tr>
  460. </tbody>
  461. </table>
  462. """
  463. )
  464. result = styler_mi.to_html()
  465. assert result == expected
  466. def test_include_css_style_rules_only_for_visible_cells(styler_mi):
  467. # GH 43619
  468. result = (
  469. styler_mi.set_uuid("")
  470. .applymap(lambda v: "color: blue;")
  471. .hide(styler_mi.data.columns[1:], axis="columns")
  472. .hide(styler_mi.data.index[1:], axis="index")
  473. .to_html()
  474. )
  475. expected_styles = dedent(
  476. """\
  477. <style type="text/css">
  478. #T__row0_col0 {
  479. color: blue;
  480. }
  481. </style>
  482. """
  483. )
  484. assert expected_styles in result
  485. def test_include_css_style_rules_only_for_visible_index_labels(styler_mi):
  486. # GH 43619
  487. result = (
  488. styler_mi.set_uuid("")
  489. .applymap_index(lambda v: "color: blue;", axis="index")
  490. .hide(styler_mi.data.columns, axis="columns")
  491. .hide(styler_mi.data.index[1:], axis="index")
  492. .to_html()
  493. )
  494. expected_styles = dedent(
  495. """\
  496. <style type="text/css">
  497. #T__level0_row0, #T__level1_row0 {
  498. color: blue;
  499. }
  500. </style>
  501. """
  502. )
  503. assert expected_styles in result
  504. def test_include_css_style_rules_only_for_visible_column_labels(styler_mi):
  505. # GH 43619
  506. result = (
  507. styler_mi.set_uuid("")
  508. .applymap_index(lambda v: "color: blue;", axis="columns")
  509. .hide(styler_mi.data.columns[1:], axis="columns")
  510. .hide(styler_mi.data.index, axis="index")
  511. .to_html()
  512. )
  513. expected_styles = dedent(
  514. """\
  515. <style type="text/css">
  516. #T__level0_col0, #T__level1_col0 {
  517. color: blue;
  518. }
  519. </style>
  520. """
  521. )
  522. assert expected_styles in result
  523. def test_hiding_index_columns_multiindex_alignment():
  524. # gh 43644
  525. midx = MultiIndex.from_product(
  526. [["i0", "j0"], ["i1"], ["i2", "j2"]], names=["i-0", "i-1", "i-2"]
  527. )
  528. cidx = MultiIndex.from_product(
  529. [["c0"], ["c1", "d1"], ["c2", "d2"]], names=["c-0", "c-1", "c-2"]
  530. )
  531. df = DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=cidx)
  532. styler = Styler(df, uuid_len=0)
  533. styler.hide(level=1, axis=0).hide(level=0, axis=1)
  534. styler.hide([("j0", "i1", "j2")], axis=0)
  535. styler.hide([("c0", "d1", "d2")], axis=1)
  536. result = styler.to_html()
  537. expected = dedent(
  538. """\
  539. <style type="text/css">
  540. </style>
  541. <table id="T_">
  542. <thead>
  543. <tr>
  544. <th class="blank" >&nbsp;</th>
  545. <th class="index_name level1" >c-1</th>
  546. <th id="T__level1_col0" class="col_heading level1 col0" colspan="2">c1</th>
  547. <th id="T__level1_col2" class="col_heading level1 col2" >d1</th>
  548. </tr>
  549. <tr>
  550. <th class="blank" >&nbsp;</th>
  551. <th class="index_name level2" >c-2</th>
  552. <th id="T__level2_col0" class="col_heading level2 col0" >c2</th>
  553. <th id="T__level2_col1" class="col_heading level2 col1" >d2</th>
  554. <th id="T__level2_col2" class="col_heading level2 col2" >c2</th>
  555. </tr>
  556. <tr>
  557. <th class="index_name level0" >i-0</th>
  558. <th class="index_name level2" >i-2</th>
  559. <th class="blank col0" >&nbsp;</th>
  560. <th class="blank col1" >&nbsp;</th>
  561. <th class="blank col2" >&nbsp;</th>
  562. </tr>
  563. </thead>
  564. <tbody>
  565. <tr>
  566. <th id="T__level0_row0" class="row_heading level0 row0" rowspan="2">i0</th>
  567. <th id="T__level2_row0" class="row_heading level2 row0" >i2</th>
  568. <td id="T__row0_col0" class="data row0 col0" >0</td>
  569. <td id="T__row0_col1" class="data row0 col1" >1</td>
  570. <td id="T__row0_col2" class="data row0 col2" >2</td>
  571. </tr>
  572. <tr>
  573. <th id="T__level2_row1" class="row_heading level2 row1" >j2</th>
  574. <td id="T__row1_col0" class="data row1 col0" >4</td>
  575. <td id="T__row1_col1" class="data row1 col1" >5</td>
  576. <td id="T__row1_col2" class="data row1 col2" >6</td>
  577. </tr>
  578. <tr>
  579. <th id="T__level0_row2" class="row_heading level0 row2" >j0</th>
  580. <th id="T__level2_row2" class="row_heading level2 row2" >i2</th>
  581. <td id="T__row2_col0" class="data row2 col0" >8</td>
  582. <td id="T__row2_col1" class="data row2 col1" >9</td>
  583. <td id="T__row2_col2" class="data row2 col2" >10</td>
  584. </tr>
  585. </tbody>
  586. </table>
  587. """
  588. )
  589. assert result == expected
  590. def test_hiding_index_columns_multiindex_trimming():
  591. # gh 44272
  592. df = DataFrame(np.arange(64).reshape(8, 8))
  593. df.columns = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
  594. df.index = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
  595. df.index.names, df.columns.names = ["a", "b"], ["c", "d"]
  596. styler = Styler(df, cell_ids=False, uuid_len=0)
  597. styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0)
  598. with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4):
  599. result = styler.to_html()
  600. expected = dedent(
  601. """\
  602. <style type="text/css">
  603. </style>
  604. <table id="T_">
  605. <thead>
  606. <tr>
  607. <th class="blank" >&nbsp;</th>
  608. <th class="index_name level0" >c</th>
  609. <th class="col_heading level0 col3" >1</th>
  610. <th class="col_heading level0 col4" colspan="2">2</th>
  611. <th class="col_heading level0 col6" >3</th>
  612. </tr>
  613. <tr>
  614. <th class="blank" >&nbsp;</th>
  615. <th class="index_name level1" >d</th>
  616. <th class="col_heading level1 col3" >1</th>
  617. <th class="col_heading level1 col4" >0</th>
  618. <th class="col_heading level1 col5" >1</th>
  619. <th class="col_heading level1 col6" >0</th>
  620. <th class="col_heading level1 col_trim" >...</th>
  621. </tr>
  622. <tr>
  623. <th class="index_name level0" >a</th>
  624. <th class="index_name level1" >b</th>
  625. <th class="blank col3" >&nbsp;</th>
  626. <th class="blank col4" >&nbsp;</th>
  627. <th class="blank col5" >&nbsp;</th>
  628. <th class="blank col6" >&nbsp;</th>
  629. <th class="blank col7 col_trim" >&nbsp;</th>
  630. </tr>
  631. </thead>
  632. <tbody>
  633. <tr>
  634. <th class="row_heading level0 row3" >1</th>
  635. <th class="row_heading level1 row3" >1</th>
  636. <td class="data row3 col3" >27</td>
  637. <td class="data row3 col4" >28</td>
  638. <td class="data row3 col5" >29</td>
  639. <td class="data row3 col6" >30</td>
  640. <td class="data row3 col_trim" >...</td>
  641. </tr>
  642. <tr>
  643. <th class="row_heading level0 row4" rowspan="2">2</th>
  644. <th class="row_heading level1 row4" >0</th>
  645. <td class="data row4 col3" >35</td>
  646. <td class="data row4 col4" >36</td>
  647. <td class="data row4 col5" >37</td>
  648. <td class="data row4 col6" >38</td>
  649. <td class="data row4 col_trim" >...</td>
  650. </tr>
  651. <tr>
  652. <th class="row_heading level1 row5" >1</th>
  653. <td class="data row5 col3" >43</td>
  654. <td class="data row5 col4" >44</td>
  655. <td class="data row5 col5" >45</td>
  656. <td class="data row5 col6" >46</td>
  657. <td class="data row5 col_trim" >...</td>
  658. </tr>
  659. <tr>
  660. <th class="row_heading level0 row6" >3</th>
  661. <th class="row_heading level1 row6" >0</th>
  662. <td class="data row6 col3" >51</td>
  663. <td class="data row6 col4" >52</td>
  664. <td class="data row6 col5" >53</td>
  665. <td class="data row6 col6" >54</td>
  666. <td class="data row6 col_trim" >...</td>
  667. </tr>
  668. <tr>
  669. <th class="row_heading level0 row_trim" >...</th>
  670. <th class="row_heading level1 row_trim" >...</th>
  671. <td class="data col3 row_trim" >...</td>
  672. <td class="data col4 row_trim" >...</td>
  673. <td class="data col5 row_trim" >...</td>
  674. <td class="data col6 row_trim" >...</td>
  675. <td class="data row_trim col_trim" >...</td>
  676. </tr>
  677. </tbody>
  678. </table>
  679. """
  680. )
  681. assert result == expected
  682. @pytest.mark.parametrize("type", ["data", "index"])
  683. @pytest.mark.parametrize(
  684. "text, exp, found",
  685. [
  686. ("no link, just text", False, ""),
  687. ("subdomain not www: sub.web.com", False, ""),
  688. ("www subdomain: www.web.com other", True, "www.web.com"),
  689. ("scheme full structure: http://www.web.com", True, "http://www.web.com"),
  690. ("scheme no top-level: http://www.web", True, "http://www.web"),
  691. ("no scheme, no top-level: www.web", False, "www.web"),
  692. ("https scheme: https://www.web.com", True, "https://www.web.com"),
  693. ("ftp scheme: ftp://www.web", True, "ftp://www.web"),
  694. ("ftps scheme: ftps://www.web", True, "ftps://www.web"),
  695. ("subdirectories: www.web.com/directory", True, "www.web.com/directory"),
  696. ("Multiple domains: www.1.2.3.4", True, "www.1.2.3.4"),
  697. ("with port: http://web.com:80", True, "http://web.com:80"),
  698. (
  699. "full net_loc scheme: http://user:pass@web.com",
  700. True,
  701. "http://user:pass@web.com",
  702. ),
  703. (
  704. "with valid special chars: http://web.com/,.':;~!@#$*()[]",
  705. True,
  706. "http://web.com/,.':;~!@#$*()[]",
  707. ),
  708. ],
  709. )
  710. def test_rendered_links(type, text, exp, found):
  711. if type == "data":
  712. df = DataFrame([text])
  713. styler = df.style.format(hyperlinks="html")
  714. else:
  715. df = DataFrame([0], index=[text])
  716. styler = df.style.format_index(hyperlinks="html")
  717. rendered = f'<a href="{found}" target="_blank">{found}</a>'
  718. result = styler.to_html()
  719. assert (rendered in result) is exp
  720. assert (text in result) is not exp # test conversion done when expected and not
  721. def test_multiple_rendered_links():
  722. links = ("www.a.b", "http://a.c", "https://a.d", "ftp://a.e")
  723. # pylint: disable-next=consider-using-f-string
  724. df = DataFrame(["text {} {} text {} {}".format(*links)])
  725. result = df.style.format(hyperlinks="html").to_html()
  726. href = '<a href="{0}" target="_blank">{0}</a>'
  727. for link in links:
  728. assert href.format(link) in result
  729. assert href.format("text") not in result
  730. def test_concat(styler):
  731. other = styler.data.agg(["mean"]).style
  732. styler.concat(other).set_uuid("X")
  733. result = styler.to_html()
  734. fp = "foot0_"
  735. expected = dedent(
  736. f"""\
  737. <tr>
  738. <th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
  739. <td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
  740. </tr>
  741. <tr>
  742. <th id="T_X_level0_{fp}row0" class="{fp}row_heading level0 {fp}row0" >mean</th>
  743. <td id="T_X_{fp}row0_col0" class="{fp}data {fp}row0 col0" >2.650000</td>
  744. </tr>
  745. </tbody>
  746. </table>
  747. """
  748. )
  749. assert expected in result
  750. def test_concat_recursion(styler):
  751. df = styler.data
  752. styler1 = styler
  753. styler2 = Styler(df.agg(["mean"]), precision=3)
  754. styler3 = Styler(df.agg(["mean"]), precision=4)
  755. styler1.concat(styler2.concat(styler3)).set_uuid("X")
  756. result = styler.to_html()
  757. # notice that the second concat (last <tr> of the output html),
  758. # there are two `foot_` in the id and class
  759. fp1 = "foot0_"
  760. fp2 = "foot0_foot0_"
  761. expected = dedent(
  762. f"""\
  763. <tr>
  764. <th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
  765. <td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
  766. </tr>
  767. <tr>
  768. <th id="T_X_level0_{fp1}row0" class="{fp1}row_heading level0 {fp1}row0" >mean</th>
  769. <td id="T_X_{fp1}row0_col0" class="{fp1}data {fp1}row0 col0" >2.650</td>
  770. </tr>
  771. <tr>
  772. <th id="T_X_level0_{fp2}row0" class="{fp2}row_heading level0 {fp2}row0" >mean</th>
  773. <td id="T_X_{fp2}row0_col0" class="{fp2}data {fp2}row0 col0" >2.6500</td>
  774. </tr>
  775. </tbody>
  776. </table>
  777. """
  778. )
  779. assert expected in result
  780. def test_concat_chain(styler):
  781. df = styler.data
  782. styler1 = styler
  783. styler2 = Styler(df.agg(["mean"]), precision=3)
  784. styler3 = Styler(df.agg(["mean"]), precision=4)
  785. styler1.concat(styler2).concat(styler3).set_uuid("X")
  786. result = styler.to_html()
  787. fp1 = "foot0_"
  788. fp2 = "foot1_"
  789. expected = dedent(
  790. f"""\
  791. <tr>
  792. <th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
  793. <td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
  794. </tr>
  795. <tr>
  796. <th id="T_X_level0_{fp1}row0" class="{fp1}row_heading level0 {fp1}row0" >mean</th>
  797. <td id="T_X_{fp1}row0_col0" class="{fp1}data {fp1}row0 col0" >2.650</td>
  798. </tr>
  799. <tr>
  800. <th id="T_X_level0_{fp2}row0" class="{fp2}row_heading level0 {fp2}row0" >mean</th>
  801. <td id="T_X_{fp2}row0_col0" class="{fp2}data {fp2}row0 col0" >2.6500</td>
  802. </tr>
  803. </tbody>
  804. </table>
  805. """
  806. )
  807. assert expected in result
  808. def test_concat_combined():
  809. def html_lines(foot_prefix: str):
  810. assert foot_prefix.endswith("_") or foot_prefix == ""
  811. fp = foot_prefix
  812. return indent(
  813. dedent(
  814. f"""\
  815. <tr>
  816. <th id="T_X_level0_{fp}row0" class="{fp}row_heading level0 {fp}row0" >a</th>
  817. <td id="T_X_{fp}row0_col0" class="{fp}data {fp}row0 col0" >2.610000</td>
  818. </tr>
  819. <tr>
  820. <th id="T_X_level0_{fp}row1" class="{fp}row_heading level0 {fp}row1" >b</th>
  821. <td id="T_X_{fp}row1_col0" class="{fp}data {fp}row1 col0" >2.690000</td>
  822. </tr>
  823. """
  824. ),
  825. prefix=" " * 4,
  826. )
  827. df = DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])
  828. s1 = df.style.highlight_max(color="red")
  829. s2 = df.style.highlight_max(color="green")
  830. s3 = df.style.highlight_max(color="blue")
  831. s4 = df.style.highlight_max(color="yellow")
  832. result = s1.concat(s2).concat(s3.concat(s4)).set_uuid("X").to_html()
  833. expected_css = dedent(
  834. """\
  835. <style type="text/css">
  836. #T_X_row1_col0 {
  837. background-color: red;
  838. }
  839. #T_X_foot0_row1_col0 {
  840. background-color: green;
  841. }
  842. #T_X_foot1_row1_col0 {
  843. background-color: blue;
  844. }
  845. #T_X_foot1_foot0_row1_col0 {
  846. background-color: yellow;
  847. }
  848. </style>
  849. """
  850. )
  851. expected_table = (
  852. dedent(
  853. """\
  854. <table id="T_X">
  855. <thead>
  856. <tr>
  857. <th class="blank level0" >&nbsp;</th>
  858. <th id="T_X_level0_col0" class="col_heading level0 col0" >A</th>
  859. </tr>
  860. </thead>
  861. <tbody>
  862. """
  863. )
  864. + html_lines("")
  865. + html_lines("foot0_")
  866. + html_lines("foot1_")
  867. + html_lines("foot1_foot0_")
  868. + dedent(
  869. """\
  870. </tbody>
  871. </table>
  872. """
  873. )
  874. )
  875. assert expected_css + expected_table == result
  876. def test_to_html_na_rep_non_scalar_data(datapath):
  877. # GH47103
  878. df = DataFrame([dict(a=1, b=[1, 2, 3], c=np.nan)])
  879. result = df.style.format(na_rep="-").to_html(table_uuid="test")
  880. expected = """\
  881. <style type="text/css">
  882. </style>
  883. <table id="T_test">
  884. <thead>
  885. <tr>
  886. <th class="blank level0" >&nbsp;</th>
  887. <th id="T_test_level0_col0" class="col_heading level0 col0" >a</th>
  888. <th id="T_test_level0_col1" class="col_heading level0 col1" >b</th>
  889. <th id="T_test_level0_col2" class="col_heading level0 col2" >c</th>
  890. </tr>
  891. </thead>
  892. <tbody>
  893. <tr>
  894. <th id="T_test_level0_row0" class="row_heading level0 row0" >0</th>
  895. <td id="T_test_row0_col0" class="data row0 col0" >1</td>
  896. <td id="T_test_row0_col1" class="data row0 col1" >[1, 2, 3]</td>
  897. <td id="T_test_row0_col2" class="data row0 col2" >-</td>
  898. </tr>
  899. </tbody>
  900. </table>
  901. """
  902. assert result == expected