html.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. """
  2. Module for formatting output data in HTML.
  3. """
  4. from textwrap import dedent
  5. from typing import IO, Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, cast
  6. from pandas._config import get_option
  7. from pandas._libs import lib
  8. from pandas.core.dtypes.generic import ABCMultiIndex
  9. from pandas import option_context
  10. from pandas.io.common import is_url
  11. from pandas.io.formats.format import (
  12. DataFrameFormatter,
  13. TableFormatter,
  14. buffer_put_lines,
  15. get_level_lengths,
  16. )
  17. from pandas.io.formats.printing import pprint_thing
  18. class HTMLFormatter(TableFormatter):
  19. """
  20. Internal class for formatting output data in html.
  21. This class is intended for shared functionality between
  22. DataFrame.to_html() and DataFrame._repr_html_().
  23. Any logic in common with other output formatting methods
  24. should ideally be inherited from classes in format.py
  25. and this class responsible for only producing html markup.
  26. """
  27. indent_delta = 2
  28. def __init__(
  29. self,
  30. formatter: DataFrameFormatter,
  31. classes: Optional[Union[str, List[str], Tuple[str, ...]]] = None,
  32. border: Optional[int] = None,
  33. ) -> None:
  34. self.fmt = formatter
  35. self.classes = classes
  36. self.frame = self.fmt.frame
  37. self.columns = self.fmt.tr_frame.columns
  38. self.elements: List[str] = []
  39. self.bold_rows = self.fmt.bold_rows
  40. self.escape = self.fmt.escape
  41. self.show_dimensions = self.fmt.show_dimensions
  42. if border is None:
  43. border = cast(int, get_option("display.html.border"))
  44. self.border = border
  45. self.table_id = self.fmt.table_id
  46. self.render_links = self.fmt.render_links
  47. if isinstance(self.fmt.col_space, int):
  48. self.fmt.col_space = "{colspace}px".format(colspace=self.fmt.col_space)
  49. @property
  50. def show_row_idx_names(self) -> bool:
  51. return self.fmt.show_row_idx_names
  52. @property
  53. def show_col_idx_names(self) -> bool:
  54. return self.fmt.show_col_idx_names
  55. @property
  56. def row_levels(self) -> int:
  57. if self.fmt.index:
  58. # showing (row) index
  59. return self.frame.index.nlevels
  60. elif self.show_col_idx_names:
  61. # see gh-22579
  62. # Column misalignment also occurs for
  63. # a standard index when the columns index is named.
  64. # If the row index is not displayed a column of
  65. # blank cells need to be included before the DataFrame values.
  66. return 1
  67. # not showing (row) index
  68. return 0
  69. def _get_columns_formatted_values(self) -> Iterable:
  70. return self.columns
  71. # https://github.com/python/mypy/issues/1237
  72. @property
  73. def is_truncated(self) -> bool: # type: ignore
  74. return self.fmt.is_truncated
  75. @property
  76. def ncols(self) -> int:
  77. return len(self.fmt.tr_frame.columns)
  78. def write(self, s: Any, indent: int = 0) -> None:
  79. rs = pprint_thing(s)
  80. self.elements.append(" " * indent + rs)
  81. def write_th(
  82. self, s: Any, header: bool = False, indent: int = 0, tags: Optional[str] = None
  83. ) -> None:
  84. """
  85. Method for writting a formatted <th> cell.
  86. If col_space is set on the formatter then that is used for
  87. the value of min-width.
  88. Parameters
  89. ----------
  90. s : object
  91. The data to be written inside the cell.
  92. header : bool, default False
  93. Set to True if the <th> is for use inside <thead>. This will
  94. cause min-width to be set if there is one.
  95. indent : int, default 0
  96. The indentation level of the cell.
  97. tags : str, default None
  98. Tags to include in the cell.
  99. Returns
  100. -------
  101. A written <th> cell.
  102. """
  103. if header and self.fmt.col_space is not None:
  104. tags = tags or ""
  105. tags += 'style="min-width: {colspace};"'.format(colspace=self.fmt.col_space)
  106. self._write_cell(s, kind="th", indent=indent, tags=tags)
  107. def write_td(self, s: Any, indent: int = 0, tags: Optional[str] = None) -> None:
  108. self._write_cell(s, kind="td", indent=indent, tags=tags)
  109. def _write_cell(
  110. self, s: Any, kind: str = "td", indent: int = 0, tags: Optional[str] = None
  111. ) -> None:
  112. if tags is not None:
  113. start_tag = "<{kind} {tags}>".format(kind=kind, tags=tags)
  114. else:
  115. start_tag = "<{kind}>".format(kind=kind)
  116. if self.escape:
  117. # escape & first to prevent double escaping of &
  118. esc = {"&": r"&amp;", "<": r"&lt;", ">": r"&gt;"}
  119. else:
  120. esc = {}
  121. rs = pprint_thing(s, escape_chars=esc).strip()
  122. if self.render_links and is_url(rs):
  123. rs_unescaped = pprint_thing(s, escape_chars={}).strip()
  124. start_tag += '<a href="{url}" target="_blank">'.format(url=rs_unescaped)
  125. end_a = "</a>"
  126. else:
  127. end_a = ""
  128. self.write(
  129. "{start}{rs}{end_a}</{kind}>".format(
  130. start=start_tag, rs=rs, end_a=end_a, kind=kind
  131. ),
  132. indent,
  133. )
  134. def write_tr(
  135. self,
  136. line: Iterable,
  137. indent: int = 0,
  138. indent_delta: int = 0,
  139. header: bool = False,
  140. align: Optional[str] = None,
  141. tags: Optional[Dict[int, str]] = None,
  142. nindex_levels: int = 0,
  143. ) -> None:
  144. if tags is None:
  145. tags = {}
  146. if align is None:
  147. self.write("<tr>", indent)
  148. else:
  149. self.write('<tr style="text-align: {align};">'.format(align=align), indent)
  150. indent += indent_delta
  151. for i, s in enumerate(line):
  152. val_tag = tags.get(i, None)
  153. if header or (self.bold_rows and i < nindex_levels):
  154. self.write_th(s, indent=indent, header=header, tags=val_tag)
  155. else:
  156. self.write_td(s, indent, tags=val_tag)
  157. indent -= indent_delta
  158. self.write("</tr>", indent)
  159. def render(self) -> List[str]:
  160. self._write_table()
  161. if self.should_show_dimensions:
  162. by = chr(215) # ×
  163. self.write(
  164. "<p>{rows} rows {by} {cols} columns</p>".format(
  165. rows=len(self.frame), by=by, cols=len(self.frame.columns)
  166. )
  167. )
  168. return self.elements
  169. def write_result(self, buf: IO[str]) -> None:
  170. buffer_put_lines(buf, self.render())
  171. def _write_table(self, indent: int = 0) -> None:
  172. _classes = ["dataframe"] # Default class.
  173. use_mathjax = get_option("display.html.use_mathjax")
  174. if not use_mathjax:
  175. _classes.append("tex2jax_ignore")
  176. if self.classes is not None:
  177. if isinstance(self.classes, str):
  178. self.classes = self.classes.split()
  179. if not isinstance(self.classes, (list, tuple)):
  180. raise TypeError(
  181. "classes must be a string, list, or tuple, "
  182. "not {typ}".format(typ=type(self.classes))
  183. )
  184. _classes.extend(self.classes)
  185. if self.table_id is None:
  186. id_section = ""
  187. else:
  188. id_section = ' id="{table_id}"'.format(table_id=self.table_id)
  189. self.write(
  190. '<table border="{border}" class="{cls}"{id_section}>'.format(
  191. border=self.border, cls=" ".join(_classes), id_section=id_section
  192. ),
  193. indent,
  194. )
  195. if self.fmt.header or self.show_row_idx_names:
  196. self._write_header(indent + self.indent_delta)
  197. self._write_body(indent + self.indent_delta)
  198. self.write("</table>", indent)
  199. def _write_col_header(self, indent: int) -> None:
  200. truncate_h = self.fmt.truncate_h
  201. if isinstance(self.columns, ABCMultiIndex):
  202. template = 'colspan="{span:d}" halign="left"'
  203. if self.fmt.sparsify:
  204. # GH3547
  205. sentinel = lib.no_default
  206. else:
  207. sentinel = False
  208. levels = self.columns.format(sparsify=sentinel, adjoin=False, names=False)
  209. level_lengths = get_level_lengths(levels, sentinel)
  210. inner_lvl = len(level_lengths) - 1
  211. for lnum, (records, values) in enumerate(zip(level_lengths, levels)):
  212. if truncate_h:
  213. # modify the header lines
  214. ins_col = self.fmt.tr_col_num
  215. if self.fmt.sparsify:
  216. recs_new = {}
  217. # Increment tags after ... col.
  218. for tag, span in list(records.items()):
  219. if tag >= ins_col:
  220. recs_new[tag + 1] = span
  221. elif tag + span > ins_col:
  222. recs_new[tag] = span + 1
  223. if lnum == inner_lvl:
  224. values = (
  225. values[:ins_col] + ("...",) + values[ins_col:]
  226. )
  227. else:
  228. # sparse col headers do not receive a ...
  229. values = (
  230. values[:ins_col]
  231. + (values[ins_col - 1],)
  232. + values[ins_col:]
  233. )
  234. else:
  235. recs_new[tag] = span
  236. # if ins_col lies between tags, all col headers
  237. # get ...
  238. if tag + span == ins_col:
  239. recs_new[ins_col] = 1
  240. values = values[:ins_col] + ("...",) + values[ins_col:]
  241. records = recs_new
  242. inner_lvl = len(level_lengths) - 1
  243. if lnum == inner_lvl:
  244. records[ins_col] = 1
  245. else:
  246. recs_new = {}
  247. for tag, span in list(records.items()):
  248. if tag >= ins_col:
  249. recs_new[tag + 1] = span
  250. else:
  251. recs_new[tag] = span
  252. recs_new[ins_col] = 1
  253. records = recs_new
  254. values = values[:ins_col] + ["..."] + values[ins_col:]
  255. # see gh-22579
  256. # Column Offset Bug with to_html(index=False) with
  257. # MultiIndex Columns and Index.
  258. # Initially fill row with blank cells before column names.
  259. # TODO: Refactor to remove code duplication with code
  260. # block below for standard columns index.
  261. row = [""] * (self.row_levels - 1)
  262. if self.fmt.index or self.show_col_idx_names:
  263. # see gh-22747
  264. # If to_html(index_names=False) do not show columns
  265. # index names.
  266. # TODO: Refactor to use _get_column_name_list from
  267. # DataFrameFormatter class and create a
  268. # _get_formatted_column_labels function for code
  269. # parity with DataFrameFormatter class.
  270. if self.fmt.show_index_names:
  271. name = self.columns.names[lnum]
  272. row.append(pprint_thing(name or ""))
  273. else:
  274. row.append("")
  275. tags = {}
  276. j = len(row)
  277. for i, v in enumerate(values):
  278. if i in records:
  279. if records[i] > 1:
  280. tags[j] = template.format(span=records[i])
  281. else:
  282. continue
  283. j += 1
  284. row.append(v)
  285. self.write_tr(row, indent, self.indent_delta, tags=tags, header=True)
  286. else:
  287. # see gh-22579
  288. # Column misalignment also occurs for
  289. # a standard index when the columns index is named.
  290. # Initially fill row with blank cells before column names.
  291. # TODO: Refactor to remove code duplication with code block
  292. # above for columns MultiIndex.
  293. row = [""] * (self.row_levels - 1)
  294. if self.fmt.index or self.show_col_idx_names:
  295. # see gh-22747
  296. # If to_html(index_names=False) do not show columns
  297. # index names.
  298. # TODO: Refactor to use _get_column_name_list from
  299. # DataFrameFormatter class.
  300. if self.fmt.show_index_names:
  301. row.append(self.columns.name or "")
  302. else:
  303. row.append("")
  304. row.extend(self._get_columns_formatted_values())
  305. align = self.fmt.justify
  306. if truncate_h:
  307. ins_col = self.row_levels + self.fmt.tr_col_num
  308. row.insert(ins_col, "...")
  309. self.write_tr(row, indent, self.indent_delta, header=True, align=align)
  310. def _write_row_header(self, indent: int) -> None:
  311. truncate_h = self.fmt.truncate_h
  312. row = [x if x is not None else "" for x in self.frame.index.names] + [""] * (
  313. self.ncols + (1 if truncate_h else 0)
  314. )
  315. self.write_tr(row, indent, self.indent_delta, header=True)
  316. def _write_header(self, indent: int) -> None:
  317. self.write("<thead>", indent)
  318. if self.fmt.header:
  319. self._write_col_header(indent + self.indent_delta)
  320. if self.show_row_idx_names:
  321. self._write_row_header(indent + self.indent_delta)
  322. self.write("</thead>", indent)
  323. def _get_formatted_values(self) -> Dict[int, List[str]]:
  324. with option_context("display.max_colwidth", None):
  325. fmt_values = {i: self.fmt._format_col(i) for i in range(self.ncols)}
  326. return fmt_values
  327. def _write_body(self, indent: int) -> None:
  328. self.write("<tbody>", indent)
  329. fmt_values = self._get_formatted_values()
  330. # write values
  331. if self.fmt.index and isinstance(self.frame.index, ABCMultiIndex):
  332. self._write_hierarchical_rows(fmt_values, indent + self.indent_delta)
  333. else:
  334. self._write_regular_rows(fmt_values, indent + self.indent_delta)
  335. self.write("</tbody>", indent)
  336. def _write_regular_rows(
  337. self, fmt_values: Mapping[int, List[str]], indent: int
  338. ) -> None:
  339. truncate_h = self.fmt.truncate_h
  340. truncate_v = self.fmt.truncate_v
  341. nrows = len(self.fmt.tr_frame)
  342. if self.fmt.index:
  343. fmt = self.fmt._get_formatter("__index__")
  344. if fmt is not None:
  345. index_values = self.fmt.tr_frame.index.map(fmt)
  346. else:
  347. index_values = self.fmt.tr_frame.index.format()
  348. row: List[str] = []
  349. for i in range(nrows):
  350. if truncate_v and i == (self.fmt.tr_row_num):
  351. str_sep_row = ["..."] * len(row)
  352. self.write_tr(
  353. str_sep_row,
  354. indent,
  355. self.indent_delta,
  356. tags=None,
  357. nindex_levels=self.row_levels,
  358. )
  359. row = []
  360. if self.fmt.index:
  361. row.append(index_values[i])
  362. # see gh-22579
  363. # Column misalignment also occurs for
  364. # a standard index when the columns index is named.
  365. # Add blank cell before data cells.
  366. elif self.show_col_idx_names:
  367. row.append("")
  368. row.extend(fmt_values[j][i] for j in range(self.ncols))
  369. if truncate_h:
  370. dot_col_ix = self.fmt.tr_col_num + self.row_levels
  371. row.insert(dot_col_ix, "...")
  372. self.write_tr(
  373. row, indent, self.indent_delta, tags=None, nindex_levels=self.row_levels
  374. )
  375. def _write_hierarchical_rows(
  376. self, fmt_values: Mapping[int, List[str]], indent: int
  377. ) -> None:
  378. template = 'rowspan="{span}" valign="top"'
  379. truncate_h = self.fmt.truncate_h
  380. truncate_v = self.fmt.truncate_v
  381. frame = self.fmt.tr_frame
  382. nrows = len(frame)
  383. idx_values = frame.index.format(sparsify=False, adjoin=False, names=False)
  384. idx_values = list(zip(*idx_values))
  385. if self.fmt.sparsify:
  386. # GH3547
  387. sentinel = lib.no_default
  388. levels = frame.index.format(sparsify=sentinel, adjoin=False, names=False)
  389. level_lengths = get_level_lengths(levels, sentinel)
  390. inner_lvl = len(level_lengths) - 1
  391. if truncate_v:
  392. # Insert ... row and adjust idx_values and
  393. # level_lengths to take this into account.
  394. ins_row = self.fmt.tr_row_num
  395. # cast here since if truncate_v is True, self.fmt.tr_row_num is not None
  396. ins_row = cast(int, ins_row)
  397. inserted = False
  398. for lnum, records in enumerate(level_lengths):
  399. rec_new = {}
  400. for tag, span in list(records.items()):
  401. if tag >= ins_row:
  402. rec_new[tag + 1] = span
  403. elif tag + span > ins_row:
  404. rec_new[tag] = span + 1
  405. # GH 14882 - Make sure insertion done once
  406. if not inserted:
  407. dot_row = list(idx_values[ins_row - 1])
  408. dot_row[-1] = "..."
  409. idx_values.insert(ins_row, tuple(dot_row))
  410. inserted = True
  411. else:
  412. dot_row = list(idx_values[ins_row])
  413. dot_row[inner_lvl - lnum] = "..."
  414. idx_values[ins_row] = tuple(dot_row)
  415. else:
  416. rec_new[tag] = span
  417. # If ins_row lies between tags, all cols idx cols
  418. # receive ...
  419. if tag + span == ins_row:
  420. rec_new[ins_row] = 1
  421. if lnum == 0:
  422. idx_values.insert(
  423. ins_row, tuple(["..."] * len(level_lengths))
  424. )
  425. # GH 14882 - Place ... in correct level
  426. elif inserted:
  427. dot_row = list(idx_values[ins_row])
  428. dot_row[inner_lvl - lnum] = "..."
  429. idx_values[ins_row] = tuple(dot_row)
  430. level_lengths[lnum] = rec_new
  431. level_lengths[inner_lvl][ins_row] = 1
  432. for ix_col in range(len(fmt_values)):
  433. fmt_values[ix_col].insert(ins_row, "...")
  434. nrows += 1
  435. for i in range(nrows):
  436. row = []
  437. tags = {}
  438. sparse_offset = 0
  439. j = 0
  440. for records, v in zip(level_lengths, idx_values[i]):
  441. if i in records:
  442. if records[i] > 1:
  443. tags[j] = template.format(span=records[i])
  444. else:
  445. sparse_offset += 1
  446. continue
  447. j += 1
  448. row.append(v)
  449. row.extend(fmt_values[j][i] for j in range(self.ncols))
  450. if truncate_h:
  451. row.insert(
  452. self.row_levels - sparse_offset + self.fmt.tr_col_num, "..."
  453. )
  454. self.write_tr(
  455. row,
  456. indent,
  457. self.indent_delta,
  458. tags=tags,
  459. nindex_levels=len(levels) - sparse_offset,
  460. )
  461. else:
  462. row = []
  463. for i in range(len(frame)):
  464. if truncate_v and i == (self.fmt.tr_row_num):
  465. str_sep_row = ["..."] * len(row)
  466. self.write_tr(
  467. str_sep_row,
  468. indent,
  469. self.indent_delta,
  470. tags=None,
  471. nindex_levels=self.row_levels,
  472. )
  473. idx_values = list(
  474. zip(*frame.index.format(sparsify=False, adjoin=False, names=False))
  475. )
  476. row = []
  477. row.extend(idx_values[i])
  478. row.extend(fmt_values[j][i] for j in range(self.ncols))
  479. if truncate_h:
  480. row.insert(self.row_levels + self.fmt.tr_col_num, "...")
  481. self.write_tr(
  482. row,
  483. indent,
  484. self.indent_delta,
  485. tags=None,
  486. nindex_levels=frame.index.nlevels,
  487. )
  488. class NotebookFormatter(HTMLFormatter):
  489. """
  490. Internal class for formatting output data in html for display in Jupyter
  491. Notebooks. This class is intended for functionality specific to
  492. DataFrame._repr_html_() and DataFrame.to_html(notebook=True)
  493. """
  494. def _get_formatted_values(self) -> Dict[int, List[str]]:
  495. return {i: self.fmt._format_col(i) for i in range(self.ncols)}
  496. def _get_columns_formatted_values(self) -> List[str]:
  497. return self.columns.format()
  498. def write_style(self) -> None:
  499. # We use the "scoped" attribute here so that the desired
  500. # style properties for the data frame are not then applied
  501. # throughout the entire notebook.
  502. template_first = """\
  503. <style scoped>"""
  504. template_last = """\
  505. </style>"""
  506. template_select = """\
  507. .dataframe %s {
  508. %s: %s;
  509. }"""
  510. element_props = [
  511. ("tbody tr th:only-of-type", "vertical-align", "middle"),
  512. ("tbody tr th", "vertical-align", "top"),
  513. ]
  514. if isinstance(self.columns, ABCMultiIndex):
  515. element_props.append(("thead tr th", "text-align", "left"))
  516. if self.show_row_idx_names:
  517. element_props.append(
  518. ("thead tr:last-of-type th", "text-align", "right")
  519. )
  520. else:
  521. element_props.append(("thead th", "text-align", "right"))
  522. template_mid = "\n\n".join(map(lambda t: template_select % t, element_props))
  523. template = dedent("\n".join((template_first, template_mid, template_last)))
  524. self.write(template)
  525. def render(self) -> List[str]:
  526. self.write("<div>")
  527. self.write_style()
  528. super().render()
  529. self.write("</div>")
  530. return self.elements