Skip to content

BUG: Styler hide compatible with max_columns #44272

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 10 commits into from
Nov 5, 2021
146 changes: 87 additions & 59 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
self.columns, sparsify_cols, max_cols, self.hidden_columns
)

clabels = self.data.columns.tolist()[:max_cols] # slice to allow trimming
clabels = self.data.columns.tolist()
if self.data.columns.nlevels == 1:
clabels = [[x] for x in clabels]
clabels = list(zip(*clabels))
Expand All @@ -339,7 +339,9 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
and not all(self.hide_index_)
and not self.hide_index_names
):
index_names_row = self._generate_index_names_row(clabels, max_cols)
index_names_row = self._generate_index_names_row(
clabels, max_cols, col_lengths
)
head.append(index_names_row)

return head
Expand Down Expand Up @@ -389,9 +391,27 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict
)
]

column_headers = []
column_headers, visible_col_count = [], 0
for c, value in enumerate(clabels[r]):
header_element_visible = _is_visible(c, r, col_lengths)
if header_element_visible:
visible_col_count += col_lengths.get((r, c), 0)
if visible_col_count > max_cols:
# add an extra column with `...` value to indicate trimming
column_headers.append(
_element(
"th",
(
f"{self.css['col_heading']} {self.css['level']}{r} "
f"{self.css['col_trim']}"
),
"...",
True,
attributes="",
)
)
break

