Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add styler option context for sparsification of columns and index separately #41512

Merged
merged 9 commits into from
May 21, 2021
8 changes: 4 additions & 4 deletions asv_bench/benchmarks/io/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ def setup(self, cols, rows):

def time_apply_render(self, cols, rows):
self._style_apply()
self.st._render_html()
self.st._render_html(True, True)

def peakmem_apply_render(self, cols, rows):
self._style_apply()
self.st._render_html()
self.st._render_html(True, True)

def time_classes_render(self, cols, rows):
self._style_classes()
self.st._render_html()
self.st._render_html(True, True)

def peakmem_classes_render(self, cols, rows):
self._style_classes()
self.st._render_html()
self.st._render_html(True, True)

def time_format_render(self, cols, rows):
self._style_format()
Expand Down
5 changes: 5 additions & 0 deletions doc/source/user_guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ plotting.backend matplotlib Change the plotting backend
like Bokeh, Altair, etc.
plotting.matplotlib.register_converters True Register custom converters with
matplotlib. Set to False to de-register.
styler.sparse.index True "Sparsify" MultiIndex display for rows
in Styler output (don't display repeated
elements in outer levels within groups).
styler.sparse.columns True "Sparsify" MultiIndex display for columns
in Styler output.
======================================= ============ ==================================


Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ precision, and perform HTML escaping (:issue:`40437` :issue:`40134`). There have
properly format HTML and eliminate some inconsistencies (:issue:`39942` :issue:`40356` :issue:`39807` :issue:`39889` :issue:`39627`)

