html.py 21 KB

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