header_element = _element(
"th",
(
Expand Down Expand Up @@ -422,23 +442,9 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict

column_headers.append(header_element)

if len(self.data.columns) > max_cols:
# add an extra column with `...` value to indicate trimming
column_headers.append(
_element(
"th",
(
f"{self.css['col_heading']} {self.css['level']}{r} "
f"{self.css['col_trim']}"
),
"...",
True,
attributes="",
)
)
return index_blanks + column_name + column_headers

def _generate_index_names_row(self, iter: tuple, max_cols):
def _generate_index_names_row(self, iter: tuple, max_cols: int, col_lengths: dict):
"""
Generate the row containing index names

Expand Down Expand Up @@ -470,22 +476,37 @@ def _generate_index_names_row(self, iter: tuple, max_cols):
for c, name in enumerate(self.data.index.names)
]

if not clabels:
blank_len = 0
elif len(self.data.columns) <= max_cols:
blank_len = len(clabels[0])
else:
blank_len = len(clabels[0]) + 1 # to allow room for `...` trim col
column_blanks, visible_col_count = [], 0
if clabels:
last_level = self.columns.nlevels - 1 # use last level since never sparsed
for c, value in enumerate(clabels[last_level]):
header_element_visible = _is_visible(c, last_level, col_lengths)
if header_element_visible:
visible_col_count += 1
if visible_col_count > max_cols:
column_blanks.append(
_element(
"th",
(
f"{self.css['blank']} {self.css['col']}{c} "
f"{self.css['col_trim']}"
),
self.css["blank_value"],
True,
attributes="",
)
)
break

column_blanks.append(
_element(
"th",
f"{self.css['blank']} {self.css['col']}{c}",
self.css["blank_value"],
c not in self.hidden_columns,
)
)

column_blanks = [
_element(
"th",
f"{self.css['blank']} {self.css['col']}{c}",
self.css["blank_value"],
c not in self.hidden_columns,
)
for c in range(blank_len)
]
return index_names + column_blanks

def _translate_body(self, sparsify_index: bool, max_rows: int, max_cols: int):
Expand Down Expand Up @@ -561,31 +582,36 @@ def _generate_trimmed_row(self, max_cols: int) -> list:
for c in range(self.data.index.nlevels)
]

data = [
_element(
"td",
f"{self.css['data']} {self.css['col']}{c} {self.css['row_trim']}",
"...",
(c not in self.hidden_columns),
attributes="",
)
for c in range(max_cols)
]
data, visible_col_count = [], 0
for c, _ in enumerate(self.columns):
data_element_visible = c not in self.hidden_columns
if data_element_visible:
visible_col_count += 1
if visible_col_count > max_cols:
data.append(
_element(
"td",
(
f"{self.css['data']} {self.css['row_trim']} "
f"{self.css['col_trim']}"
),
"...",
True,
attributes="",
)
)
break

if len(self.data.columns) > max_cols:
# columns are also trimmed so we add the final element
data.append(
_element(
"td",
(
f"{self.css['data']} {self.css['row_trim']} "
f"{self.css['col_trim']}"
),
f"{self.css['data']} {self.css['col']}{c} {self.css['row_trim']}",
"...",
True,
data_element_visible,
attributes="",
)
)

return index_headers + data

def _generate_body_row(
Expand Down Expand Up @@ -654,9 +680,14 @@ def _generate_body_row(

index_headers.append(header_element)

data = []
data, visible_col_count = [], 0
for c, value in enumerate(row_tup[1:]):
if c >= max_cols:
data_element_visible = (
c not in self.hidden_columns and r not in self.hidden_rows
)
if data_element_visible:
visible_col_count += 1
if visible_col_count > max_cols:
data.append(
_element(
"td",
Expand All @@ -676,9 +707,6 @@ def _generate_body_row(
if (r, c) in self.cell_context:
cls = " " + self.cell_context[r, c]

data_element_visible = (
c not in self.hidden_columns and r not in self.hidden_rows
)
data_element = _element(
"td",
(
Expand Down Expand Up @@ -1252,15 +1280,15 @@ def _get_level_lengths(
elif j not in hidden_elements:
# then element must be part of sparsified section and is visible
visible_row_count += 1
if visible_row_count > max_index:
break # do not add a length since the render trim limit reached
if lengths[(i, last_label)] == 0:
# if previous iteration was first-of-section but hidden then offset
last_label = j
lengths[(i, last_label)] = 1
else:
# else add to previous iteration but do not extend more than max
lengths[(i, last_label)] = min(
max_index, 1 + lengths[(i, last_label)]
)
# else add to previous iteration
lengths[(i, last_label)] += 1

non_zero_lengths = {
element: length for element, length in lengths.items() if length >= 1
Expand Down
96 changes: 96 additions & 0 deletions pandas/tests/io/formats/style/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,99 @@ def test_hiding_index_columns_multiindex_alignment():
"""
)
assert result == expected


def test_hiding_index_columns_multiindex_trimming():
# gh 44272
df = DataFrame(np.arange(64).reshape(8, 8))
df.columns = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
df.index = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
df.index.names, df.columns.names = ["a", "b"], ["c", "d"]
styler = Styler(df, cell_ids=False, uuid_len=0)
styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0)
with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4):
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 level0" >c</th>
<th class="col_heading level0 col3" >1</th>
<th class="col_heading level0 col4" colspan="2">2</th>
<th class="col_heading level0 col6" >3</th>
</tr>
<tr>
<th class="blank" >&nbsp;</th>
<th class="index_name level1" >d</th>
<th class="col_heading level1 col3" >1</th>
<th class="col_heading level1 col4" >0</th>
<th class="col_heading level1 col5" >1</th>
<th class="col_heading level1 col6" >0</th>
<th class="col_heading level1 col_trim" >...</th>
</tr>
<tr>
<th class="index_name level0" >a</th>
<th class="index_name level1" >b</th>
<th class="blank col3" >&nbsp;</th>
<th class="blank col4" >&nbsp;</th>
<th class="blank col5" >&nbsp;</th>
<th class="blank col6" >&nbsp;</th>
<th class="blank col7 col_trim" >&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<th class="row_heading level0 row3" >1</th>
<th class="row_heading level1 row3" >1</th>
<td class="data row3 col3" >27</td>
<td class="data row3 col4" >28</td>
<td class="data row3 col5" >29</td>
<td class="data row3 col6" >30</td>
<td class="data row3 col_trim" >...</td>
</tr>
<tr>
<th class="row_heading level0 row4" rowspan="2">2</th>
<th class="row_heading level1 row4" >0</th>
<td class="data row4 col3" >35</td>
<td class="data row4 col4" >36</td>
<td class="data row4 col5" >37</td>
<td class="data row4 col6" >38</td>
<td class="data row4 col_trim" >...</td>
</tr>
<tr>
<th class="row_heading level1 row5" >1</th>
<td class="data row5 col3" >43</td>
<td class="data row5 col4" >44</td>
<td class="data row5 col5" >45</td>
<td class="data row5 col6" >46</td>
<td class="data row5 col_trim" >...</td>
</tr>
<tr>
<th class="row_heading level0 row6" >3</th>
<th class="row_heading level1 row6" >0</th>
<td class="data row6 col3" >51</td>
<td class="data row6 col4" >52</td>
<td class="data row6 col5" >53</td>
<td class="data row6 col6" >54</td>
<td class="data row6 col_trim" >...</td>
</tr>
<tr>
<th class="row_heading level0 row_trim" >...</th>
<th class="row_heading level1 row_trim" >...</th>
<td class="data col3 row_trim" >...</td>
<td class="data col4 row_trim" >...</td>
<td class="data col5 row_trim" >...</td>
<td class="data col6 row_trim" >...</td>
<td class="data row_trim col_trim" >...</td>
</tr>
</tbody>
</table>
"""
)

assert result == expected
17 changes: 14 additions & 3 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,6 @@ def test_render_trimming_mi():
assert {"class": "data row_trim col_trim"}.items() <= ctx["body"][2][4].items()
assert len(ctx["body"]) == 3 # 2 data rows + trimming row

assert len(ctx["head"][0]) == 5 # 2 indexes + 2 column headers + trimming col
assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items()


def test_render_empty_mi():
# GH 43305
Expand Down Expand Up @@ -1600,3 +1597,17 @@ def test_row_trimming_hide_index_mi():
assert ctx["body"][r][1]["display_value"] == val # level 1 index headers
for r, val in enumerate(["3", "4", "..."]):
assert ctx["body"][r][2]["display_value"] == val # data values


def test_col_trimming_hide_columns():
# gh 44272
df = DataFrame([[1, 2, 3, 4, 5]])
with pd.option_context("styler.render.max_columns", 2):
ctx = df.style.hide([0, 1], axis="columns")._translate(True, True)

assert len(ctx["head"][0]) == 6 # blank, [0, 1 (hidden)], [2 ,3 (visible)], + trim
for c, vals in enumerate([(1, False), (2, True), (3, True), ("...", True)]):
assert ctx["head"][0][c + 2]["value"] == vals[0]
assert ctx["head"][0][c + 2]["is_visible"] == vals[1]

assert len(ctx["body"][0]) == 6 # index + 2 hidden + 2 visible + trimming col