From bb501d3a643cd293b0b644874198a95ffb7c5250 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 10 Mar 2021 17:55:24 +0100 Subject: [PATCH 01/95] MVP for Styler.to_latex --- pandas/io/formats/style.py | 7 ++++++- pandas/io/formats/templates/latex.tpl | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 pandas/io/formats/templates/latex.tpl diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ec09a4cc4cd89..1a4293c8d8640 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -157,6 +157,7 @@ class Styler: loader = jinja2.PackageLoader("pandas", "io/formats/templates") env = jinja2.Environment(loader=loader, trim_blocks=True) template = env.get_template("html.tpl") + template2 = env.get_template("latex.tpl") def __init__( self, @@ -732,12 +733,14 @@ def set_td_classes(self, classes: DataFrame) -> Styler: return self - def render(self, **kwargs) -> str: + def render(self, latex: bool = False, **kwargs) -> str: """ Render the built up styles to HTML. Parameters ---------- + latex : bool + Output in latex format rather than HTML. **kwargs Any additional keyword arguments are passed through to ``self.template.render``. @@ -773,6 +776,8 @@ def render(self, **kwargs) -> str: # TODO: namespace all the pandas keys d = self._translate() d.update(kwargs) + if latex: + return self.template2.render(**d) return self.template.render(**d) def _update_ctx(self, attrs: DataFrame) -> None: diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl new file mode 100644 index 0000000000000..a1634bcc94a8e --- /dev/null +++ b/pandas/io/formats/templates/latex.tpl @@ -0,0 +1,11 @@ +\begin{table} +\centering +\begin{tabular}{% raw %}{{% endraw %}{% for item in head[0] %}l{% endfor %}{% raw %}}{% endraw %} + +{% for item in head[0] %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ +\hline +{% for row in body %} +{% for item in row %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ +{% endfor %} +\end{tabular} +\end{table} From 606644944d553e9fb1967e512c34ff1d9bf2666d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 10 Mar 2021 21:38:41 +0100 Subject: [PATCH 02/95] MVP for Styler.to_latex --- pandas/io/formats/style.py | 11 +++++++++++ pandas/io/formats/templates/latex.tpl | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1a4293c8d8640..54a918b4cafc0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -777,6 +777,7 @@ def render(self, latex: bool = False, **kwargs) -> str: d = self._translate() d.update(kwargs) if latex: + self.template2.globals["parse"] = _parse_latex_table_styles return self.template2.render(**d) return self.template.render(**d) @@ -2249,3 +2250,13 @@ def pred(part) -> bool: else: slice_ = [part if pred(part) else [part] for part in slice_] return tuple(slice_) + + +def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str]: + for style in styles[::-1]: # in reverse for most recently applied style + if style["selector"] == selector: + if style["props"][0][0]: + return f"{style['props'][0][0]}:{style['props'][0][1]}" + else: + return style["props"][0][1] + return None diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index a1634bcc94a8e..7d12304ff1283 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -1,11 +1,24 @@ -\begin{table} -\centering +\begin{table}{% if parse(table_styles, 'position') is not none %}[{{parse(table_styles, 'position')}}]{% endif %} +{%- if parse(table_styles, 'float') is not none%} + +\{{parse(table_styles, 'float')}} +{%- endif %} + \begin{tabular}{% raw %}{{% endraw %}{% for item in head[0] %}l{% endfor %}{% raw %}}{% endraw %} -{% for item in head[0] %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ +{% for item in head[0] %}{%- if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ \hline {% for row in body %} {% for item in row %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ {% endfor %} \end{tabular} +{%- if caption %} + +\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} +{%- endif %} +{%- if parse(table_styles, 'label') is not none %} + +\label{% raw %}{{% endraw %}{{parse(table_styles, 'label')}}{% raw %}}{% endraw %} +{%- endif %} + \end{table} From 6e2b7880e3cfc1c96389cda557dd0f066a15705a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 08:30:35 +0100 Subject: [PATCH 03/95] rules --- pandas/io/formats/style.py | 13 ++++++++ pandas/io/formats/templates/latex.tpl | 44 ++++++++++++++++++--------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 54a918b4cafc0..1f7a04a6ff3a3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -299,6 +299,19 @@ def set_tooltips( return self + def to_latex(self, rules=False): + if rules: + self.set_table_styles( + [ + {"selector": "toprule", "props": ":toprule"}, + {"selector": "midrule", "props": ":midrule"}, + {"selector": "bottomrule", "props": ":bottomrule"}, + ], + overwrite=False, + ) + + return self.render(latex=True) + @doc( NDFrame.to_excel, klass="Styler", diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index 7d12304ff1283..ed8930f12446f 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -1,24 +1,38 @@ -\begin{table}{% if parse(table_styles, 'position') is not none %}[{{parse(table_styles, 'position')}}]{% endif %} -{%- if parse(table_styles, 'float') is not none%} +\begin{table} +{%- set position = parse(table_styles, 'position') %} +{%- if position is not none %} +[{{position}}] +{% endif %} +{% set float = parse(table_styles, 'float') %} +{% if float is not none%} +\{{float}} +{% endif %} +{% if caption %} +\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} -\{{parse(table_styles, 'float')}} -{%- endif %} +{% endif %} +{% set label = parse(table_styles, 'label') %} +{% if label is not none %} +\label{% raw %}{{% endraw %}{{label}}{% raw %}}{% endraw %} -\begin{tabular}{% raw %}{{% endraw %}{% for item in head[0] %}l{% endfor %}{% raw %}}{% endraw %} +{% endif %} +\begin{tabular}{% raw %}{{% endraw %}{% for c in head[0] %}{% if c.is_visible != False %}l{% endif %}{% endfor %}{% raw %}}{% endraw %} +{% set toprule = parse(table_styles, 'toprule') %} +{% if toprule is not none %} +\{{toprule}} +{% endif %} {% for item in head[0] %}{%- if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ -\hline +{% set midrule = parse(table_styles, 'midrule') %} +{% if midrule is not none %} +\{{midrule}} +{% endif %} {% for row in body %} {% for item in row %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ {% endfor %} +{% set bottomrule = parse(table_styles, 'bottomrule') %} +{% if bottomrule is not none %} +\{{bottomrule}} +{% endif %} \end{tabular} -{%- if caption %} - -\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} -{%- endif %} -{%- if parse(table_styles, 'label') is not none %} - -\label{% raw %}{{% endraw %}{{parse(table_styles, 'label')}}{% raw %}}{% endraw %} -{%- endif %} - \end{table} From 2f7469057229f3105526c022e08cbb573b267e14 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 09:08:24 +0100 Subject: [PATCH 04/95] hidden columns and index --- pandas/io/formats/style.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1f7a04a6ff3a3..c3282c10b28d7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -359,7 +359,7 @@ def to_excel( engine=engine, ) - def _translate(self): + def _translate(self, latex: bool = False): """ Convert the DataFrame in `self.data` and the attrs from `_build_styles` into a dictionary of {head, body, uuid, cellstyle}. @@ -553,6 +553,11 @@ def _translate(self): if self.tooltips: d = self.tooltips._translate(self.data, self.uuid, d) + if latex: + # post processing of d for latex template + d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] + d["body"] = [[col for col in row if col["is_visible"]] for row in d["body"]] + return d def format( @@ -787,7 +792,7 @@ def render(self, latex: bool = False, **kwargs) -> str: """ self._compute() # TODO: namespace all the pandas keys - d = self._translate() + d = self._translate(latex=latex) d.update(kwargs) if latex: self.template2.globals["parse"] = _parse_latex_table_styles From 9c9405ef4d548874b1772245a4e86c93291658f2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 11:06:11 +0100 Subject: [PATCH 05/95] basic cell colors and background colors parsing. --- pandas/io/formats/style.py | 37 ++++++++++++++++++++++++--- pandas/io/formats/templates/latex.tpl | 14 +++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index c3282c10b28d7..e946bfd5d1d4e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -554,9 +554,20 @@ def _translate(self, latex: bool = False): d = self.tooltips._translate(self.data, self.uuid, d) if latex: - # post processing of d for latex template + # post processing of d for latex template: + # - remove hidden columns + # - place cellstyles in individual cell dicts that are not index cells d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] - d["body"] = [[col for col in row if col["is_visible"]] for row in d["body"]] + d["body"] = [ + [ + {**col, "cellstyle": None} + if col["type"] == "th" + else {**col, "cellstyle": ctx[r, c - n_rlvls]} + for c, col in enumerate(row) + if col["is_visible"] + ] + for r, row in enumerate(d["body"]) + ] return d @@ -795,7 +806,8 @@ def render(self, latex: bool = False, **kwargs) -> str: d = self._translate(latex=latex) d.update(kwargs) if latex: - self.template2.globals["parse"] = _parse_latex_table_styles + self.template2.globals["parse_table"] = _parse_latex_table_styles + self.template2.globals["parse_cell"] = _parse_latex_cell_styles return self.template2.render(**d) return self.template.render(**d) @@ -2278,3 +2290,22 @@ def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str] else: return style["props"][0][1] return None + + +LATEX_SUPPORTED_CELLSTYLE_ATTRS = { + "color": lambda color, disp: f"\\textcolor{color}{{{disp}}}", + "background-color": lambda color, disp: fr"\cellcolor{color} {disp}", +} + + +def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: + if styles is None: + return display_value + else: + ret = display_value + for attr, func in LATEX_SUPPORTED_CELLSTYLE_ATTRS.items(): + for style in styles[::-1]: # in reverse for most recently applied style + if style[0] == attr: + ret = func(style[1], ret) + break + return ret diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index ed8930f12446f..26b5b84091910 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -1,9 +1,9 @@ \begin{table} -{%- set position = parse(table_styles, 'position') %} +{%- set position = parse_table(table_styles, 'position') %} {%- if position is not none %} [{{position}}] {% endif %} -{% set float = parse(table_styles, 'float') %} +{% set float = parse_table(table_styles, 'float') %} {% if float is not none%} \{{float}} {% endif %} @@ -11,26 +11,26 @@ \caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} {% endif %} -{% set label = parse(table_styles, 'label') %} +{% set label = parse_table(table_styles, 'label') %} {% if label is not none %} \label{% raw %}{{% endraw %}{{label}}{% raw %}}{% endraw %} {% endif %} \begin{tabular}{% raw %}{{% endraw %}{% for c in head[0] %}{% if c.is_visible != False %}l{% endif %}{% endfor %}{% raw %}}{% endraw %} -{% set toprule = parse(table_styles, 'toprule') %} +{% set toprule = parse_table(table_styles, 'toprule') %} {% if toprule is not none %} \{{toprule}} {% endif %} {% for item in head[0] %}{%- if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ -{% set midrule = parse(table_styles, 'midrule') %} +{% set midrule = parse_table(table_styles, 'midrule') %} {% if midrule is not none %} \{{midrule}} {% endif %} {% for row in body %} -{% for item in row %}{% if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ +{% for c in row %}{% if not loop.first %} & {% endif %}{{parse_cell(c.cellstyle, c.display_value)}}{% endfor %} \\ {% endfor %} -{% set bottomrule = parse(table_styles, 'bottomrule') %} +{% set bottomrule = parse_table(table_styles, 'bottomrule') %} {% if bottomrule is not none %} \{{bottomrule}} {% endif %} From a6772e77c946790b8f555cab3f92d420881e3e96 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 11:11:59 +0100 Subject: [PATCH 06/95] basic cell colors and background colors parsing. --- pandas/io/formats/style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e946bfd5d1d4e..f8a5fa0d6e739 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2293,8 +2293,8 @@ def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str] LATEX_SUPPORTED_CELLSTYLE_ATTRS = { - "color": lambda color, disp: f"\\textcolor{color}{{{disp}}}", - "background-color": lambda color, disp: fr"\cellcolor{color} {disp}", + "textcolor": lambda color, disp: f"\\textcolor{color}{{{disp}}}", + "cellcolor": lambda color, disp: f"\\cellcolor{color} {disp}", } @@ -2307,5 +2307,5 @@ def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: for style in styles[::-1]: # in reverse for most recently applied style if style[0] == attr: ret = func(style[1], ret) - break + break return ret From 6505564ff88b94c1d301a1512e1bb3dcf3f07c17 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 12:07:24 +0100 Subject: [PATCH 07/95] basic cell colors and background colors parsing. --- pandas/io/formats/style.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f8a5fa0d6e739..a75dc7f513ad2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -560,7 +560,7 @@ def _translate(self, latex: bool = False): d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] d["body"] = [ [ - {**col, "cellstyle": None} + {**col, "cellstyle": []} if col["type"] == "th" else {**col, "cellstyle": ctx[r, c - n_rlvls]} for c, col in enumerate(row) @@ -2292,20 +2292,7 @@ def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str] return None -LATEX_SUPPORTED_CELLSTYLE_ATTRS = { - "textcolor": lambda color, disp: f"\\textcolor{color}{{{disp}}}", - "cellcolor": lambda color, disp: f"\\cellcolor{color} {disp}", -} - - def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: - if styles is None: - return display_value - else: - ret = display_value - for attr, func in LATEX_SUPPORTED_CELLSTYLE_ATTRS.items(): - for style in styles[::-1]: # in reverse for most recently applied style - if style[0] == attr: - ret = func(style[1], ret) - break - return ret + for style in styles[::-1]: # in reverse for most recently applied style + display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" + return display_value From b25644fa7d0a6798b4f8b310f7d5715b2265e2e4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 12:33:05 +0100 Subject: [PATCH 08/95] column_format arg input --- pandas/io/formats/style.py | 34 +++++++++++++++++++++++++-- pandas/io/formats/templates/latex.tpl | 9 ++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a75dc7f513ad2..3d24fb66d133d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -299,8 +299,27 @@ def set_tooltips( return self - def to_latex(self, rules=False): - if rules: + def to_latex( + self, + column_format: Optional[str] = None, + position: Optional[str] = None, + hrules: bool = False, + label: Optional[str] = None, + caption: Optional[str] = None, + ): + if column_format: + 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 hrules: self.set_table_styles( [ {"selector": "toprule", "props": ":toprule"}, @@ -310,6 +329,17 @@ def to_latex(self, rules=False): overwrite=False, ) + if label: + label = label.split(":") + label = f":{label[0]}" if len(label) == 1 else f"{label[0]}:{label[1]}" + self.set_table_styles( + [{"selector": "label", "props": f"{label}"}], + overwrite=False, + ) + + if caption: + self.set_caption(caption) + return self.render(latex=True) @doc( diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index 26b5b84091910..ca880f3350e38 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -16,8 +16,15 @@ \label{% raw %}{{% endraw %}{{label}}{% raw %}}{% endraw %} {% endif %} -\begin{tabular}{% raw %}{{% endraw %}{% for c in head[0] %}{% if c.is_visible != False %}l{% endif %}{% endfor %}{% raw %}}{% endraw %} +\begin{tabular} +{%- set column_format = parse_table(table_styles, 'column_format') %} +{%- if column_format is not none %} +{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %} +{% else %} +{% raw %}{{% endraw %}{% for c in head[0] %}{% if c.is_visible != False %}l{% endif %}{% endfor %}{% raw %}}{% endraw %} + +{% endif %} {% set toprule = parse_table(table_styles, 'toprule') %} {% if toprule is not none %} \{{toprule}} From 2ba7e021f1a95e03d3968018bb6b106db8c40954 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 13:01:12 +0100 Subject: [PATCH 09/95] add file buffer --- pandas/io/formats/style.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3d24fb66d133d..7e3aad2d49a01 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -30,6 +30,7 @@ from pandas._libs import lib from pandas._typing import ( Axis, + FilePathOrBuffer, FrameOrSeries, FrameOrSeriesUnion, IndexLabel, @@ -47,6 +48,8 @@ from pandas.core.generic import NDFrame from pandas.core.indexes.api import Index +from pandas.io.formats.format import save_to_buffer + jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") BaseFormatter = Union[str, Callable] @@ -301,11 +304,13 @@ def set_tooltips( def to_latex( self, + buf: Optional[FilePathOrBuffer[str]] = None, column_format: Optional[str] = None, position: Optional[str] = None, hrules: bool = False, label: Optional[str] = None, caption: Optional[str] = None, + encoding: Optional[str] = None, ): if column_format: self.set_table_styles( @@ -340,7 +345,8 @@ def to_latex( if caption: self.set_caption(caption) - return self.render(latex=True) + latex = self.render(latex=True) + return save_to_buffer(latex, buf=buf, encoding=encoding) @doc( NDFrame.to_excel, From 79b978be4ede3360c65a0f0d7c7bc1a37a886b5c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 13:22:41 +0100 Subject: [PATCH 10/95] optional table wrapping --- pandas/io/formats/style.py | 8 +++++++- pandas/io/formats/templates/latex.tpl | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 7e3aad2d49a01..2ca23eb263f4b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -345,7 +345,13 @@ def to_latex( if caption: self.set_caption(caption) - latex = self.render(latex=True) + wrappers = ["position", "label", "caption"] + if not any(d["selector"] in wrappers for d in self.table_styles): + table_wrapping = False + else: + table_wrapping = True + + latex = self.render(latex=True, table_wrapping=table_wrapping) return save_to_buffer(latex, buf=buf, encoding=encoding) @doc( diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index ca880f3350e38..4b638eff971f9 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -1,3 +1,4 @@ +{% if table_wrapping %} \begin{table} {%- set position = parse_table(table_styles, 'position') %} {%- if position is not none %} @@ -15,6 +16,7 @@ {% if label is not none %} \label{% raw %}{{% endraw %}{{label}}{% raw %}}{% endraw %} +{% endif %} {% endif %} \begin{tabular} {%- set column_format = parse_table(table_styles, 'column_format') %} @@ -42,4 +44,6 @@ \{{bottomrule}} {% endif %} \end{tabular} +{% if table_wrapping %} \end{table} +{% endif %} From ed8b40549d0fd6afe742cf4c724c79d905c94d2c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 14:50:53 +0100 Subject: [PATCH 11/95] refactor --- pandas/io/formats/style.py | 53 +++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2ca23eb263f4b..ffba3c4177087 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -401,7 +401,7 @@ def to_excel( engine=engine, ) - def _translate(self, latex: bool = False): + def _translate(self): """ Convert the DataFrame in `self.data` and the attrs from `_build_styles` into a dictionary of {head, body, uuid, cellstyle}. @@ -595,22 +595,23 @@ def _translate(self, latex: bool = False): if self.tooltips: d = self.tooltips._translate(self.data, self.uuid, d) - if latex: - # post processing of d for latex template: - # - remove hidden columns - # - place cellstyles in individual cell dicts that are not index cells - d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] - d["body"] = [ - [ - {**col, "cellstyle": []} - if col["type"] == "th" - else {**col, "cellstyle": ctx[r, c - n_rlvls]} - for c, col in enumerate(row) - if col["is_visible"] - ] - for r, row in enumerate(d["body"]) - ] + return d + def _translate_latex(self, d: Dict) -> Dict: + # post processing of d for latex template: + # - remove hidden columns + # - place cellstyles in individual cell dicts that are not index cells + d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] + d["body"] = [ + [ + {**col, "cellstyle": []} + if col["type"] == "th" + else {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} + for c, col in enumerate(row) + if col["is_visible"] + ] + for r, row in enumerate(d["body"]) + ] return d def format( @@ -845,13 +846,16 @@ def render(self, latex: bool = False, **kwargs) -> str: """ self._compute() # TODO: namespace all the pandas keys - d = self._translate(latex=latex) - d.update(kwargs) + d = self._translate() + template = self.template if latex: - self.template2.globals["parse_table"] = _parse_latex_table_styles - self.template2.globals["parse_cell"] = _parse_latex_cell_styles - return self.template2.render(**d) - return self.template.render(**d) + d = self._translate_latex(d) + template = self.template2 + template.globals["parse_table"] = _parse_latex_table_styles + template.globals["parse_cell"] = _parse_latex_cell_styles + + d.update(kwargs) + return template.render(**d) def _update_ctx(self, attrs: DataFrame) -> None: """ @@ -2336,5 +2340,8 @@ def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str] def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: for style in styles[::-1]: # in reverse for most recently applied style - display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" + if style[1] == "-wrap-": + display_value = f"{{\\{style[0]} {display_value}}}" + else: + display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" return display_value From f7d52f4064de248065a3a240477144c8babef319 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 17:36:59 +0100 Subject: [PATCH 12/95] more flexible table_styles --- pandas/io/formats/style.py | 9 ++------- pandas/io/formats/templates/latex.tpl | 8 ++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ffba3c4177087..6cd8fdeebeeac 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -335,10 +335,8 @@ def to_latex( ) if label: - label = label.split(":") - label = f":{label[0]}" if len(label) == 1 else f"{label[0]}:{label[1]}" self.set_table_styles( - [{"selector": "label", "props": f"{label}"}], + [{"selector": "label", "props": f":{{{label.replace(':', '§')}}}"}], overwrite=False, ) @@ -2331,10 +2329,7 @@ def pred(part) -> bool: def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str]: for style in styles[::-1]: # in reverse for most recently applied style if style["selector"] == selector: - if style["props"][0][0]: - return f"{style['props'][0][0]}:{style['props'][0][1]}" - else: - return style["props"][0][1] + return style["props"][0][1].replace("§", ":") return None diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index 4b638eff971f9..5f9a36db0c46d 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -12,11 +12,11 @@ \caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} {% endif %} -{% set label = parse_table(table_styles, 'label') %} -{% if label is not none %} -\label{% raw %}{{% endraw %}{{label}}{% raw %}}{% endraw %} - +{% for style in table_styles %} +{% if style['selector'] not in ['position', 'float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format'] %} +\{{style['selector']}}{{parse_table(table_styles, style['selector'])}} {% endif %} +{% endfor %} {% endif %} \begin{tabular} {%- set column_format = parse_table(table_styles, 'column_format') %} From f35c7e11ca03b1d22485e4dcc7b8d550d9b596c5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 12 Mar 2021 16:52:18 +0100 Subject: [PATCH 13/95] docs to parse_latex --- pandas/io/formats/style.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6cd8fdeebeeac..5f4e252c22427 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2327,6 +2327,18 @@ def pred(part) -> bool: def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str]: + """ + Find the relevant first `props` `value` from a list of `(attribute,value)` tuples + within `table_styles` identified by a given selector. + + For example: table_styles =[ + {'selector': 'foo', 'props': [('attr','value')], + {'selector': 'bar', 'props': [('attr', 'overwritten')]}, + {'selector': 'bar', 'props': [('attr', 'baz'), ('attr2', 'ignored')]} + ] + + Then for selector='bar', the return value is 'baz'. + """ for style in styles[::-1]: # in reverse for most recently applied style if style["selector"] == selector: return style["props"][0][1].replace("§", ":") @@ -2334,9 +2346,30 @@ def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str] def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: + r""" + Build a recursive latex chain of commands based on CSS list values, nested around + `display_value`. + + If a CSS style is given as ('', '') this is translated to + '\{display_value}', and this value is treated as the + display value for the next iteration. + + The most recent style forms the inner component, for example: + `styles=[('emph', ''), ('cellcolor', '[rgb]{0,1,1}')]` will yield: + \emph{\cellcolor[rgb]{0,1,1}{display_value}} + + Sometimes latex commands have to be wrapped with curly braces: + Instead of `\{ text}` + In this case if the keyphrase '-wrap-' is detected in it will return + correctly, for example: + `styles=[('Huge', '-wrap-'), ('cellcolor', '[rgb]{0,1,1}')]` will yield: + {\Huge \cellcolor[rgb]{0,1,1}{display_value}} + """ for style in styles[::-1]: # in reverse for most recently applied style - if style[1] == "-wrap-": - display_value = f"{{\\{style[0]} {display_value}}}" + if "-wrap-" in style[1]: + display_value = ( + f"{{\\{style[0]}{style[1].replace('-wrap-','')} {display_value}}}" + ) else: display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" return display_value From 521aee22b3c1ac0b1d0eb6ce91ee89cc70085336 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 07:46:55 +0100 Subject: [PATCH 14/95] auto column_format for numerics --- pandas/io/formats/style.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5f4e252c22427..9c00971304643 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -312,11 +312,17 @@ def to_latex( caption: Optional[str] = None, encoding: Optional[str] = None, ): - if column_format: - self.set_table_styles( - [{"selector": "column_format", "props": f":{column_format}"}], - overwrite=False, - ) + if column_format is None: # set float, complex, int cols to 'r', index to 'l' + numeric_cols = list(self.data.select_dtypes(include=[np.number]).columns) + numeric_cols = self.columns.get_indexer_for(numeric_cols) + 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 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( @@ -344,7 +350,9 @@ def to_latex( self.set_caption(caption) wrappers = ["position", "label", "caption"] - if not any(d["selector"] in wrappers for d in self.table_styles): + if self.table_styles is not None and not any( + d["selector"] in wrappers for d in self.table_styles + ): table_wrapping = False else: table_wrapping = True From 849656f0757d454b95090b8db13fbee37c3f038e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 07:48:19 +0100 Subject: [PATCH 15/95] to_latex unit tests --- .../tests/io/formats/style/test_to_latex.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pandas/tests/io/formats/style/test_to_latex.py diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py new file mode 100644 index 0000000000000..fa5cdc1dca618 --- /dev/null +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -0,0 +1,91 @@ +from textwrap import dedent + +import pytest + +from pandas import DataFrame + +from pandas.io.formats.style import ( + _parse_latex_cell_styles, + _parse_latex_table_styles, +) + +pytest.importorskip("jinja2") + + +class TestStylerLatex: + def setup_method(self, method): + self.df = DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]}) + + def test_parse_latex_table_styles(self): + s = self.df.style.set_table_styles( + [ + {"selector": "foo", "props": [("attr", "value")]}, + {"selector": "bar", "props": [("attr", "overwritten")]}, + {"selector": "bar", "props": [("attr", "baz"), ("attr2", "ignored")]}, + {"selector": "label", "props": [("", "fig§item")]}, + ] + ) + assert _parse_latex_table_styles(s.table_styles, "bar") == "baz" + + # test '§' replaced by ':' [for CSS compatibility] + assert _parse_latex_table_styles(s.table_styles, "label") == "fig:item" + + def test_parse_latex_cell_styles_basic(self): + cell_style = [("emph", ""), ("cellcolor", "[rgb]{0,1,1}")] + expected = "\\emph{\\cellcolor[rgb]{0,1,1}{text}}" + assert _parse_latex_cell_styles(cell_style, "text") == expected + + def test_parse_latex_cell_styles_braces_wrap(self): + cell_style = [("Huge", "-wrap-"), ("cellcolor", "-wrap-[rgb]{0,1,1}")] + expected = "{\\Huge {\\cellcolor[rgb]{0,1,1} text}}" + assert _parse_latex_cell_styles(cell_style, "text") == expected + + def test_minimal_latex_tabular(self): + s = self.df.style.format(precision=2) + expected = dedent( + """\ + \\begin{tabular}{lrrl} + & A & B & C \\\\ + 0 & 0 & -0.61 & ab \\\\ + 1 & 1 & -1.22 & cd \\\\ + \\end{tabular} + """ + ) + assert s.to_latex() == expected + + def test_latex_tabular_hrules(self): + s = self.df.style.format(precision=2) + expected = dedent( + """\ + \\begin{tabular}{lrrl} + \\toprule + & A & B & C \\\\ + \\midrule + 0 & 0 & -0.61 & ab \\\\ + 1 & 1 & -1.22 & cd \\\\ + \\bottomrule + \\end{tabular} + """ + ) + assert s.to_latex(hrules=True) == expected + + def test_latex_tabular_custom_hrules(self): + s = self.df.style.format(precision=2) + s.set_table_styles( + [ + {"selector": "toprule", "props": ":hline"}, + {"selector": "bottomrule", "props": ":otherline"}, + ] + ) # no midrule + expected = dedent( + """\ + \\begin{tabular}{lrrl} + \\hline + & A & B & C \\\\ + 0 & 0 & -0.61 & ab \\\\ + 1 & 1 & -1.22 & cd \\\\ + \\otherline + \\end{tabular} + """ + ) + assert s.to_latex() == expected From cce7285b4256814f6e68a4eb36813fd50d5d5624 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 13 Mar 2021 10:43:21 +0100 Subject: [PATCH 16/95] more unit tests and code bugs removed --- pandas/io/formats/style.py | 40 ++++++--- pandas/io/formats/templates/latex.tpl | 3 +- .../tests/io/formats/style/test_to_latex.py | 81 ++++++++++++++++--- 3 files changed, 103 insertions(+), 21 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9c00971304643..54beb00afff07 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -312,17 +312,32 @@ def to_latex( caption: Optional[str] = None, encoding: Optional[str] = None, ): - if column_format is None: # set float, complex, int cols to 'r', index to 'l' + 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', index to 'l' numeric_cols = list(self.data.select_dtypes(include=[np.number]).columns) numeric_cols = self.columns.get_indexer_for(numeric_cols) 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 ci in numeric_cols else "l" - self.set_table_styles( - [{"selector": "column_format", "props": f":{column_format}"}], - overwrite=False, - ) + self.set_table_styles( + [{"selector": "column_format", "props": f":{column_format}"}], + overwrite=False, + ) if position: self.set_table_styles( @@ -349,13 +364,16 @@ def to_latex( if caption: self.set_caption(caption) - wrappers = ["position", "label", "caption"] - if self.table_styles is not None and not any( - d["selector"] in wrappers for d in self.table_styles - ): - table_wrapping = False - else: + IGNORED_WRAPPERS = ["toprule", "midrule", "bottomrule", "column_format"] + # {tabular} is wrapped inside {table} if table_styles selectors exist that are + # not any of the ignored wrappers. + if ( + self.table_styles is not None + and any(d["selector"] not in IGNORED_WRAPPERS for d in self.table_styles) + ) or self.caption: table_wrapping = True + else: + table_wrapping = False latex = self.render(latex=True, table_wrapping=table_wrapping) return save_to_buffer(latex, buf=buf, encoding=encoding) diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index 5f9a36db0c46d..d81c8749b1268 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -3,7 +3,8 @@ {%- set position = parse_table(table_styles, 'position') %} {%- if position is not none %} [{{position}}] -{% endif %} +{%- endif %} + {% set float = parse_table(table_styles, 'float') %} {% if float is not none%} \{{float}} diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index fa5cdc1dca618..d88b7d5b41926 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -15,6 +15,7 @@ class TestStylerLatex: def setup_method(self, method): self.df = DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]}) + self.s = self.df.style.format(precision=2) def test_parse_latex_table_styles(self): s = self.df.style.set_table_styles( @@ -41,7 +42,6 @@ def test_parse_latex_cell_styles_braces_wrap(self): assert _parse_latex_cell_styles(cell_style, "text") == expected def test_minimal_latex_tabular(self): - s = self.df.style.format(precision=2) expected = dedent( """\ \\begin{tabular}{lrrl} @@ -51,10 +51,9 @@ def test_minimal_latex_tabular(self): \\end{tabular} """ ) - assert s.to_latex() == expected + assert self.s.to_latex() == expected - def test_latex_tabular_hrules(self): - s = self.df.style.format(precision=2) + def test_tabular_hrules(self): expected = dedent( """\ \\begin{tabular}{lrrl} @@ -67,11 +66,10 @@ def test_latex_tabular_hrules(self): \\end{tabular} """ ) - assert s.to_latex(hrules=True) == expected + assert self.s.to_latex(hrules=True) == expected - def test_latex_tabular_custom_hrules(self): - s = self.df.style.format(precision=2) - s.set_table_styles( + def test_tabular_custom_hrules(self): + self.s.set_table_styles( [ {"selector": "toprule", "props": ":hline"}, {"selector": "bottomrule", "props": ":otherline"}, @@ -88,4 +86,69 @@ def test_latex_tabular_custom_hrules(self): \\end{tabular} """ ) - assert s.to_latex() == expected + assert self.s.to_latex() == expected + + def test_column_format(self): + # default setting is already tested in `test_latex_minimal_tabular` + self.s.set_table_styles([{"selector": "column_format", "props": ":cccc"}]) + + assert "\\begin{tabular}{rrrr}" in self.s.to_latex(column_format="rrrr") + self.s.set_table_styles([{"selector": "column_format", "props": ":rrrr"}]) + assert "\\begin{tabular}{rrrr}" in self.s.to_latex() + + def test_position(self): + assert "\\begin{table}[h!]" in self.s.to_latex(position="h!") + assert "\\end{table}" in self.s.to_latex(position="h!") + self.s.set_table_styles([{"selector": "position", "props": ":h!"}]) + assert "\\begin{table}[h!]" in self.s.to_latex() + assert "\\end{table}" in self.s.to_latex() + + def test_label(self): + assert "\\label{text}" in self.s.to_latex(label="text") + self.s.set_table_styles([{"selector": "label", "props": ":{text}"}]) + assert "\\label{text}" in self.s.to_latex() + + @pytest.mark.parametrize("label", [(None, ""), ("text", "\\label{text}")]) + @pytest.mark.parametrize("position", [(None, ""), ("h!", "{table}[h!]")]) + @pytest.mark.parametrize("caption", [(None, ""), ("text", "\\caption{text}")]) + @pytest.mark.parametrize("column_format", [(None, ""), ("rcrl", "{tabular}{rcrl}")]) + def test_kwargs_combinations(self, label, position, caption, column_format): + result = self.s.to_latex( + label=label[0], + position=position[0], + caption=caption[0], + column_format=column_format[0], + ) + assert label[1] in result + assert position[1] in result + assert caption[1] in result + assert column_format[1] in result + + def test_custom_table_styles(self): + self.s.set_table_styles( + [ + {"selector": "mycommand", "props": ":{myoptions}"}, + {"selector": "mycommand2", "props": ":{myoptions2}"}, + ] + ) + expected = dedent( + """\ + \\begin{table} + \\mycommand{myoptions} + \\mycommand2{myoptions2} + """ + ) + assert expected in self.s.to_latex() + + def test_cell_styling(self): + self.s.highlight_max(props="emph:;Huge:-wrap-;") + expected = dedent( + """\ + \\begin{tabular}{lrrl} + & A & B & C \\\\ + 0 & 0 & \\emph{{\\Huge -0.61}} & ab \\\\ + 1 & \\emph{{\\Huge 1}} & -1.22 & \\emph{{\\Huge cd}} \\\\ + \\end{tabular} + """ + ) + assert expected == self.s.to_latex() From 26edba49040ea514a27ed1bd40847b456bb602c0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 13 Mar 2021 12:35:34 +0100 Subject: [PATCH 17/95] deal with multi col headers --- pandas/io/formats/style.py | 18 ++++++++++++++++++ pandas/io/formats/templates/latex.tpl | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 54beb00afff07..c3b4bf32dd5e2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -877,6 +877,7 @@ def render(self, latex: bool = False, **kwargs) -> str: template = self.template2 template.globals["parse_table"] = _parse_latex_table_styles template.globals["parse_cell"] = _parse_latex_cell_styles + template.globals["parse_col_head"] = _parse_latex_header_colspan d.update(kwargs) return template.render(**d) @@ -2399,3 +2400,20 @@ def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: else: display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" return display_value + + +def _parse_latex_header_colspan(cell: Dict) -> str: + """ + examines a header cell dict and if it detects a 'colspan' attribute will reformat + the latex display value + + For example: if cell = {'display_vale':'text', 'attributes': 'colspan="3"'} + The latex output will be '& & text' instead of 'text' + """ + colspan = 1 + if "attributes" in cell: + attrs = cell["attributes"] + if 'colspan="' in attrs: + colspan = attrs[attrs.find('colspan="') + 9 :] + colspan = int(colspan[: colspan.find('"')]) + return "& " * (colspan - 1) + str(cell["display_value"]) diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index d81c8749b1268..cecc77cc2e0b1 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -32,7 +32,9 @@ {% if toprule is not none %} \{{toprule}} {% endif %} -{% for item in head[0] %}{%- if not loop.first %} & {% endif %}{{item.display_value}}{% endfor %} \\ +{% for row in head %} +{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_col_head(c)}}{% endfor %} \\ +{% endfor %} {% set midrule = parse_table(table_styles, 'midrule') %} {% if midrule is not none %} \{{midrule}} From 3d7c614a05a4aba068d3cc773176bd29af3d7123 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 14:40:26 +0100 Subject: [PATCH 18/95] deal with sparsification with multirow and multicol --- pandas/io/formats/style.py | 62 +++++++++++++++++++-------- pandas/io/formats/templates/latex.tpl | 6 ++- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index c3b4bf32dd5e2..d23af7def7326 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -623,19 +623,40 @@ def _translate(self): def _translate_latex(self, d: Dict) -> Dict: # post processing of d for latex template: - # - remove hidden columns - # - place cellstyles in individual cell dicts that are not index cells + # - remove hidden columns from the non-index part of the table + # - place cellstyles in individual td cells + # - reinsert missing index th in row if part of multiindex for multirow d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]] - d["body"] = [ - [ - {**col, "cellstyle": []} - if col["type"] == "th" - else {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} + body = [] + for r, row in enumerate(d["body"]): + row_body_cells = [ # replicate the td cells, give them cellstyle + {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} for c, col in enumerate(row) - if col["is_visible"] + if (col["is_visible"] and col["type"] == "td") ] - for r, row in enumerate(d["body"]) - ] + row_body_headers = [ # if a th element is not visible due to multiindex + { # then reinsert it for latex structuring + **col, + "display_value": col["display_value"] if col["is_visible"] else "", + } + for col in row + if col["type"] == "th" + ] + row_body_headers = [] if self.hidden_index else row_body_headers + body.append(row_body_headers + row_body_cells) + d["body"] = body + + # d["body"] = [ + # [ + # {**col, "cellstyle": []} + # if col["type"] == "th" + # else {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} + # for c, col in enumerate(row) + # if col["is_visible"] + # ] + # for r, row in enumerate(d["body"]) + # ] + return d def format( @@ -877,7 +898,7 @@ def render(self, latex: bool = False, **kwargs) -> str: template = self.template2 template.globals["parse_table"] = _parse_latex_table_styles template.globals["parse_cell"] = _parse_latex_cell_styles - template.globals["parse_col_head"] = _parse_latex_header_colspan + template.globals["parse_header"] = _parse_latex_header_span d.update(kwargs) return template.render(**d) @@ -2402,18 +2423,25 @@ def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: return display_value -def _parse_latex_header_colspan(cell: Dict) -> str: - """ - examines a header cell dict and if it detects a 'colspan' attribute will reformat +def _parse_latex_header_span(cell: Dict) -> str: + r""" + examines a header cell dict and if it detects a 'colspan' attribute or a 'rowspan' + attribute (which do not occur simultaneously) will reformat the latex display value For example: if cell = {'display_vale':'text', 'attributes': 'colspan="3"'} - The latex output will be '& & text' instead of 'text' + The latex output will be '\multicol{3}{*}{text}' instead of 'text' + + Notes: needs latex packages {multirow} and {multicol} """ - colspan = 1 if "attributes" in cell: attrs = cell["attributes"] if 'colspan="' in attrs: colspan = attrs[attrs.find('colspan="') + 9 :] colspan = int(colspan[: colspan.find('"')]) - return "& " * (colspan - 1) + str(cell["display_value"]) + return f"\\multicolumn{{{colspan}}}{{r}}{{{cell['display_value']}}}" + elif 'rowspan="' in attrs: + rowspan = attrs[attrs.find('rowspan="') + 9 :] + rowspan = int(rowspan[: rowspan.find('"')]) + return f"\\multirow{{{rowspan}}}{{*}}{{{cell['display_value']}}}" + return cell["display_value"] diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index cecc77cc2e0b1..09934d81cc14b 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -33,14 +33,16 @@ \{{toprule}} {% endif %} {% for row in head %} -{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_col_head(c)}}{% endfor %} \\ +{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c)}}{% endfor %} \\ {% endfor %} {% set midrule = parse_table(table_styles, 'midrule') %} {% if midrule is not none %} \{{midrule}} {% endif %} {% for row in body %} -{% for c in row %}{% if not loop.first %} & {% endif %}{{parse_cell(c.cellstyle, c.display_value)}}{% endfor %} \\ +{% for c in row %}{% if not loop.first %} & {% endif %} + {%- if c.type == 'th' %}{{parse_header(c)}}{% else %}{{parse_cell(c.cellstyle, c.display_value)}}{% endif %} +{%- endfor %} \\ {% endfor %} {% set bottomrule = parse_table(table_styles, 'bottomrule') %} {% if bottomrule is not none %} From e2f0f0ce8431df370985fd98d137b55cf51eccb4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 17:30:21 +0100 Subject: [PATCH 19/95] tests for multirow and multicol --- .../tests/io/formats/style/test_to_latex.py | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index d88b7d5b41926..881065d2376df 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -2,7 +2,10 @@ import pytest -from pandas import DataFrame +from pandas import ( + DataFrame, + MultiIndex, +) from pandas.io.formats.style import ( _parse_latex_cell_styles, @@ -23,13 +26,13 @@ def test_parse_latex_table_styles(self): {"selector": "foo", "props": [("attr", "value")]}, {"selector": "bar", "props": [("attr", "overwritten")]}, {"selector": "bar", "props": [("attr", "baz"), ("attr2", "ignored")]}, - {"selector": "label", "props": [("", "fig§item")]}, + {"selector": "label", "props": [("", "{fig§item}")]}, ] ) assert _parse_latex_table_styles(s.table_styles, "bar") == "baz" # test '§' replaced by ':' [for CSS compatibility] - assert _parse_latex_table_styles(s.table_styles, "label") == "fig:item" + assert _parse_latex_table_styles(s.table_styles, "label") == "{fig:item}" def test_parse_latex_cell_styles_basic(self): cell_style = [("emph", ""), ("cellcolor", "[rgb]{0,1,1}")] @@ -37,8 +40,8 @@ def test_parse_latex_cell_styles_basic(self): assert _parse_latex_cell_styles(cell_style, "text") == expected def test_parse_latex_cell_styles_braces_wrap(self): - cell_style = [("Huge", "-wrap-"), ("cellcolor", "-wrap-[rgb]{0,1,1}")] - expected = "{\\Huge {\\cellcolor[rgb]{0,1,1} text}}" + cell_style = [("emph", "-wrap-"), ("cellcolor", "-wrap-[rgb]{0,1,1}")] + expected = "{\\emph {\\cellcolor[rgb]{0,1,1} text}}" assert _parse_latex_cell_styles(cell_style, "text") == expected def test_minimal_latex_tabular(self): @@ -152,3 +155,54 @@ def test_cell_styling(self): """ ) assert expected == self.s.to_latex() + + def test_multiindex_columns(self): + cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + self.df.columns = cidx + expected = dedent( + """\ + \\begin{tabular}{lrrl} + & \\multicolumn{2}{r}{A} & B \\\\ + & a & b & c \\\\ + 0 & 0 & -0.61 & ab \\\\ + 1 & 1 & -1.22 & cd \\\\ + \\end{tabular} + """ + ) + assert expected == self.df.style.format(precision=2).to_latex() + + def test_multiindex_row(self): + ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + self.df.loc[2, :] = [2, -2.22, "de"] + self.df = self.df.astype({"A": int}) + self.df.index = ridx + expected = dedent( + """\ + \\begin{tabular}{llrrl} + & & A & B & C \\\\ + \\multirow{2}{*}{A} & a & 0 & -0.61 & ab \\\\ + & b & 1 & -1.22 & cd \\\\ + B & c & 2 & -2.22 & de \\\\ + \\end{tabular} + """ + ) + assert expected == self.df.style.format(precision=2).to_latex() + + def test_multiindex_row_and_col(self): + cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) + ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + self.df.loc[2, :] = [2, -2.22, "de"] + self.df = self.df.astype({"A": int}) + self.df.index, self.df.columns = ridx, cidx + expected = dedent( + """\ + \\begin{tabular}{llrrl} + & & \\multicolumn{2}{r}{Z} & Y \\\\ + & & a & b & c \\\\ + \\multirow{2}{*}{A} & a & 0 & -0.61 & ab \\\\ + & b & 1 & -1.22 & cd \\\\ + B & c & 2 & -2.22 & de \\\\ + \\end{tabular} + """ + ) + assert expected == self.df.style.format(precision=2).to_latex() From 3243ed789d2119d4eb439c0385d4023345bb5b41 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 13 Mar 2021 19:45:17 +0100 Subject: [PATCH 20/95] comprehensive test --- pandas/io/formats/style.py | 1 + .../tests/io/formats/style/test_to_latex.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d23af7def7326..50b2880b8d354 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -895,6 +895,7 @@ def render(self, latex: bool = False, **kwargs) -> str: template = self.template if latex: d = self._translate_latex(d) + d.update({"table_wrapping": True}) template = self.template2 template.globals["parse_table"] = _parse_latex_table_styles template.globals["parse_cell"] = _parse_latex_cell_styles diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 881065d2376df..9694fd01f2ebd 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -206,3 +206,51 @@ def test_multiindex_row_and_col(self): """ ) assert expected == self.df.style.format(precision=2).to_latex() + + def test_comprehensive(self): + # test as many low level features simultaneously as possible + cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) + ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + self.df.loc[2, :] = [2, -2.22, "de"] + self.df = self.df.astype({"A": int}) + self.df.index, self.df.columns = ridx, cidx + s = self.df.style + s.set_caption("mycap") + s.set_table_styles( + [ + {"selector": "label", "props": ":{fig§item}"}, + {"selector": "position", "props": ":h!"}, + {"selector": "column_format", "props": ":rlrlr"}, + {"selector": "toprule", "props": ":toprule"}, + {"selector": "midrule", "props": ":midrule"}, + {"selector": "bottomrule", "props": ":bottomrule"}, + {"selector": "rowcolors", "props": ":{3}{pink}{}"}, # custom command + ] + ) + s.highlight_max(axis=0, props="textbf:;cellcolor:[rgb]{1,1,0.6}") + s.highlight_max( + axis=None, props="Huge:-wrap-;", subset=[("Z", "a"), ("Z", "b")] + ) + + expected = ( + """\ +\\begin{table}[h!] +\\caption{mycap} +\\label{fig:item} +\\rowcolors{3}{pink}{} +\\begin{tabular}{rlrlr} +\\toprule + & & \\multicolumn{2}{r}{Z} & Y \\\\ + & & a & b & c \\\\ +\\midrule +\\multirow{2}{*}{A} & a & 0 & \\textbf{\\cellcolor[rgb]{1,1,0.6}{-0.61}} & ab \\\\ + & b & 1 & -1.22 & cd \\\\ +B & c & \\textbf{\\cellcolor[rgb]{1,1,0.6}{{\\Huge 2}}} & -2.22 & """ + """\ +\\textbf{\\cellcolor[rgb]{1,1,0.6}{de}} \\\\ +\\bottomrule +\\end{tabular} +\\end{table} +""" + ) + assert expected == s.format(precision=2).render(latex=True) From 0decf2daeae1de45bc5ec12f935cc2183f4e2c0c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 13 Mar 2021 20:24:30 +0100 Subject: [PATCH 21/95] refactor --- pandas/io/formats/style.py | 31 ++++++++++--------- pandas/io/formats/templates/latex.tpl | 4 +-- .../tests/io/formats/style/test_to_latex.py | 2 ++ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 50b2880b8d354..fc52249fdcb05 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -160,7 +160,7 @@ class Styler: loader = jinja2.PackageLoader("pandas", "io/formats/templates") env = jinja2.Environment(loader=loader, trim_blocks=True) template = env.get_template("html.tpl") - template2 = env.get_template("latex.tpl") + template_latex = env.get_template("latex.tpl") def __init__( self, @@ -364,18 +364,7 @@ def to_latex( if caption: self.set_caption(caption) - IGNORED_WRAPPERS = ["toprule", "midrule", "bottomrule", "column_format"] - # {tabular} is wrapped inside {table} if table_styles selectors exist that are - # not any of the ignored wrappers. - if ( - self.table_styles is not None - and any(d["selector"] not in IGNORED_WRAPPERS for d in self.table_styles) - ) or self.caption: - table_wrapping = True - else: - table_wrapping = False - - latex = self.render(latex=True, table_wrapping=table_wrapping) + latex = self.render(latex=True) return save_to_buffer(latex, buf=buf, encoding=encoding) @doc( @@ -895,8 +884,8 @@ def render(self, latex: bool = False, **kwargs) -> str: template = self.template if latex: d = self._translate_latex(d) - d.update({"table_wrapping": True}) - template = self.template2 + template = self.template_latex + template.globals["parse_wrap"] = _parse_latex_table_wrapping template.globals["parse_table"] = _parse_latex_table_styles template.globals["parse_cell"] = _parse_latex_cell_styles template.globals["parse_header"] = _parse_latex_header_span @@ -2375,6 +2364,18 @@ def pred(part) -> bool: return tuple(slice_) +def _parse_latex_table_wrapping(styles: CSSStyles, caption: Optional[str]) -> bool: + IGNORED_WRAPPERS = ["toprule", "midrule", "bottomrule", "column_format"] + # {tabular} is wrapped inside {table} if table_styles selectors exist that are + # not any of the ignored wrappers. + if ( + styles is not None + and any(d["selector"] not in IGNORED_WRAPPERS for d in styles) + ) or caption: + return True + return False + + def _parse_latex_table_styles(styles: CSSStyles, selector: str) -> Optional[str]: """ Find the relevant first `props` `value` from a list of `(attribute,value)` tuples diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl index 09934d81cc14b..3be1b617aabbb 100644 --- a/pandas/io/formats/templates/latex.tpl +++ b/pandas/io/formats/templates/latex.tpl @@ -1,4 +1,4 @@ -{% if table_wrapping %} +{% if parse_wrap(table_styles, caption) %} \begin{table} {%- set position = parse_table(table_styles, 'position') %} {%- if position is not none %} @@ -49,6 +49,6 @@ \{{bottomrule}} {% endif %} \end{tabular} -{% if table_wrapping %} +{% if parse_wrap(table_styles, caption) %} \end{table} {% endif %} diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 9694fd01f2ebd..587fd3b289d22 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -220,6 +220,7 @@ def test_comprehensive(self): [ {"selector": "label", "props": ":{fig§item}"}, {"selector": "position", "props": ":h!"}, + {"selector": "float", "props": ":centering"}, {"selector": "column_format", "props": ":rlrlr"}, {"selector": "toprule", "props": ":toprule"}, {"selector": "midrule", "props": ":midrule"}, @@ -235,6 +236,7 @@ def test_comprehensive(self): expected = ( """\ \\begin{table}[h!] +\\centering \\caption{mycap} \\label{fig:item} \\rowcolors{3}{pink}{} From c8dc5e5bb5f636f9f3eaa4d8602ae05ca2f739a4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 20:56:15 +0100 Subject: [PATCH 22/95] change -wrap- to --wrap --- pandas/io/formats/style.py | 8 +- .../tests/io/formats/style/test_to_latex.py | 85 +++++++++++++------ 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index fc52249fdcb05..2a6c86f511738 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2410,15 +2410,15 @@ def _parse_latex_cell_styles(styles: CSSList, display_value: str) -> str: Sometimes latex commands have to be wrapped with curly braces: Instead of `\{ text}` - In this case if the keyphrase '-wrap-' is detected in it will return + In this case if the keyphrase '--wrap' is detected in it will return correctly, for example: - `styles=[('Huge', '-wrap-'), ('cellcolor', '[rgb]{0,1,1}')]` will yield: + `styles=[('Huge', '--wrap'), ('cellcolor', '[rgb]{0,1,1}')]` will yield: {\Huge \cellcolor[rgb]{0,1,1}{display_value}} """ for style in styles[::-1]: # in reverse for most recently applied style - if "-wrap-" in style[1]: + if "--wrap" in style[1]: display_value = ( - f"{{\\{style[0]}{style[1].replace('-wrap-','')} {display_value}}}" + f"{{\\{style[0]}{style[1].replace('--wrap','')} {display_value}}}" ) else: display_value = f"\\{style[0]}{style[1]}{{{display_value}}}" diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 587fd3b289d22..d70f66c94fe44 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -9,7 +9,9 @@ from pandas.io.formats.style import ( _parse_latex_cell_styles, + _parse_latex_header_span, _parse_latex_table_styles, + _parse_latex_table_wrapping, ) pytest.importorskip("jinja2") @@ -20,30 +22,6 @@ def setup_method(self, method): self.df = DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]}) self.s = self.df.style.format(precision=2) - def test_parse_latex_table_styles(self): - s = self.df.style.set_table_styles( - [ - {"selector": "foo", "props": [("attr", "value")]}, - {"selector": "bar", "props": [("attr", "overwritten")]}, - {"selector": "bar", "props": [("attr", "baz"), ("attr2", "ignored")]}, - {"selector": "label", "props": [("", "{fig§item}")]}, - ] - ) - assert _parse_latex_table_styles(s.table_styles, "bar") == "baz" - - # test '§' replaced by ':' [for CSS compatibility] - assert _parse_latex_table_styles(s.table_styles, "label") == "{fig:item}" - - def test_parse_latex_cell_styles_basic(self): - cell_style = [("emph", ""), ("cellcolor", "[rgb]{0,1,1}")] - expected = "\\emph{\\cellcolor[rgb]{0,1,1}{text}}" - assert _parse_latex_cell_styles(cell_style, "text") == expected - - def test_parse_latex_cell_styles_braces_wrap(self): - cell_style = [("emph", "-wrap-"), ("cellcolor", "-wrap-[rgb]{0,1,1}")] - expected = "{\\emph {\\cellcolor[rgb]{0,1,1} text}}" - assert _parse_latex_cell_styles(cell_style, "text") == expected - def test_minimal_latex_tabular(self): expected = dedent( """\ @@ -144,7 +122,7 @@ def test_custom_table_styles(self): assert expected in self.s.to_latex() def test_cell_styling(self): - self.s.highlight_max(props="emph:;Huge:-wrap-;") + self.s.highlight_max(props="emph:;Huge:--wrap;") expected = dedent( """\ \\begin{tabular}{lrrl} @@ -230,7 +208,7 @@ def test_comprehensive(self): ) s.highlight_max(axis=0, props="textbf:;cellcolor:[rgb]{1,1,0.6}") s.highlight_max( - axis=None, props="Huge:-wrap-;", subset=[("Z", "a"), ("Z", "b")] + axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")] ) expected = ( @@ -256,3 +234,58 @@ def test_comprehensive(self): """ ) assert expected == s.format(precision=2).render(latex=True) + + def test_parse_latex_table_styles(self): + s = self.df.style.set_table_styles( + [ + {"selector": "foo", "props": [("attr", "value")]}, + {"selector": "bar", "props": [("attr", "overwritten")]}, + {"selector": "bar", "props": [("attr", "baz"), ("attr2", "ignored")]}, + {"selector": "label", "props": [("", "{fig§item}")]}, + ] + ) + assert _parse_latex_table_styles(s.table_styles, "bar") == "baz" + + # test '§' replaced by ':' [for CSS compatibility] + assert _parse_latex_table_styles(s.table_styles, "label") == "{fig:item}" + + def test_parse_latex_cell_styles_basic(self): + cell_style = [("emph", ""), ("cellcolor", "[rgb]{0,1,1}")] + expected = "\\emph{\\cellcolor[rgb]{0,1,1}{text}}" + assert _parse_latex_cell_styles(cell_style, "text") == expected + + def test_parse_latex_cell_styles_braces_wrap(self): + cell_style = [("emph", "--wrap"), ("cellcolor", "[rgb]{0,1,1}--wrap")] + expected = "{\\emph {\\cellcolor[rgb]{0,1,1} text}}" + assert _parse_latex_cell_styles(cell_style, "text") == expected + + def test_parse_latex_header_span(self): + cell = {"attributes": 'colspan="3"', "display_value": "text"} + expected = "\\multicolumn{3}{r}{text}" + assert _parse_latex_header_span(cell) == expected + + cell = {"attributes": 'rowspan="5"', "display_value": "text"} + expected = "\\multirow{5}{*}{text}" + assert _parse_latex_header_span(cell) == expected + + cell = {"display_value": "text"} + assert _parse_latex_header_span(cell) == "text" + + def test_parse_latex_table_wrapping(self): + self.s.set_table_styles( + [ + {"selector": "toprule", "props": ":value"}, + {"selector": "bottomrule", "props": ":value"}, + {"selector": "midrule", "props": ":value"}, + {"selector": "column_format", "props": ":value"}, + ] + ) + assert _parse_latex_table_wrapping(self.s.table_styles, self.s.caption) is False + assert _parse_latex_table_wrapping(self.s.table_styles, "some caption") is True + self.s.set_table_styles( + [ + {"selector": "not-ignored", "props": ":value"}, + ], + overwrite=False, + ) + assert _parse_latex_table_wrapping(self.s.table_styles, None) is True From 8c58538a16d11428c9254c12cdf1c4c6c3628115 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 21:50:17 +0100 Subject: [PATCH 23/95] remove redundant comments --- pandas/io/formats/style.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2a6c86f511738..307ed5af0f8b6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -634,18 +634,6 @@ def _translate_latex(self, d: Dict) -> Dict: row_body_headers = [] if self.hidden_index else row_body_headers body.append(row_body_headers + row_body_cells) d["body"] = body - - # d["body"] = [ - # [ - # {**col, "cellstyle": []} - # if col["type"] == "th" - # else {**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]} - # for c, col in enumerate(row) - # if col["is_visible"] - # ] - # for r, row in enumerate(d["body"]) - # ] - return d def format( From 5c9d50a626e851d4fac0f32d764fb78e107bb8c4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 13 Mar 2021 22:44:48 +0100 Subject: [PATCH 24/95] docs --- pandas/io/formats/style.py | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 307ed5af0f8b6..8a2f6c8fb566a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -312,6 +312,80 @@ def to_latex( caption: Optional[str] = None, encoding: Optional[str] = None, ): + """ + Output Styler to a file, buffer or string in LaTeX format. + + .. versionadded:: TODO + + Parameters + ---------- + buf: TODO + column_format : str, optional + The LaTeX column specification placed in location: + `\begin{tabular}{}`. + Defaults to 'l' for index and non-numeric data columns, otherwise 'r'. + position : str, optional + The LaTeX positional argument for tables, placed in location: + `\begin{table}[]`. + hrules : bool, default False + Whether to add `\toprule`, `\\midrule` and `\bottomrule` from the + {booktabs} LaTeX package. + label : str, optional + The LaTeX label placed in location: `\\label{