: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`).

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

Expand Down
23 changes: 23 additions & 0 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,26 @@ def register_converter_cb(key):
validator=is_one_of_factory(["auto", True, False]),
cb=register_converter_cb,
)

# ------
# Styler
# ------

styler_sparse_index_doc = """
: bool
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.
"""

styler_sparse_columns_doc = """
: bool
Whether to sparsify the display of hierarchical columns. Setting to False will
display each explicit level element in a hierarchical key for each column.
"""

with cf.config_prefix("styler"):
cf.register_option("sparse.index", True, styler_sparse_index_doc, validator=bool)

cf.register_option(
"sparse.columns", True, styler_sparse_columns_doc, validator=bool
)
25 changes: 22 additions & 3 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import numpy as np

from pandas._config import get_option

from pandas._typing import (
Axis,
FrameOrSeries,
Expand Down Expand Up @@ -201,14 +203,27 @@ def _repr_html_(self) -> str:
"""
Hooks into Jupyter notebook rich display system.
"""
return self._render_html()
return self.render()

def render(self, **kwargs) -> str:
def render(
self,
sparse_index: bool | None = None,
sparse_columns: bool | None = None,
**kwargs,
) -> str:
"""
Render the ``Styler`` including all applied styles to HTML.

Parameters
----------
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.
**kwargs
Any additional keyword arguments are passed
through to ``self.template.render``.
Expand Down Expand Up @@ -240,7 +255,11 @@ def render(self, **kwargs) -> str:
* caption
* table_attributes
"""
return self._render_html(**kwargs)
if sparse_index is None:
sparse_index = get_option("styler.sparse.index")
if sparse_columns is None:
sparse_columns = get_option("styler.sparse.columns")
return self._render_html(sparse_index, sparse_columns, **kwargs)

def set_tooltips(
self,
Expand Down
27 changes: 11 additions & 16 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ def __init__(
tuple[int, int], Callable[[Any], str]
] = defaultdict(lambda: partial(_default_formatter, precision=def_precision))

def _render_html(self, **kwargs) -> str:
def _render_html(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str:
"""
Renders the ``Styler`` including all applied styles to HTML.
Generates a dict with necessary kwargs passed to jinja2 template.
"""
self._compute()
# TODO: namespace all the pandas keys
d = self._translate()
d = self._translate(sparse_index, sparse_columns)
d.update(kwargs)
return self.template_html.render(**d)

Expand All @@ -133,9 +133,7 @@ def _compute(self):
r = func(self)(*args, **kwargs)
return r

def _translate(
self, sparsify_index: bool | None = None, sparsify_cols: bool | None = None
):
def _translate(self, sparse_index: bool, sparse_cols: bool):
"""
Process Styler data and settings into a dict for template rendering.

Expand All @@ -144,22 +142,19 @@ def _translate(

Parameters
----------
sparsify_index : bool, optional
Whether to sparsify the index or print all hierarchical index elements
sparsify_cols : bool, optional
Whether to sparsify the columns or print all hierarchical column elements
sparse_index : bool
Whether to sparsify the index or print all hierarchical index elements.
Upstream defaults are typically to `pandas.options.styler.sparse.index`.
sparse_cols : bool
Whether to sparsify the columns or print all hierarchical column elements.
Upstream defaults are typically to `pandas.options.styler.sparse.columns`.

Returns
-------
d : dict
The following structure: {uuid, table_styles, caption, head, body,
cellstyle, table_attributes}
"""
if sparsify_index is None:
sparsify_index = get_option("display.multi_sparse")
if sparsify_cols is None:
sparsify_cols = get_option("display.multi_sparse")

ROW_HEADING_CLASS = "row_heading"
COL_HEADING_CLASS = "col_heading"
INDEX_NAME_CLASS = "index_name"
Expand All @@ -176,14 +171,14 @@ def _translate(
}

head = self._translate_header(
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparsify_cols
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparse_cols
)
d.update({"head": head})

self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
list
)
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparsify_index)
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparse_index)
d.update({"body": body})

cellstyle: list[dict[str, CSSList | list[str]]] = [
Expand Down
46 changes: 24 additions & 22 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,28 @@ def styler(df):


def test_display_format(styler):
ctx = styler.format("{:0.1f}")._translate()
ctx = styler.format("{:0.1f}")._translate(True, True)
assert all(["display_value" in c for c in row] for row in ctx["body"])
assert all([len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"])
assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3


def test_format_dict(styler):
ctx = styler.format({"A": "{:0.1f}", "B": "{0:.2%}"})._translate()
ctx = styler.format({"A": "{:0.1f}", "B": "{0:.2%}"})._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "0.0"
assert ctx["body"][0][2]["display_value"] == "-60.90%"


def test_format_string(styler):
ctx = styler.format("{:.2f}")._translate()
ctx = styler.format("{:.2f}")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "0.00"
assert ctx["body"][0][2]["display_value"] == "-0.61"
assert ctx["body"][1][1]["display_value"] == "1.00"
assert ctx["body"][1][2]["display_value"] == "-1.23"


def test_format_callable(styler):
ctx = styler.format(lambda v: "neg" if v < 0 else "pos")._translate()
ctx = styler.format(lambda v: "neg" if v < 0 else "pos")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "pos"
assert ctx["body"][0][2]["display_value"] == "neg"
assert ctx["body"][1][1]["display_value"] == "pos"
Expand All @@ -60,17 +60,17 @@ def test_format_with_na_rep():
# GH 21527 28358
df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"])

ctx = df.style.format(None, na_rep="-")._translate()
ctx = df.style.format(None, na_rep="-")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "-"
assert ctx["body"][0][2]["display_value"] == "-"

ctx = df.style.format("{:.2%}", na_rep="-")._translate()
ctx = df.style.format("{:.2%}", na_rep="-")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "-"
assert ctx["body"][0][2]["display_value"] == "-"
assert ctx["body"][1][1]["display_value"] == "110.00%"
assert ctx["body"][1][2]["display_value"] == "120.00%"

ctx = df.style.format("{:.2%}", na_rep="-", subset=["B"])._translate()
ctx = df.style.format("{:.2%}", na_rep="-", subset=["B"])._translate(True, True)
assert ctx["body"][0][2]["display_value"] == "-"
assert ctx["body"][1][2]["display_value"] == "120.00%"

Expand All @@ -85,13 +85,13 @@ def test_format_non_numeric_na():
)

with tm.assert_produces_warning(FutureWarning):
ctx = df.style.set_na_rep("NA")._translate()
ctx = df.style.set_na_rep("NA")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "NA"
assert ctx["body"][0][2]["display_value"] == "NA"
assert ctx["body"][1][1]["display_value"] == "NA"
assert ctx["body"][1][2]["display_value"] == "NA"

ctx = df.style.format(None, na_rep="-")._translate()
ctx = df.style.format(None, na_rep="-")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "-"
assert ctx["body"][0][2]["display_value"] == "-"
assert ctx["body"][1][1]["display_value"] == "-"
Expand Down Expand Up @@ -150,19 +150,19 @@ def test_format_with_precision():
df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"])
s = Styler(df)

ctx = s.format(precision=1)._translate()
ctx = s.format(precision=1)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.0"
assert ctx["body"][0][2]["display_value"] == "2.0"
assert ctx["body"][1][1]["display_value"] == "3.2"
assert ctx["body"][1][2]["display_value"] == "4.6"

ctx = s.format(precision=2)._translate()
ctx = s.format(precision=2)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.00"
assert ctx["body"][0][2]["display_value"] == "2.01"
assert ctx["body"][1][1]["display_value"] == "3.21"
assert ctx["body"][1][2]["display_value"] == "4.57"

ctx = s.format(precision=3)._translate()
ctx = s.format(precision=3)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.000"
assert ctx["body"][0][2]["display_value"] == "2.009"
assert ctx["body"][1][1]["display_value"] == "3.212"
Expand All @@ -173,26 +173,28 @@ def test_format_subset():
df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"])
ctx = df.style.format(
{"a": "{:0.1f}", "b": "{0:.2%}"}, subset=IndexSlice[0, :]
)._translate()
)._translate(True, True)
expected = "0.1"
raw_11 = "1.123400"
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == raw_11
assert ctx["body"][0][2]["display_value"] == "12.34%"

ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, :])._translate()
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, :])._translate(True, True)
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == raw_11

ctx = df.style.format("{:0.1f}", subset=IndexSlice["a"])._translate()
ctx = df.style.format("{:0.1f}", subset=IndexSlice["a"])._translate(True, True)
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][0][2]["display_value"] == "0.123400"

ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, "a"])._translate()
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, "a"])._translate(True, True)
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == raw_11

ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate()
ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate(
True, True
)
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == "1.1"
assert ctx["body"][0][2]["display_value"] == "0.123400"
Expand All @@ -206,19 +208,19 @@ def test_format_thousands(formatter, decimal, precision):
s = DataFrame([[1000000.123456789]]).style # test float
result = s.format(
thousands="_", formatter=formatter, decimal=decimal, precision=precision
)._translate()
)._translate(True, True)
assert "1_000_000" in result["body"][0][1]["display_value"]

s = DataFrame([[1000000]]).style # test int
result = s.format(
thousands="_", formatter=formatter, decimal=decimal, precision=precision
)._translate()
)._translate(True, True)
assert "1_000_000" in result["body"][0][1]["display_value"]

s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
result = s.format(
thousands="_", formatter=formatter, decimal=decimal, precision=precision
)._translate()
)._translate(True, True)
assert "1_000_000" in result["body"][0][1]["display_value"]


Expand All @@ -229,11 +231,11 @@ def test_format_decimal(formatter, thousands, precision):
s = DataFrame([[1000000.123456789]]).style # test float
result = s.format(
decimal="_", formatter=formatter, thousands=thousands, precision=precision
)._translate()
)._translate(True, True)
assert "000_123" in result["body"][0][1]["display_value"]

s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
result = s.format(
decimal="_", formatter=formatter, thousands=thousands, precision=precision
)._translate()
)._translate(True, True)
assert "000_123" in result["body"][0][1]["display_value"]
2 changes: 1 addition & 1 deletion pandas/tests/io/formats/style/test_non_unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def test_set_td_classes_non_unique_raises(styler):


def test_hide_columns_non_unique(styler):
ctx = styler.hide_columns(["d"])._translate()
ctx = styler.hide_columns(["d"])._translate(True, True)

assert ctx["head"][0][1]["display_value"] == "c"
assert ctx["head"][0][1]["is_visible"] is True
Expand Down
Loading