Skip to content

BUG: styler multiindex hiding indexes and columns alignment #43649

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

Merged
merged 58 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9743099
ignore hidden rows in loop
attack68 Sep 18, 2021
8a0253e
add latex 43644 test
attack68 Sep 18, 2021
74c418e
add latex 43644 test
attack68 Sep 18, 2021
70535c5
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Sep 21, 2021
2fbe569
clean up code
attack68 Sep 21, 2021
7903723
clean up code
attack68 Sep 21, 2021
6a2793c
row, col and level css
attack68 Sep 21, 2021
d227914
whats new
attack68 Sep 21, 2021
7ca5002
tests and user guide
attack68 Sep 21, 2021
f27f7ed
merge
attack68 Sep 22, 2021
a1000a7
merge
attack68 Sep 22, 2021
c44dcda
move private methods
attack68 Sep 22, 2021
c4c9aaa
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Sep 22, 2021
c22cf0d
refactor methods
attack68 Sep 22, 2021
0e0b46e
add test for 43703
attack68 Sep 22, 2021
1f3bbec
add test for 43703
attack68 Sep 22, 2021
021bc26
docs
attack68 Sep 22, 2021
4ba3dff
docs
attack68 Sep 22, 2021
7fee05d
more explicit test
attack68 Sep 22, 2021
baa3233
more explicit test
attack68 Sep 22, 2021
f4ad390
fix checks
attack68 Sep 23, 2021
22b03e3
fix checks
attack68 Sep 23, 2021
db214d8
fix checks
attack68 Sep 23, 2021
771d056
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Sep 24, 2021
24952ae
fix checks
attack68 Sep 24, 2021
1d47d0f
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Sep 24, 2021
566738d
fix checks
attack68 Sep 24, 2021
230138a
fix checks
attack68 Sep 24, 2021
b9ba9ea
refactor to get tests to pass
attack68 Sep 25, 2021
ea2bba1
refactor to get tests to pass
attack68 Sep 25, 2021
75de033
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Sep 25, 2021
4c34bc2
try filter
attack68 Sep 25, 2021
68ee832
docs methods
attack68 Sep 25, 2021
ca70491
correct css classes in set_table_styles
attack68 Sep 25, 2021
7a1994e
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Sep 29, 2021
91320f8
add version added
attack68 Sep 29, 2021
92c1941
add version added
attack68 Sep 29, 2021
aad0e16
checks fix
attack68 Sep 29, 2021
61b24ed
add tests allow none
attack68 Sep 29, 2021
d4c5715
fix checks
attack68 Sep 29, 2021
aa0172f
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 2, 2021
84a814f
rename `css` `css_class_names`
attack68 Oct 2, 2021
f06b727
rename `css` `css_class_names`
attack68 Oct 2, 2021
131070f
rename `css` `css_class_names`
attack68 Oct 2, 2021
a048122
update arg name
attack68 Oct 2, 2021
4931f32
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 3, 2021
e9716f2
fix line length
attack68 Oct 3, 2021
d983464
black
attack68 Oct 3, 2021
0f11a62
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Oct 5, 2021
16946b0
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 9, 2021
451d6d5
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Oct 9, 2021
6e71e8c
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 11, 2021
fd76da9
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Oct 11, 2021
042148c
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Oct 16, 2021
66fc164
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Oct 16, 2021
e8b24f0
fix merge
attack68 Oct 16, 2021
b1ba432
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Oct 17, 2021
d54779b
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Oct 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 114 additions & 94 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,119 +497,131 @@ def _translate_body(

body = []
for r, row_tup in enumerate(self.data.itertuples()):
if r >= max_rows: # used only to add a '...' trimmed row:
index_headers = [
_element(
"th",
f"{row_heading_class} level{c} {trimmed_row_class}",
"...",
not self.hide_index_[c],
attributes="",
)
for c in range(self.data.index.nlevels)
]

data = [
_element(
"td",
f"{data_class} col{c} {trimmed_row_class}",
"...",
(c not in self.hidden_columns),
attributes="",
)
for c in range(max_cols)
]
if r not in self.hidden_rows:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only added this line to this method and all code below it was indended one tab.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth splitting up translate_body to some function calls to make it simpler to grok (future)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeh agreed, i already did this once splitting _translate into header and body and now it has acquired some more stuff, like min/mix limiting etc.. will add an issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will finish making this clean up after #43686

if r >= max_rows: # used only to add a '...' trimmed row:
index_headers = [
_element(
"th",
f"{row_heading_class} level{c} {trimmed_row_class}",
"...",
not self.hide_index_[c],
attributes="",
)
for c in range(self.data.index.nlevels)
]

if len(self.data.columns) > max_cols:
# columns are also trimmed so we add the final element
data.append(
data = [
_element(
"td",
f"{data_class} {trimmed_row_class} {trimmed_col_class}",
f"{data_class} col{c} {trimmed_row_class}",
"...",
True,
(c not in self.hidden_columns),
attributes="",
)
)
for c in range(max_cols)
]

body.append(index_headers + data)
break
if len(self.data.columns) > max_cols:
# columns are also trimmed so we add the final element
data.append(
_element(
"td",
f"{data_class} {trimmed_row_class} {trimmed_col_class}",
"...",
True,
attributes="",
)
)

index_headers = []
for c, value in enumerate(rlabels[r]):
header_element = _element(
"th",
f"{row_heading_class} level{c} row{r}",
value,
_is_visible(r, c, idx_lengths) and not self.hide_index_[c],
display_value=self._display_funcs_index[(r, c)](value),
attributes=(
f'rowspan="{idx_lengths.get((c, r), 0)}"'
if idx_lengths.get((c, r), 0) > 1
else ""
),
)
body.append(index_headers + data)
break

if self.cell_ids:
header_element["id"] = f"level{c}_row{r}" # id is specified
if (r, c) in self.ctx_index and self.ctx_index[r, c]:
# always add id if a style is specified
header_element["id"] = f"level{c}_row{r}"
self.cellstyle_map_index[tuple(self.ctx_index[r, c])].append(
f"level{c}_row{r}"
index_headers = []
for c, value in enumerate(rlabels[r]):
header_element = _element(
"th",
f"{row_heading_class} level{c} row{r}",
value,
_is_visible(r, c, idx_lengths) and not self.hide_index_[c],
display_value=self._display_funcs_index[(r, c)](value),
attributes=(
f'rowspan="{idx_lengths.get((c, r), 0)}"'
if idx_lengths.get((c, r), 0) > 1
else ""
),
)

index_headers.append(header_element)
if self.cell_ids:
header_element["id"] = f"level{c}_row{r}" # id is specified
if (r, c) in self.ctx_index and self.ctx_index[r, c]:
# always add id if a style is specified
header_element["id"] = f"level{c}_row{r}"
self.cellstyle_map_index[tuple(self.ctx_index[r, c])].append(
f"level{c}_row{r}"
)

data = []
for c, value in enumerate(row_tup[1:]):
if c >= max_cols:
data.append(
_element(
"td",
f"{data_class} row{r} {trimmed_col_class}",
"...",
True,
attributes="",
index_headers.append(header_element)

data = []
for c, value in enumerate(row_tup[1:]):
if c >= max_cols:
data.append(
_element(
"td",
f"{data_class} row{r} {trimmed_col_class}",
"...",
True,
attributes="",
)
)
)
break
break

# add custom classes from cell context
cls = ""
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 and r not in self.hidden_rows),
attributes="",
display_value=self._display_funcs[(r, c)](value),
)
# add custom classes from cell context
cls = ""
if (r, c) in self.cell_context:
cls = " " + self.cell_context[r, c]

if self.cell_ids:
data_element["id"] = f"row{r}_col{c}"
if (r, c) in self.ctx and self.ctx[r, c]:
# always add id if needed due to specified style
data_element["id"] = f"row{r}_col{c}"
self.cellstyle_map[tuple(self.ctx[r, c])].append(f"row{r}_col{c}")
data_element = _element(
"td",
f"{data_class} row{r} col{c}{cls}",
value,
(c not in self.hidden_columns and r not in self.hidden_rows),
attributes="",
display_value=self._display_funcs[(r, c)](value),
)

if self.cell_ids:
data_element["id"] = f"row{r}_col{c}"
if (r, c) in self.ctx and self.ctx[r, c]:
# always add id if needed due to specified style
data_element["id"] = f"row{r}_col{c}"
self.cellstyle_map[tuple(self.ctx[r, c])].append(
f"row{r}_col{c}"
)

data.append(data_element)
data.append(data_element)

body.append(index_headers + data)
body.append(index_headers + data)
return body

def _translate_latex(self, d: dict) -> None:
r"""
Post-process the default render dict for the LaTeX template format.

Processing items included are:
- Remove hidden columns from the non-headers part of the body.
- Place cellstyles directly in td cells rather than use cellstyle_map.
- Remove hidden indexes or reinsert missing th elements if part of multiindex
or multirow sparsification (so that \multirow and \multicol work correctly).

1) Remove hidden columns from the non-headers part of the body. This is done
so that there are no repeated "&" latex separators generated from the template.
Alternatively this logic could be refactored into the template/template
parsing function.

2) Place cellstyles directly in td cells rather than use cellstyle_map. This is
necessary for a LaTeX format where styles have to be coded for each cell
specifically.

3) Remove hidden indexes or reinsert missing th elements if part of multiindex
or multirow sparsification (so that \multirow and \multicol work correctly), and
there are the correct number of cells (possibly blank) in a row.
"""
d["head"] = [
[
Expand All @@ -619,8 +631,16 @@ def _translate_latex(self, d: dict) -> None:
]
for r, row in enumerate(d["head"])
]

body = []
for r, row in enumerate(d["body"]):
index_levels = self.data.index.nlevels
for r, row in zip(
[r for r in range(len(self.data.index)) if r not in self.hidden_rows],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth a property to have this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only needed for latex, so for most cases not necessary and might add additional overhead for large HTML (usually latex tables quite a lot smaller)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took a second look here. d is dynamic so a class property won't work. think the comment is sufficient.

d["body"],
):
# note: cannot enumerate d["body"] because rows were dropped if hidden
# during _translate_body so must zip to acquire the true r-index associated
# with the ctx obj which contains the cell styles.
if all(self.hide_index_):
row_body_headers = []
else:
Expand All @@ -632,13 +652,13 @@ def _translate_latex(self, d: dict) -> None:
else "",
"cellstyle": self.ctx_index[r, c] if col["is_visible"] else [],
}
for c, col in enumerate(row)
if col["type"] == "th"
for c, col in enumerate(row[:index_levels])
if (col["type"] == "th" and not self.hide_index_[c])
]

