diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst
index 2f2e8aed6fdb8..8cc17da3d5647 100644
--- a/doc/source/whatsnew/v1.3.0.rst
+++ b/doc/source/whatsnew/v1.3.0.rst
@@ -142,7 +142,7 @@ properly format HTML and eliminate some inconsistencies (:issue:`39942` :issue:`
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.
+which renders the necessary LaTeX format including built-up styles. An additional file io function :meth:`Styler.to_html` has been added for convenience (:issue:`40312`).
Documentation has also seen major revisions in light of new features (:issue:`39720` :issue:`39317` :issue:`40493`)
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index 977a3a24f0844..2f4e51478aaf6 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -739,6 +739,74 @@ def to_latex(
return save_to_buffer(latex, buf=buf, encoding=encoding)
+ def to_html(
+ self,
+ buf: FilePathOrBuffer[str] | None = None,
+ *,
+ table_uuid: str | None = None,
+ table_attributes: str | None = None,
+ encoding: str | None = None,
+ doctype_html: bool = False,
+ exclude_styles: bool = False,
+ ):
+ """
+ Write Styler to a file, buffer or string in HTML-CSS 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.
+ table_uuid: str, optional
+ Id attribute assigned to the
HTML element in the format:
+
+ ````
+
+ If not given uses Styler's initially assigned value.
+ table_attributes: str, optional
+ Attributes to assign within the `` HTML element in the format:
+
+ `` >``
+
+ If not given defaults to Styler's preexisting value.
+ encoding : str, optional
+ Character encoding setting for file output, and HTML meta tags,
+ defaults to "utf-8" if None.
+ doctype_html : bool, default False
+ Whether to output a fully structured HTML file including all
+ HTML elements, or just the core ``
-{% endblock style %}
-{% block before_table %}{% endblock before_table %}
-{% block table %}
-
-{% block caption %}
-{% if caption %}
- {{caption}}
+{# Update the template_structure.html documentation too #}
+{% if doctype_html %}
+
+
+
+
+{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %}
+
+
+{% include "html_table.tpl" %}
+
+
+{% elif not doctype_html %}
+{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %}
+{% include "html_table.tpl" %}
{% endif %}
-{% endblock caption %}
-{% block thead %}
-
-{% block before_head_rows %}{% endblock %}
-{% for r in head %}
-{% block head_tr scoped %}
-
-{% for c in r %}
-{% if c.is_visible != False %}
- <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}}{{c.type}}>
-{% endif %}
-{% endfor %}
-
-{% endblock head_tr %}
-{% endfor %}
-{% block after_head_rows %}{% endblock %}
-
-{% endblock thead %}
-{% block tbody %}
-
-{% block before_rows %}{% endblock before_rows %}
-{% for r in body %}
-{% block tr scoped %}
-
-{% for c in r %}
-{% if c.is_visible != False %}
- <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}}{{c.type}}>
-{% endif %}
-{% endfor %}
-
-{% endblock tr %}
-{% endfor %}
-{% block after_rows %}{% endblock after_rows %}
-
-{% endblock tbody %}
-
-{% endblock table %}
-{% block after_table %}{% endblock after_table %}
diff --git a/pandas/io/formats/templates/html_style.tpl b/pandas/io/formats/templates/html_style.tpl
new file mode 100644
index 0000000000000..b34893076bedd
--- /dev/null
+++ b/pandas/io/formats/templates/html_style.tpl
@@ -0,0 +1,24 @@
+{%- block before_style -%}{%- endblock before_style -%}
+{% block style %}
+
+{% endblock style %}
diff --git a/pandas/io/formats/templates/html_table.tpl b/pandas/io/formats/templates/html_table.tpl
new file mode 100644
index 0000000000000..dadefa4bd8365
--- /dev/null
+++ b/pandas/io/formats/templates/html_table.tpl
@@ -0,0 +1,61 @@
+{% block before_table %}{% endblock before_table %}
+{% block table %}
+{% if exclude_styles %}
+
+{% else %}
+
+{% endif %}
+{% block caption %}
+{% if caption %}
+ {{caption}}
+{% endif %}
+{% endblock caption %}
+{% block thead %}
+
+{% block before_head_rows %}{% endblock %}
+{% for r in head %}
+{% block head_tr scoped %}
+
+{% if exclude_styles %}
+{% for c in r %}
+{% if c.is_visible != False %}
+ <{{c.type}} {{c.attributes}}>{{c.value}}{{c.type}}>
+{% endif %}
+{% endfor %}
+{% else %}
+{% for c in r %}
+{% if c.is_visible != False %}
+ <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}}{{c.type}}>
+{% endif %}
+{% endfor %}
+{% endif %}
+
+{% endblock head_tr %}
+{% endfor %}
+{% block after_head_rows %}{% endblock %}
+
+{% endblock thead %}
+{% block tbody %}
+
+{% block before_rows %}{% endblock before_rows %}
+{% for r in body %}
+{% block tr scoped %}
+
+{% if exclude_styles %}
+{% for c in r %}{% if c.is_visible != False %}
+ <{{c.type}} {{c.attributes}}>{{c.display_value}}{{c.type}}>
+{% endif %}{% endfor %}
+{% else %}
+{% for c in r %}{% if c.is_visible != False %}
+ <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}}{{c.type}}>
+{% endif %}{% endfor %}
+{% endif %}
+
+{% endblock tr %}
+{% endfor %}
+{% block after_rows %}{% endblock after_rows %}
+
+{% endblock tbody %}
+
+{% endblock table %}
+{% block after_table %}{% endblock after_table %}
diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py
new file mode 100644
index 0000000000000..6c3abe04db926
--- /dev/null
+++ b/pandas/tests/io/formats/style/test_html.py
@@ -0,0 +1,233 @@
+from textwrap import dedent
+
+import pytest
+
+from pandas import DataFrame
+
+jinja2 = pytest.importorskip("jinja2")
+from pandas.io.formats.style import Styler
+
+loader = jinja2.PackageLoader("pandas", "io/formats/templates")
+env = jinja2.Environment(loader=loader, trim_blocks=True)
+
+
+@pytest.fixture
+def styler():
+ return Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]))
+
+
+@pytest.fixture
+def tpl_style():
+ return env.get_template("html_style.tpl")
+
+
+@pytest.fixture
+def tpl_table():
+ return env.get_template("html_table.tpl")
+
+
+def test_html_template_extends_options():
+ # make sure if templates are edited tests are updated as are setup fixtures
+ # to understand the dependency
+ with open("pandas/io/formats/templates/html.tpl") as file:
+ result = file.read()
+ assert '{% include "html_style.tpl" %}' in result
+ assert '{% include "html_table.tpl" %}' in result
+
+
+def test_exclude_styles(styler):
+ result = styler.to_html(exclude_styles=True, doctype_html=True)
+ expected = dedent(
+ """\
+
+
+
+
+
+
+
+
+
+ |
+ A |
+
+
+
+
+ a |
+ 2.610000 |
+
+
+ b |
+ 2.690000 |
+
+
+
+
+
+ """
+ )
+ assert result == expected
+
+
+def test_w3_html_format(styler):
+ styler.set_uuid("").set_table_styles(
+ [{"selector": "th", "props": "att2:v2;"}]
+ ).applymap(lambda x: "att1:v1;").set_table_attributes(
+ 'class="my-cls1" style="attr3:v3;"'
+ ).set_td_classes(
+ DataFrame(["my-cls2"], index=["a"], columns=["A"])
+ ).format(
+ "{:.1f}"
+ ).set_caption(
+ "A comprehensive test"
+ )
+ expected = dedent(
+ """\
+
+
+ A comprehensive test
+
+
+ |
+ A |
+
+
+
+
+ a |
+ 2.6 |
+
+
+ b |
+ 2.7 |
+
+
+
+ """
+ )
+ assert expected == styler.render()
+
+
+def test_colspan_w3():
+ # GH 36223
+ df = DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]])
+ styler = Styler(df, uuid="_", cell_ids=False)
+ assert 'l0 | ' in styler.render()
+
+
+def test_rowspan_w3():
+ # GH 38533
+ df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]])
+ styler = Styler(df, uuid="_", cell_ids=False)
+ assert (
+ 'l0 | ' in styler.render()
+ )
+
+
+def test_styles(styler):
+ styler.set_uuid("abc_")
+ styler.set_table_styles([{"selector": "td", "props": "color: red;"}])
+ result = styler.to_html(doctype_html=True)
+ expected = dedent(
+ """\
+
+
+
+
+
+
+
+
+
+
+ |
+ A |
+
+
+
+
+ a |
+ 2.610000 |
+
+
+ b |
+ 2.690000 |
+
+
+
+
+
+ """
+ )
+ assert result == expected
+
+
+def test_doctype(styler):
+ result = styler.to_html(doctype_html=False)
+ assert "" not in result
+ assert "" not in result
+ assert "" not in result
+ assert "" not in result
+
+
+def test_block_names(tpl_style, tpl_table):
+ # catch accidental removal of a block
+ expected_style = {
+ "before_style",
+ "style",
+ "table_styles",
+ "before_cellstyle",
+ "cellstyle",
+ }
+ expected_table = {
+ "before_table",
+ "table",
+ "caption",
+ "thead",
+ "tbody",
+ "after_table",
+ "before_head_rows",
+ "head_tr",
+ "after_head_rows",
+ "before_rows",
+ "tr",
+ "after_rows",
+ }
+ result1 = set(tpl_style.blocks)
+ assert result1 == expected_style
+
+ result2 = set(tpl_table.blocks)
+ assert result2 == expected_table
+
+
+def test_from_custom_template(tmpdir):
+ p = tmpdir.mkdir("templates").join("myhtml.tpl")
+ p.write(
+ dedent(
+ """\
+ {% extends "html.tpl" %}
+ {% block table %}
+ {{ table_title|default("My Table") }}
+ {{ super() }}
+ {% endblock table %}"""
+ )
+ )
+ result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl")
+ assert issubclass(result, Styler)
+ assert result.env is not Styler.env
+ assert result.template_html is not Styler.template_html
+ styler = result(DataFrame({"A": [1, 2]}))
+ assert styler.render()
diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py
index c556081b5f562..12b4a13ade271 100644
--- a/pandas/tests/io/formats/style/test_style.py
+++ b/pandas/tests/io/formats/style/test_style.py
@@ -1,6 +1,5 @@
import copy
import re
-import textwrap
import numpy as np
import pytest
@@ -1293,21 +1292,6 @@ def test_column_and_row_styling(self):
)
assert "#T__ .row0 {\n color: blue;\n}" in s.render()
- def test_colspan_w3(self):
- # GH 36223
- df = DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]])
- s = Styler(df, uuid="_", cell_ids=False)
- assert 'l0 | ' in s.render()
-
- def test_rowspan_w3(self):
- # GH 38533
- df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]])
- s = Styler(df, uuid="_", cell_ids=False)
- assert (
- 'l0 | ' in s.render()
- )
-
@pytest.mark.parametrize("len_", [1, 5, 32, 33, 100])
def test_uuid_len(self, len_):
# GH 36345
@@ -1328,49 +1312,6 @@ def test_uuid_len_raises(self, len_):
with pytest.raises(TypeError, match=msg):
Styler(df, uuid_len=len_, cell_ids=False).render()
- def test_w3_html_format(self):
- s = (
- Styler(
- DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]),
- uuid_len=0,
- )
- .set_table_styles([{"selector": "th", "props": "att2:v2;"}])
- .applymap(lambda x: "att1:v1;")
- .set_table_attributes('class="my-cls1" style="attr3:v3;"')
- .set_td_classes(DataFrame(["my-cls2"], index=["a"], columns=["A"]))
- .format("{:.1f}")
- .set_caption("A comprehensive test")
- )
- expected = """
-
- A comprehensive test
-
-
- |
- A |
-
-
-
-
- a |
- 2.6 |
-
-
- b |
- 2.7 |
-
-
-
-"""
- assert expected == s.render()
-
@pytest.mark.parametrize(
"slc",
[
@@ -1457,48 +1398,3 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_):
expected = df.loc[slice_]
result = df.loc[non_reducing_slice(slice_)]
tm.assert_frame_equal(result, expected)
-
-
-def test_block_names():
- # catch accidental removal of a block
- expected = {
- "before_style",
- "style",
- "table_styles",
- "before_cellstyle",
- "cellstyle",
- "before_table",
- "table",
- "caption",
- "thead",
- "tbody",
- "after_table",
- "before_head_rows",
- "head_tr",
- "after_head_rows",
- "before_rows",
- "tr",
- "after_rows",
- }
- result = set(Styler.template_html.blocks)
- assert result == expected
-
-
-def test_from_custom_template(tmpdir):
- p = tmpdir.mkdir("templates").join("myhtml.tpl")
- p.write(
- textwrap.dedent(
- """\
- {% extends "html.tpl" %}
- {% block table %}
- {{ table_title|default("My Table") }}
- {{ super() }}
- {% endblock table %}"""
- )
- )
- result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl")
- assert issubclass(result, Styler)
- assert result.env is not Styler.env
- assert result.template_html is not Styler.template_html
- styler = result(DataFrame({"A": [1, 2]}))
- assert styler.render()