diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py
index 82f57b71caebf..0b7279d796464 100644
--- a/pandas/io/formats/style_render.py
+++ b/pandas/io/formats/style_render.py
@@ -173,15 +173,6 @@ def _translate(self):
BLANK_CLASS = "blank"
BLANK_VALUE = " "
- # mapping variables
- ctx = self.ctx # td css styles from apply() and applymap()
- cell_context = self.cell_context # td css classes from set_td_classes()
- cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(list)
-
- # copied attributes
- hidden_index = self.hidden_index
- hidden_columns = self.hidden_columns
-
# construct render dict
d = {
"uuid": self.uuid,
@@ -189,165 +180,185 @@ def _translate(self):
"caption": self.caption,
}
+ head = self._translate_header(
+ BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS
+ )
+ d.update({"head": head})
+
+ self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
+ list
+ )
+ body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS)
+ d.update({"body": body})
+
+ cellstyle: list[dict[str, CSSList | list[str]]] = [
+ {"props": list(props), "selectors": selectors}
+ for props, selectors in self.cellstyle_map.items()
+ ]
+ d.update({"cellstyle": cellstyle})
+
+ table_attr = self.table_attributes
+ use_mathjax = get_option("display.html.use_mathjax")
+ if not use_mathjax:
+ table_attr = table_attr or ""
+ if 'class="' in table_attr:
+ table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
+ else:
+ table_attr += ' class="tex2jax_ignore"'
+ d.update({"table_attributes": table_attr})
+
+ if self.tooltips:
+ d = self.tooltips._translate(self.data, self.uuid, d)
+
+ return d
+
+ def _translate_header(
+ self, blank_class, blank_value, index_name_class, col_heading_class
+ ):
+ """
+ Build each
within table , using the structure:
+ +----------------------------+---------------+---------------------------+
+ | index_blanks ... | column_name_0 | column_headers (level_0) |
+ 1) | .. | .. | .. |
+ | index_blanks ... | column_name_n | column_headers (level_n) |
+ +----------------------------+---------------+---------------------------+
+ 2) | index_names (level_0 to level_n) ... | column_blanks ... |
+ +----------------------------+---------------+---------------------------+
+ """
# for sparsifying a MultiIndex
- idx_lengths = _get_level_lengths(self.index)
- col_lengths = _get_level_lengths(self.columns, hidden_columns)
+ col_lengths = _get_level_lengths(self.columns, self.hidden_columns)
- n_rlvls = self.data.index.nlevels
- n_clvls = self.data.columns.nlevels
- rlabels = self.data.index.tolist()
clabels = self.data.columns.tolist()
-
- if n_rlvls == 1:
- rlabels = [[x] for x in rlabels]
- if n_clvls == 1:
+ if self.data.columns.nlevels == 1:
clabels = [[x] for x in clabels]
clabels = list(zip(*clabels))
head = []
- for r in range(n_clvls):
- # Blank for Index columns...
- row_es = [
- {
- "type": "th",
- "value": BLANK_VALUE,
- "display_value": BLANK_VALUE,
- "is_visible": not hidden_index,
- "class": " ".join([BLANK_CLASS]),
- }
- ] * (n_rlvls - 1)
-
- # ... except maybe the last for columns.names
+ # 1) column headers
+ for r in range(self.data.columns.nlevels):
+ index_blanks = [
+ _element("th", blank_class, blank_value, not self.hidden_index)
+ ] * (self.data.index.nlevels - 1)
+
name = self.data.columns.names[r]
- cs = [
- BLANK_CLASS if name is None else INDEX_NAME_CLASS,
- f"level{r}",
+ column_name = [
+ _element(
+ "th",
+ f"{blank_class if name is None else index_name_class} level{r}",
+ name if name is not None else blank_value,
+ not self.hidden_index,
+ )
]
- name = BLANK_VALUE if name is None else name
- row_es.append(
- {
- "type": "th",
- "value": name,
- "display_value": name,
- "class": " ".join(cs),
- "is_visible": not hidden_index,
- }
- )
if clabels:
- for c, value in enumerate(clabels[r]):
- es = {
- "type": "th",
- "value": value,
- "display_value": value,
- "class": f"{COL_HEADING_CLASS} level{r} col{c}",
- "is_visible": _is_visible(c, r, col_lengths),
- }
- colspan = col_lengths.get((r, c), 0)
- if colspan > 1:
- es["attributes"] = f'colspan="{colspan}"'
- row_es.append(es)
- head.append(row_es)
+ column_headers = [
+ _element(
+ "th",
+ f"{col_heading_class} level{r} col{c}",
+ value,
+ _is_visible(c, r, col_lengths),
+ attributes=(
+ f'colspan="{col_lengths.get((r, c), 0)}"'
+ if col_lengths.get((r, c), 0) > 1
+ else ""
+ ),
+ )
+ for c, value in enumerate(clabels[r])
+ ]
+ head.append(index_blanks + column_name + column_headers)
+ # 2) index names
if (
self.data.index.names
and com.any_not_none(*self.data.index.names)
- and not hidden_index
+ and not self.hidden_index
):
- index_header_row = []
+ index_names = [
+ _element(
+ "th",
+ f"{index_name_class} level{c}",
+ blank_value if name is None else name,
+ True,
+ )
+ for c, name in enumerate(self.data.index.names)
+ ]
- for c, name in enumerate(self.data.index.names):
- cs = [INDEX_NAME_CLASS, f"level{c}"]
- name = "" if name is None else name
- index_header_row.append(
- {"type": "th", "value": name, "class": " ".join(cs)}
+ column_blanks = [
+ _element(
+ "th",
+ f"{blank_class} col{c}",
+ blank_value,
+ c not in self.hidden_columns,
)
+ for c in range(len(clabels[0]))
+ ]
+ head.append(index_names + column_blanks)
- index_header_row.extend(
- [
- {
- "type": "th",
- "value": BLANK_VALUE,
- "class": " ".join([BLANK_CLASS, f"col{c}"]),
- }
- for c in range(len(clabels[0]))
- if c not in hidden_columns
- ]
- )
+ return head
- head.append(index_header_row)
- d.update({"head": head})
+ def _translate_body(self, data_class, row_heading_class):
+ """
+ Build each
in table in the following format:
+ +--------------------------------------------+---------------------------+
+ | index_header_0 ... index_header_n | data_by_column |
+ +--------------------------------------------+---------------------------+
+
+ Also add elements to the cellstyle_map for more efficient grouped elements in
+ block
+ """
+ # for sparsifying a MultiIndex
+ idx_lengths = _get_level_lengths(self.index)
+
+ rlabels = self.data.index.tolist()
+ if self.data.index.nlevels == 1:
+ rlabels = [[x] for x in rlabels]
body = []
for r, row_tup in enumerate(self.data.itertuples()):
- row_es = []
- for c, value in enumerate(rlabels[r]):
- rid = [
- ROW_HEADING_CLASS,
- f"level{c}",
- f"row{r}",
- ]
- es = {
- "type": "th",
- "is_visible": (_is_visible(r, c, idx_lengths) and not hidden_index),
- "value": value,
- "display_value": value,
- "id": "_".join(rid[1:]),
- "class": " ".join(rid),
- }
- rowspan = idx_lengths.get((c, r), 0)
- if rowspan > 1:
- es["attributes"] = f'rowspan="{rowspan}"'
- row_es.append(es)
+ index_headers = [
+ _element(
+ "th",
+ f"{row_heading_class} level{c} row{r}",
+ value,
+ (_is_visible(r, c, idx_lengths) and not self.hidden_index),
+ id=f"level{c}_row{r}",
+ attributes=(
+ f'rowspan="{idx_lengths.get((c, r), 0)}"'
+ if idx_lengths.get((c, r), 0) > 1
+ else ""
+ ),
+ )
+ for c, value in enumerate(rlabels[r])
+ ]
+ data = []
for c, value in enumerate(row_tup[1:]):
- formatter = self._display_funcs[(r, c)]
- row_dict = {
- "type": "td",
- "value": value,
- "display_value": formatter(value),
- "is_visible": (c not in hidden_columns),
- "attributes": "",
- }
-
- # only add an id if the cell has a style
- props: CSSList = []
- if self.cell_ids or (r, c) in ctx:
- row_dict["id"] = f"row{r}_col{c}"
- props.extend(ctx[r, c])
-
# add custom classes from cell context
cls = ""
- if (r, c) in cell_context:
- cls = " " + cell_context[r, c]
- row_dict["class"] = f"{DATA_CLASS} row{r} col{c}{cls}"
-
- row_es.append(row_dict)
- if props: # (), [] won't be in cellstyle_map, cellstyle respectively
- cellstyle_map[tuple(props)].append(f"row{r}_col{c}")
- body.append(row_es)
- d.update({"body": body})
-
- cellstyle: list[dict[str, CSSList | list[str]]] = [
- {"props": list(props), "selectors": selectors}
- for props, selectors in cellstyle_map.items()
- ]
- d.update({"cellstyle": cellstyle})
+ if (r, c) in self.cell_context:
+ cls = " " + self.cell_context[r, c]
+
+ data_element = _element(
+ "td",
+ f"{data_class} row{r} col{c}{cls}",
+ value,
+ (c not in self.hidden_columns),
+ attributes="",
+ display_value=self._display_funcs[(r, c)](value),
+ )
- table_attr = self.table_attributes
- use_mathjax = get_option("display.html.use_mathjax")
- if not use_mathjax:
- table_attr = table_attr or ""
- if 'class="' in table_attr:
- table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
- else:
- table_attr += ' class="tex2jax_ignore"'
- d.update({"table_attributes": table_attr})
+ # only add an id if the cell has a style
+ if self.cell_ids or (r, c) in self.ctx:
+ data_element["id"] = f"row{r}_col{c}"
+ if (r, c) in self.ctx and self.ctx[r, c]: # only add if non-empty
+ self.cellstyle_map[tuple(self.ctx[r, c])].append(
+ f"row{r}_col{c}"
+ )
- if self.tooltips:
- d = self.tooltips._translate(self.data, self.uuid, d)
+ data.append(data_element)
- return d
+ body.append(index_headers + data)
+ return body
def format(
self,
@@ -502,6 +513,27 @@ def format(
return self
+def _element(
+ html_element: str,
+ html_class: str,
+ value: Any,
+ is_visible: bool,
+ **kwargs,
+) -> dict:
+ """
+ Template to return container with information for a | or | element.
+ """
+ if "display_value" not in kwargs:
+ kwargs["display_value"] = value
+ return {
+ "type": html_element,
+ "value": value,
+ "class": html_class,
+ "is_visible": is_visible,
+ **kwargs,
+ }
+
+
def _get_level_lengths(index, hidden_elements=None):
"""
Given an index, find the level length for each element.
diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py
index 56e9581f8785a..25a7eb36d6b48 100644
--- a/pandas/tests/io/formats/style/test_style.py
+++ b/pandas/tests/io/formats/style/test_style.py
@@ -273,6 +273,7 @@ def test_empty_index_name_doesnt_display(self):
"type": "th",
"value": "A",
"is_visible": True,
+ "attributes": "",
},
{
"class": "col_heading level0 col1",
@@ -280,6 +281,7 @@ def test_empty_index_name_doesnt_display(self):
"type": "th",
"value": "B",
"is_visible": True,
+ "attributes": "",
},
{
"class": "col_heading level0 col2",
@@ -287,6 +289,7 @@ def test_empty_index_name_doesnt_display(self):
"type": "th",
"value": "C",
"is_visible": True,
+ "attributes": "",
},
]
]
@@ -295,6 +298,7 @@ def test_empty_index_name_doesnt_display(self):
def test_index_name(self):
# https://github.com/pandas-dev/pandas/issues/11655
+ # TODO: this test can be minimised to address the test more directly
df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
result = df.set_index("A").style._translate()
@@ -313,6 +317,7 @@ def test_index_name(self):
"value": "B",
"display_value": "B",
"is_visible": True,
+ "attributes": "",
},
{
"class": "col_heading level0 col1",
@@ -320,12 +325,31 @@ def test_index_name(self):
"value": "C",
"display_value": "C",
"is_visible": True,
+ "attributes": "",
},
],
[
- {"class": "index_name level0", "type": "th", "value": "A"},
- {"class": "blank col0", "type": "th", "value": self.blank_value},
- {"class": "blank col1", "type": "th", "value": self.blank_value},
+ {
+ "class": "index_name level0",
+ "type": "th",
+ "value": "A",
+ "is_visible": True,
+ "display_value": "A",
+ },
+ {
+ "class": "blank col0",
+ "type": "th",
+ "value": self.blank_value,
+ "is_visible": True,
+ "display_value": self.blank_value,
+ },
+ {
+ "class": "blank col1",
+ "type": "th",
+ "value": self.blank_value,
+ "is_visible": True,
+ "display_value": self.blank_value,
+ },
],
]
@@ -333,6 +357,7 @@ def test_index_name(self):
def test_multiindex_name(self):
# https://github.com/pandas-dev/pandas/issues/11655
+ # TODO: this test can be minimised to address the test more directly
df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
result = df.set_index(["A", "B"]).style._translate()
@@ -358,12 +383,31 @@ def test_multiindex_name(self):
"value": "C",
"display_value": "C",
"is_visible": True,
+ "attributes": "",
},
],
[
- {"class": "index_name level0", "type": "th", "value": "A"},
- {"class": "index_name level1", "type": "th", "value": "B"},
- {"class": "blank col0", "type": "th", "value": self.blank_value},
+ {
+ "class": "index_name level0",
+ "type": "th",
+ "value": "A",
+ "is_visible": True,
+ "display_value": "A",
+ },
+ {
+ "class": "index_name level1",
+ "type": "th",
+ "value": "B",
+ "is_visible": True,
+ "display_value": "B",
+ },
+ {
+ "class": "blank col0",
+ "type": "th",
+ "value": self.blank_value,
+ "is_visible": True,
+ "display_value": self.blank_value,
+ },
],
]
@@ -838,7 +882,7 @@ def test_mi_sparse(self):
"class": "row_heading level0 row0",
"id": "level0_row0",
}
- tm.assert_dict_equal(body_0, expected_0)
+ assert body_0 == expected_0
body_1 = result["body"][0][1]
expected_1 = {
@@ -848,8 +892,9 @@ def test_mi_sparse(self):
"type": "th",
"class": "row_heading level1 row0",
"id": "level1_row0",
+ "attributes": "",
}
- tm.assert_dict_equal(body_1, expected_1)
+ assert body_1 == expected_1
body_10 = result["body"][1][0]
expected_10 = {
@@ -859,8 +904,9 @@ def test_mi_sparse(self):
"type": "th",
"class": "row_heading level0 row1",
"id": "level0_row1",
+ "attributes": "",
}
- tm.assert_dict_equal(body_10, expected_10)
+ assert body_10 == expected_10
head = result["head"][0]
expected = [
@@ -884,21 +930,26 @@ def test_mi_sparse(self):
"value": "A",
"is_visible": True,
"display_value": "A",
+ "attributes": "",
},
]
assert head == expected
def test_mi_sparse_disabled(self):
+ df = DataFrame(
+ {"A": [1, 2]}, index=pd.MultiIndex.from_arrays([["a", "a"], [0, 1]])
+ )
+ result = df.style._translate()["body"]
+ assert 'rowspan="2"' in result[0][0]["attributes"]
+ assert result[1][0]["is_visible"] is False
+
with pd.option_context("display.multi_sparse", False):
- df = DataFrame(
- {"A": [1, 2]}, index=pd.MultiIndex.from_arrays([["a", "a"], [0, 1]])
- )
- result = df.style._translate()
- body = result["body"]
- for row in body:
- assert "attributes" not in row[0]
+ result = df.style._translate()["body"]
+ assert 'rowspan="2"' not in result[0][0]["attributes"]
+ assert result[1][0]["is_visible"] is True
def test_mi_sparse_index_names(self):
+ # TODO this test is verbose can be minimised to more directly target test
df = DataFrame(
{"A": [1, 2]},
index=pd.MultiIndex.from_arrays(
@@ -908,14 +959,33 @@ def test_mi_sparse_index_names(self):
result = df.style._translate()
head = result["head"][1]
expected = [
- {"class": "index_name level0", "value": "idx_level_0", "type": "th"},
- {"class": "index_name level1", "value": "idx_level_1", "type": "th"},
- {"class": "blank col0", "value": self.blank_value, "type": "th"},
+ {
+ "class": "index_name level0",
+ "value": "idx_level_0",
+ "type": "th",
+ "is_visible": True,
+ "display_value": "idx_level_0",
+ },
+ {
+ "class": "index_name level1",
+ "value": "idx_level_1",
+ "type": "th",
+ "is_visible": True,
+ "display_value": "idx_level_1",
+ },
+ {
+ "class": "blank col0",
+ "value": self.blank_value,
+ "type": "th",
+ "is_visible": True,
+ "display_value": self.blank_value,
+ },
]
assert head == expected
def test_mi_sparse_column_names(self):
+ # TODO this test is verbose - could be minimised
df = DataFrame(
np.arange(16).reshape(4, 4),
index=pd.MultiIndex.from_arrays(
@@ -949,6 +1019,7 @@ def test_mi_sparse_column_names(self):
"is_visible": True,
"type": "th",
"value": 1,
+ "attributes": "",
},
{
"class": "col_heading level1 col1",
@@ -956,6 +1027,7 @@ def test_mi_sparse_column_names(self):
"is_visible": True,
"type": "th",
"value": 0,
+ "attributes": "",
},
{
"class": "col_heading level1 col2",
@@ -963,6 +1035,7 @@ def test_mi_sparse_column_names(self):
"is_visible": True,
"type": "th",
"value": 1,
+ "attributes": "",
},
{
"class": "col_heading level1 col3",
@@ -970,6 +1043,7 @@ def test_mi_sparse_column_names(self):
"is_visible": True,
"type": "th",
"value": 0,
+ "attributes": "",
},
]
assert head == expected