row_body_cells = [
{**col, "cellstyle": self.ctx[r, c - self.data.index.nlevels]}
for c, col in enumerate(row)
{**col, "cellstyle": self.ctx[r, c]}
for c, col in enumerate(row[index_levels:])
if (col["is_visible"] and col["type"] == "td")
]

Expand Down
69 changes: 69 additions & 0 deletions pandas/tests/io/formats/style/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,72 @@ def test_maximums(styler_mi, rows, cols):
assert ">5</td>" in result # [[0,1], [4,5]] always visible
assert (">8</td>" in result) is not rows # first trimmed vertical element
assert (">2</td>" in result) is not cols # first trimmed horizontal element


def test_hiding_index_columns_multiindex_alignment():
# gh 43644
midx = MultiIndex.from_product(
[["i0", "j0"], ["i1"], ["i2", "j2"]], names=["i-0", "i-1", "i-2"]
)
cidx = MultiIndex.from_product(
[["c0"], ["c1", "d1"], ["c2", "d2"]], names=["c-0", "c-1", "c-2"]
)
df = DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=cidx)
styler = Styler(df, uuid_len=0)
styler.hide_index(level=1).hide_columns(level=0)
styler.hide_index([("j0", "i1", "j2")])
styler.hide_columns([("c0", "d1", "d2")])
result = styler.to_html()
expected = dedent(
"""\
<style type="text/css">
</style>
<table id="T_">
<thead>
<tr>
<th class="blank" >&nbsp;</th>
<th class="index_name level1" >c-1</th>
<th id="T__level1_col0" class="col_heading level1 col0" colspan="2">c1</th>
<th id="T__level1_col2" class="col_heading level1 col2" >d1</th>
</tr>
<tr>
<th class="blank" >&nbsp;</th>
<th class="index_name level2" >c-2</th>
<th id="T__level2_col0" class="col_heading level2 col0" >c2</th>
<th id="T__level2_col1" class="col_heading level2 col1" >d2</th>
<th id="T__level2_col2" class="col_heading level2 col2" >c2</th>
</tr>
<tr>
<th class="index_name level0" >i-0</th>
<th class="index_name level2" >i-2</th>
<th class="blank col0" >&nbsp;</th>
<th class="blank col1" >&nbsp;</th>
<th class="blank col2" >&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<th id="T__level0_row0" class="row_heading level0 row0" rowspan="2">i0</th>
<th id="T__level2_row0" class="row_heading level2 row0" >i2</th>
<td id="T__row0_col0" class="data row0 col0" >0</td>
<td id="T__row0_col1" class="data row0 col1" >1</td>
<td id="T__row0_col2" class="data row0 col2" >2</td>
</tr>
<tr>
<th id="T__level2_row1" class="row_heading level2 row1" >j2</th>
<td id="T__row1_col0" class="data row1 col0" >4</td>
<td id="T__row1_col1" class="data row1 col1" >5</td>
<td id="T__row1_col2" class="data row1 col2" >6</td>
</tr>
<tr>
<th id="T__level0_row2" class="row_heading level0 row2" >j0</th>
<th id="T__level2_row2" class="row_heading level2 row2" >i2</th>
<td id="T__row2_col0" class="data row2 col0" >8</td>
<td id="T__row2_col1" class="data row2 col1" >9</td>
<td id="T__row2_col2" class="data row2 col2" >10</td>
</tr>
</tbody>
</table>
"""
)
assert result == expected
42 changes: 21 additions & 21 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,17 +1212,15 @@ def test_hide_columns_index_mult_levels(self):
assert ctx["body"][1][2]["is_visible"]
assert ctx["body"][1][2]["display_value"] == "3"

