Skip to content

Commit

Permalink
ENH: Styler.to_latex(): conditional styling with native latex format (p…
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 authored and JulianWgs committed Jul 3, 2021
1 parent 4abc9c4 commit c5e62b5
Show file tree
Hide file tree
Showing 9 changed files with 1,039 additions and 2 deletions.
Binary file added doc/source/_static/style/latex_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/latex_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions doc/source/reference/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Styler properties

Styler.env
Styler.template_html
Styler.template_latex
Styler.loader

Style application
Expand Down Expand Up @@ -66,3 +67,4 @@ Style export and import
Styler.export
Styler.use
Styler.to_excel
Styler.to_latex
3 changes: 3 additions & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ properly format HTML and eliminate some inconsistencies (:issue:`39942` :issue:`
:class:`.Styler` has also been compatible with non-unique index or columns, at least for as many features as are fully compatible, others made only partially compatible (:issue:`41269`).
One also has greater control of the display through separate sparsification of the index or columns, using the new 'styler' options context (:issue:`41142`).

We have added an extension to allow LaTeX styling as an alternative to CSS styling and a method :meth:`.Styler.to_latex`
which renders the necessary LaTeX format including built-up styles.

Documentation has also seen major revisions in light of new features (:issue:`39720` :issue:`39317` :issue:`40493`)

.. _whatsnew_130.dataframe_honors_copy_with_dict:
Expand Down
336 changes: 336 additions & 0 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from pandas._typing import (
Axis,
FilePathOrBuffer,
FrameOrSeries,
FrameOrSeriesUnion,
IndexLabel,
Expand All @@ -30,6 +31,7 @@
from pandas.util._decorators import doc

import pandas as pd
from pandas import RangeIndex
from pandas.api.types import is_list_like
from pandas.core import generic
import pandas.core.common as com
Expand All @@ -39,6 +41,8 @@
)
from pandas.core.generic import NDFrame

from pandas.io.formats.format import save_to_buffer

jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")

from pandas.io.formats.style_render import (
Expand Down Expand Up @@ -403,6 +407,338 @@ def to_excel(
engine=engine,
)

def to_latex(
self,
buf: FilePathOrBuffer[str] | None = None,
*,
column_format: str | None = None,
position: str | None = None,
position_float: str | None = None,
hrules: bool = False,
label: str | None = None,
caption: str | None = None,
sparse_index: bool | None = None,
sparse_columns: bool | None = None,
multirow_align: str = "c",
multicol_align: str = "r",
siunitx: bool = False,
encoding: str | None = None,
):
r"""
Write Styler to a file, buffer or string in LaTeX format.
.. versionadded:: 1.3.0
Parameters
----------
buf : str, Path, or StringIO-like, optional, default None
Buffer to write to. If ``None``, the output is returned as a string.
column_format : str, optional
The LaTeX column specification placed in location:
\\begin{tabular}{<column_format>}
Defaults to 'l' for index and
non-numeric data columns, and, for numeric data columns,
to 'r' by default, or 'S' if ``siunitx`` is ``True``.
position : str, optional
The LaTeX positional argument (e.g. 'h!') for tables, placed in location:
\\begin{table}[<position>]
position_float : {"centering", "raggedleft", "raggedright"}, optional
The LaTeX float command placed in location:
\\begin{table}[<position>]
\\<position_float>
hrules : bool, default False
Set to `True` to add \\toprule, \\midrule and \\bottomrule from the
{booktabs} LaTeX package.
label : str, optional
The LaTeX label included as: \\label{<label>}.
This is used with \\ref{<label>} in the main .tex file.
caption : str, optional
The LaTeX table caption included as: \\caption{<caption>}.
sparse_index : bool, optional
Whether to sparsify the display of a hierarchical index. Setting to False
will display each explicit level element in a hierarchical key for each row.
Defaults to ``pandas.options.styler.sparse.index`` value.
sparse_columns : bool, optional
Whether to sparsify the display of a hierarchical index. Setting to False
will display each explicit level element in a hierarchical key for each row.
Defaults to ``pandas.options.styler.sparse.columns`` value.
multirow_align : {"c", "t", "b"}
If sparsifying hierarchical MultiIndexes whether to align text centrally,
at the top or bottom.
multicol_align : {"r", "c", "l"}
If sparsifying hierarchical MultiIndex columns whether to align text at
the left, centrally, or at the right.
siunitx : bool, default False
Set to ``True`` to structure LaTeX compatible with the {siunitx} package.
encoding : str, default "utf-8"
Character encoding setting.
Returns
-------
str or None
If `buf` is None, returns the result as a string. Otherwise returns `None`.
See Also
--------
Styler.format: Format the text display value of cells.
Notes
-----
**Latex Packages**
For the following features we recommend the following LaTeX inclusions:
===================== ==========================================================
Feature Inclusion
===================== ==========================================================
sparse columns none: included within default {tabular} environment
sparse rows \\usepackage{multirow}
hrules \\usepackage{booktabs}
colors \\usepackage[table]{xcolor}
siunitx \\usepackage{siunitx}
bold (with siunitx) | \\usepackage{etoolbox}
| \\robustify\\bfseries
| \\sisetup{detect-all = true} *(within {document})*
italic (with siunitx) | \\usepackage{etoolbox}
| \\robustify\\itshape
| \\sisetup{detect-all = true} *(within {document})*
===================== ==========================================================
**Cell Styles**
LaTeX styling can only be rendered if the accompanying styling functions have
been constructed with appropriate LaTeX commands. All styling
functionality is built around the concept of a CSS ``(<attribute>, <value>)``
pair (see `Table Visualization <../../user_guide/style.ipynb>`_), and this
should be replaced by a LaTeX
``(<command>, <options>)`` approach. Each cell will be styled individually
using nested LaTeX commands with their accompanied options.
For example the following code will highlight and bold a cell in HTML-CSS:
>>> df = pd.DataFrame([[1,2], [3,4]])
>>> s = df.style.highlight_max(axis=None,
... props='background-color:red; font-weight:bold;')
>>> s.render()
The equivalent using LaTeX only commands is the following:
>>> s = df.style.highlight_max(axis=None,
... props='cellcolor:{red}; bfseries: ;')
>>> s.to_latex()
Internally these structured LaTeX ``(<command>, <options>)`` pairs
are translated to the
``display_value`` with the default structure:
``\<command><options> <display_value>``.
Where there are multiple commands the latter is nested recursively, so that
the above example highlighed cell is rendered as
``\cellcolor{red} \bfseries 4``.
Occasionally this format does not suit the applied command, or
combination of LaTeX packages that is in use, so additional flags can be
added to the ``<options>``, within the tuple, to result in different
positions of required braces (the **default** being the same as ``--nowrap``):
=================================== ============================================
Tuple Format Output Structure
=================================== ============================================
(<command>,<options>) \\<command><options> <display_value>
(<command>,<options> ``--nowrap``) \\<command><options> <display_value>
(<command>,<options> ``--rwrap``) \\<command><options>{<display_value>}
(<command>,<options> ``--wrap``) {\\<command><options> <display_value>}
(<command>,<options> ``--lwrap``) {\\<command><options>} <display_value>
(<command>,<options> ``--dwrap``) {\\<command><options>}{<display_value>}
=================================== ============================================
For example the `textbf` command for font-weight
should always be used with `--rwrap` so ``('textbf', '--rwrap')`` will render a
working cell, wrapped with braces, as ``\textbf{<display_value>}``.
A more comprehensive example is as follows:
>>> df = pd.DataFrame([[1, 2.2, "dogs"], [3, 4.4, "cats"], [2, 6.6, "cows"]],
... index=["ix1", "ix2", "ix3"],
... columns=["Integers", "Floats", "Strings"])
>>> s = df.style.highlight_max(
... props='cellcolor:[HTML]{FFFF00}; color:{red};'
... 'textit:--rwrap; textbf:--rwrap;'
... )
>>> s.to_latex()
.. figure:: ../../_static/style/latex_1.png
**Table Styles**
Internally Styler uses its ``table_styles`` object to parse the
``column_format``, ``position``, ``position_float``, and ``label``
input arguments. These arguments are added to table styles in the format:
.. code-block:: python
set_table_styles([
{"selector": "column_format", "props": f":{column_format};"},
{"selector": "position", "props": f":{position};"},
{"selector": "position_float", "props": f":{position_float};"},
{"selector": "label", "props": f":{{{label.replace(':','§')}}};"}
], overwrite=False)
Exception is made for the ``hrules`` argument which, in fact, controls all three
commands: ``toprule``, ``bottomrule`` and ``midrule`` simultaneously. Instead of
setting ``hrules`` to ``True``, it is also possible to set each
individual rule definition, by manually setting the ``table_styles``,
for example below we set a regular ``toprule``, set an ``hline`` for
``bottomrule`` and exclude the ``midrule``:
.. code-block:: python
set_table_styles([
{'selector': 'toprule', 'props': ':toprule;'},
{'selector': 'bottomrule', 'props': ':hline;'},
], overwrite=False)
If other ``commands`` are added to table styles they will be detected, and
positioned immediately above the '\\begin{tabular}' command. For example to
add odd and even row coloring, from the {colortbl} package, in format
``\rowcolors{1}{pink}{red}``, use:
.. code-block:: python
set_table_styles([
{'selector': 'rowcolors', 'props': ':{1}{pink}{red};'}
], overwrite=False)
A more comprehensive example using these arguments is as follows:
>>> df.columns = pd.MultiIndex.from_tuples([
... ("Numeric", "Integers"),
... ("Numeric", "Floats"),
... ("Non-Numeric", "Strings")
... ])
>>> df.index = pd.MultiIndex.from_tuples([
... ("L0", "ix1"), ("L0", "ix2"), ("L1", "ix3")
... ])
>>> s = df.style.highlight_max(
... props='cellcolor:[HTML]{FFFF00}; color:{red}; itshape:; bfseries:;'
... )
>>> s.to_latex(
... column_format="rrrrr", position="h", position_float="centering",
... hrules=True, label="table:5", caption="Styled LaTeX Table",
... multirow_align="t", multicol_align="r"
... )
.. figure:: ../../_static/style/latex_2.png
**Formatting**
To format values :meth:`Styler.format` should be used prior to calling
`Styler.to_latex`, as well as other methods such as :meth:`Styler.hide_index`
or :meth:`Styler.hide_columns`, for example:
>>> s.clear()
>>> s.table_styles = []
>>> s.caption = None
>>> s.format({
... ("Numeric", "Integers"): '\${}',
... ("Numeric", "Floats"): '{:.3f}',
... ("Non-Numeric", "Strings"): str.upper
... })
>>> s.to_latex()
\begin{tabular}{llrrl}
{} & {} & \multicolumn{2}{r}{Numeric} & {Non-Numeric} \\
{} & {} & {Integers} & {Floats} & {Strings} \\
\multirow[c]{2}{*}{L0} & ix1 & \\$1 & 2.200 & DOGS \\
& ix2 & \$3 & 4.400 & CATS \\
L1 & ix3 & \$2 & 6.600 & COWS \\
\end{tabular}
"""
table_selectors = (
[style["selector"] for style in self.table_styles]
if self.table_styles is not None
else []
)

if column_format is not None:
# add more recent setting to table_styles
self.set_table_styles(
[{"selector": "column_format", "props": f":{column_format}"}],
overwrite=False,
)
elif "column_format" in table_selectors:
pass # adopt what has been previously set in table_styles
else:
# create a default: set float, complex, int cols to 'r' ('S'), index to 'l'
_original_columns = self.data.columns
self.data.columns = RangeIndex(stop=len(self.data.columns))
numeric_cols = self.data._get_numeric_data().columns.to_list()
self.data.columns = _original_columns
column_format = "" if self.hidden_index else "l" * self.data.index.nlevels
for ci, _ in enumerate(self.data.columns):
if ci not in self.hidden_columns:
column_format += (
("r" if not siunitx else "S") if ci in numeric_cols else "l"
)
self.set_table_styles(
[{"selector": "column_format", "props": f":{column_format}"}],
overwrite=False,
)

if position:
self.set_table_styles(
[{"selector": "position", "props": f":{position}"}],
overwrite=False,
)

if position_float:
if position_float not in ["raggedright", "raggedleft", "centering"]:
raise ValueError(
f"`position_float` should be one of "
f"'raggedright', 'raggedleft', 'centering', "
f"got: '{position_float}'"
)
self.set_table_styles(
[{"selector": "position_float", "props": f":{position_float}"}],
overwrite=False,
)

if hrules:
self.set_table_styles(
[
{"selector": "toprule", "props": ":toprule"},
{"selector": "midrule", "props": ":midrule"},
{"selector": "bottomrule", "props": ":bottomrule"},
],
overwrite=False,
)

if label:
self.set_table_styles(
[{"selector": "label", "props": f":{{{label.replace(':', '§')}}}"}],
overwrite=False,
)

if caption:
self.set_caption(caption)

if sparse_index is None:
sparse_index = get_option("styler.sparse.index")
if sparse_columns is None:
sparse_columns = get_option("styler.sparse.columns")

latex = self._render_latex(
sparse_index=sparse_index,
sparse_columns=sparse_columns,
multirow_align=multirow_align,
multicol_align=multicol_align,
)

return save_to_buffer(latex, buf=buf, encoding=encoding)

def set_td_classes(self, classes: DataFrame) -> Styler:
"""
Set the DataFrame of strings added to the ``class`` attribute of ``<td>``
Expand Down
Loading

0 comments on commit c5e62b5

Please sign in to comment.