# hide top row level, which hides both rows
# hide top row level, which hides both rows so body empty
ctx = df.style.hide_index("a")._translate(True, True)
for i in [0, 1, 2, 3]:
assert not ctx["body"][0][i]["is_visible"]
assert not ctx["body"][1][i]["is_visible"]
assert ctx["body"] == []

# hide first row only
ctx = df.style.hide_index(("a", 0))._translate(True, True)
for i in [0, 1, 2, 3]:
assert not ctx["body"][0][i]["is_visible"]
assert ctx["body"][1][i]["is_visible"]
assert "row1" in ctx["body"][0][i]["class"] # row0 not included in body
assert ctx["body"][0][i]["is_visible"]

def test_pipe(self):
def set_caption_from_template(styler, a, b):
Expand Down Expand Up @@ -1479,25 +1477,27 @@ def test_caption_raises(mi_styler, caption):
mi_styler.set_caption(caption)


@pytest.mark.parametrize("axis", ["index", "columns"])
def test_hiding_headers_over_axis_no_sparsify(axis):
def test_hiding_headers_over_index_no_sparsify():
# GH 43464
midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
df = DataFrame(
9,
index=midx if axis == "index" else [0],
columns=midx if axis == "columns" else [0],
)
df = DataFrame(9, index=midx, columns=[0])
ctx = df.style._translate(False, False)
assert len(ctx["body"]) == 6
ctx = df.style.hide_index((1, "a"))._translate(False, False)
assert len(ctx["body"]) == 4
assert "row2" in ctx["body"][0][0]["class"]

styler = getattr(df.style, f"hide_{axis}")((1, "a"))
ctx = styler._translate(False, False)

if axis == "columns": # test column headers
for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
assert ctx["head"][ix[0]][ix[1]]["is_visible"] is False
if axis == "index": # test row headers
for ix in [(0, 0), (0, 1), (1, 0), (1, 1)]:
assert ctx["body"][ix[0]][ix[1]]["is_visible"] is False
def test_hiding_headers_over_columns_no_sparsify():
# GH 43464
midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
df = DataFrame(9, columns=midx, index=[0])
ctx = df.style._translate(False, False)
for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
assert ctx["head"][ix[0]][ix[1]]["is_visible"] is True
ctx = df.style.hide_columns((1, "a"))._translate(False, False)
for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
assert ctx["head"][ix[0]][ix[1]]["is_visible"] is False


def test_get_level_lengths_mi_hidden():
Expand Down
Loading