Skip to content

Commit 025f892

Browse files
authored
BUG: Styler hide compatible with max_columns (#44272)
1 parent 6f75f72 commit 025f892

File tree

3 files changed

+197
-62
lines changed

3 files changed

+197
-62
lines changed

pandas/io/formats/style_render.py

+87-59
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
316316
self.columns, sparsify_cols, max_cols, self.hidden_columns
317317
)
318318

319-
clabels = self.data.columns.tolist()[:max_cols] # slice to allow trimming
319+
clabels = self.data.columns.tolist()
320320
if self.data.columns.nlevels == 1:
321321
clabels = [[x] for x in clabels]
322322
clabels = list(zip(*clabels))
@@ -339,7 +339,9 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
339339
and not all(self.hide_index_)
340340
and not self.hide_index_names
341341
):
342-
index_names_row = self._generate_index_names_row(clabels, max_cols)
342+
index_names_row = self._generate_index_names_row(
343+
clabels, max_cols, col_lengths
344+
)
343345
head.append(index_names_row)
344346

345347
return head
@@ -389,9 +391,27 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict
389391
)
390392
]
391393

392-
column_headers = []
394+
column_headers, visible_col_count = [], 0
393395
for c, value in enumerate(clabels[r]):
394396
header_element_visible = _is_visible(c, r, col_lengths)
397+
if header_element_visible:
398+
visible_col_count += col_lengths.get((r, c), 0)
399+
if visible_col_count > max_cols:
400+
# add an extra column with `...` value to indicate trimming
401+
column_headers.append(
402+
_element(
403+
"th",
404+
(
405+
f"{self.css['col_heading']} {self.css['level']}{r} "
406+
f"{self.css['col_trim']}"
407+
),
408+
"...",
409+
True,
410+
attributes="",
411+
)
412+
)
413+
break
414+
395415
header_element = _element(
396416
"th",
397417
(
@@ -422,23 +442,9 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict
422442

423443
column_headers.append(header_element)
424444

425-
if len(self.data.columns) > max_cols:
426-
# add an extra column with `...` value to indicate trimming
427-
column_headers.append(
428-
_element(
429-
"th",
430-
(
431-
f"{self.css['col_heading']} {self.css['level']}{r} "
432-
f"{self.css['col_trim']}"
433-
),
434-
"...",
435-
True,
436-
attributes="",
437-
)
438-
)
439445
return index_blanks + column_name + column_headers
440446

441-
def _generate_index_names_row(self, iter: tuple, max_cols):
447+
def _generate_index_names_row(self, iter: tuple, max_cols: int, col_lengths: dict):
442448
"""
443449
Generate the row containing index names
444450
@@ -470,22 +476,37 @@ def _generate_index_names_row(self, iter: tuple, max_cols):
470476
for c, name in enumerate(self.data.index.names)
471477
]
472478

473-
if not clabels:
474-
blank_len = 0
475-
elif len(self.data.columns) <= max_cols:
476-
blank_len = len(clabels[0])
477-
else:
478-
blank_len = len(clabels[0]) + 1 # to allow room for `...` trim col
479+
column_blanks, visible_col_count = [], 0
480+
if clabels:
481+
last_level = self.columns.nlevels - 1 # use last level since never sparsed
482+
for c, value in enumerate(clabels[last_level]):
483+
header_element_visible = _is_visible(c, last_level, col_lengths)
484+
if header_element_visible:
485+
visible_col_count += 1
486+
if visible_col_count > max_cols:
487+
column_blanks.append(
488+
_element(
489+
"th",
490+
(
491+
f"{self.css['blank']} {self.css['col']}{c} "
492+
f"{self.css['col_trim']}"
493+
),
494+
self.css["blank_value"],
495+
True,
496+
attributes="",
497+
)
498+
)
499+
break
500+
501+
column_blanks.append(
502+
_element(
503+
"th",
504+
f"{self.css['blank']} {self.css['col']}{c}",
505+
self.css["blank_value"],
506+
c not in self.hidden_columns,
507+
)
508+
)
479509

480-
column_blanks = [
481-
_element(
482-
"th",
483-
f"{self.css['blank']} {self.css['col']}{c}",
484-
self.css["blank_value"],
485-
c not in self.hidden_columns,
486-
)
487-
for c in range(blank_len)
488-
]
489510
return index_names + column_blanks
490511

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

564-
data = [
565-
_element(
566-
"td",
567-
f"{self.css['data']} {self.css['col']}{c} {self.css['row_trim']}",
568-
"...",
569-
(c not in self.hidden_columns),
570-
attributes="",
571-
)
572-
for c in range(max_cols)
573-
]
585+
data, visible_col_count = [], 0
586+
for c, _ in enumerate(self.columns):
587+
data_element_visible = c not in self.hidden_columns
588+
if data_element_visible:
589+
visible_col_count += 1
590+
if visible_col_count > max_cols:
591+
data.append(
592+
_element(
593+
"td",
594+
(
595+
f"{self.css['data']} {self.css['row_trim']} "
596+
f"{self.css['col_trim']}"
597+
),
598+
"...",
599+
True,
600+
attributes="",
601+
)
602+
)
603+
break
574604

575-
if len(self.data.columns) > max_cols:
576-
# columns are also trimmed so we add the final element
577605
data.append(
578606
_element(
579607
"td",
580-
(
581-
f"{self.css['data']} {self.css['row_trim']} "
582-
f"{self.css['col_trim']}"
583-
),
608+
f"{self.css['data']} {self.css['col']}{c} {self.css['row_trim']}",
584609
"...",
585-
True,
610+
data_element_visible,
586611
attributes="",
587612
)
588613
)
614+
589615
return index_headers + data
590616

591617
def _generate_body_row(
@@ -654,9 +680,14 @@ def _generate_body_row(
654680

655681
index_headers.append(header_element)
656682

657-
data = []
683+
data, visible_col_count = [], 0
658684
for c, value in enumerate(row_tup[1:]):
659-
if c >= max_cols:
685+
data_element_visible = (
686+
c not in self.hidden_columns and r not in self.hidden_rows
687+
)
688+
if data_element_visible:
689+
visible_col_count += 1
690+
if visible_col_count > max_cols:
660691
data.append(
661692
_element(
662693
"td",
@@ -676,9 +707,6 @@ def _generate_body_row(
676707
if (r, c) in self.cell_context:
677708
cls = " " + self.cell_context[r, c]
678709

679-
data_element_visible = (
680-
c not in self.hidden_columns and r not in self.hidden_rows
681-
)
682710
data_element = _element(
683711
"td",
684712
(
@@ -1252,15 +1280,15 @@ def _get_level_lengths(
12521280
elif j not in hidden_elements:
12531281
# then element must be part of sparsified section and is visible
12541282
visible_row_count += 1
1283+
if visible_row_count > max_index:
1284+
break # do not add a length since the render trim limit reached
12551285
if lengths[(i, last_label)] == 0:
12561286
# if previous iteration was first-of-section but hidden then offset
12571287
last_label = j
12581288
lengths[(i, last_label)] = 1
12591289
else:
1260-
# else add to previous iteration but do not extend more than max
1261-
lengths[(i, last_label)] = min(
1262-
max_index, 1 + lengths[(i, last_label)]
1263-
)
1290+
# else add to previous iteration
1291+
lengths[(i, last_label)] += 1
12641292

12651293
non_zero_lengths = {
12661294
element: length for element, length in lengths.items() if length >= 1

pandas/tests/io/formats/style/test_html.py

+96
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,99 @@ def test_hiding_index_columns_multiindex_alignment():
668668
"""
669669
)
670670
assert result == expected
671+
672+
673+
def test_hiding_index_columns_multiindex_trimming():
674+
# gh 44272
675+
df = DataFrame(np.arange(64).reshape(8, 8))
676+
df.columns = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
677+
df.index = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
678+
df.index.names, df.columns.names = ["a", "b"], ["c", "d"]
679+
styler = Styler(df, cell_ids=False, uuid_len=0)
680+
styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0)
681+
with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4):
682+
result = styler.to_html()
683+
684+
expected = dedent(
685+
"""\
686+
<style type="text/css">
687+
</style>
688+
<table id="T_">
689+
<thead>
690+
<tr>
691+
<th class="blank" >&nbsp;</th>
692+
<th class="index_name level0" >c</th>
693+
<th class="col_heading level0 col3" >1</th>
694+
<th class="col_heading level0 col4" colspan="2">2</th>
695+
<th class="col_heading level0 col6" >3</th>
696+
</tr>
697+
<tr>
698+
<th class="blank" >&nbsp;</th>
699+
<th class="index_name level1" >d</th>
700+
<th class="col_heading level1 col3" >1</th>
701+
<th class="col_heading level1 col4" >0</th>
702+
<th class="col_heading level1 col5" >1</th>
703+
<th class="col_heading level1 col6" >0</th>
704+
<th class="col_heading level1 col_trim" >...</th>
705+
</tr>
706+
<tr>
707+
<th class="index_name level0" >a</th>
708+
<th class="index_name level1" >b</th>
709+
<th class="blank col3" >&nbsp;</th>
710+
<th class="blank col4" >&nbsp;</th>
711+
<th class="blank col5" >&nbsp;</th>
712+
<th class="blank col6" >&nbsp;</th>
713+
<th class="blank col7 col_trim" >&nbsp;</th>
714+
</tr>
715+
</thead>
716+
<tbody>
717+
<tr>
718+
<th class="row_heading level0 row3" >1</th>
719+
<th class="row_heading level1 row3" >1</th>
720+
<td class="data row3 col3" >27</td>
721+
<td class="data row3 col4" >28</td>
722+
<td class="data row3 col5" >29</td>
723+
<td class="data row3 col6" >30</td>
724+
<td class="data row3 col_trim" >...</td>
725+
</tr>
726+
<tr>
727+
<th class="row_heading level0 row4" rowspan="2">2</th>
728+
<th class="row_heading level1 row4" >0</th>
729+
<td class="data row4 col3" >35</td>
730+
<td class="data row4 col4" >36</td>
731+
<td class="data row4 col5" >37</td>
732+
<td class="data row4 col6" >38</td>
733+
<td class="data row4 col_trim" >...</td>
734+
</tr>
735+
<tr>
736+
<th class="row_heading level1 row5" >1</th>
737+
<td class="data row5 col3" >43</td>
738+
<td class="data row5 col4" >44</td>
739+
<td class="data row5 col5" >45</td>
740+
<td class="data row5 col6" >46</td>
741+
<td class="data row5 col_trim" >...</td>
742+
</tr>
743+
<tr>
744+
<th class="row_heading level0 row6" >3</th>
745+
<th class="row_heading level1 row6" >0</th>
746+
<td class="data row6 col3" >51</td>
747+
<td class="data row6 col4" >52</td>
748+
<td class="data row6 col5" >53</td>
749+
<td class="data row6 col6" >54</td>
750+
<td class="data row6 col_trim" >...</td>
751+
</tr>
752+
<tr>
753+
<th class="row_heading level0 row_trim" >...</th>
754+
<th class="row_heading level1 row_trim" >...</th>
755+
<td class="data col3 row_trim" >...</td>
756+
<td class="data col4 row_trim" >...</td>
757+
<td class="data col5 row_trim" >...</td>
758+
<td class="data col6 row_trim" >...</td>
759+
<td class="data row_trim col_trim" >...</td>
760+
</tr>
761+
</tbody>
762+
</table>
763+
"""
764+
)
765+
766+
assert result == expected

pandas/tests/io/formats/style/test_style.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,6 @@ def test_render_trimming_mi():
216216
assert {"class": "data row_trim col_trim"}.items() <= ctx["body"][2][4].items()
217217
assert len(ctx["body"]) == 3 # 2 data rows + trimming row
218218

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

223220
def test_render_empty_mi():
224221
# GH 43305
@@ -1600,3 +1597,17 @@ def test_row_trimming_hide_index_mi():
16001597
assert ctx["body"][r][1]["display_value"] == val # level 1 index headers
16011598
for r, val in enumerate(["3", "4", "..."]):
16021599
assert ctx["body"][r][2]["display_value"] == val # data values
1600+
1601+
1602+
def test_col_trimming_hide_columns():
1603+
# gh 44272
1604+
df = DataFrame([[1, 2, 3, 4, 5]])
1605+
with pd.option_context("styler.render.max_columns", 2):
1606+
ctx = df.style.hide([0, 1], axis="columns")._translate(True, True)
1607+
1608+
assert len(ctx["head"][0]) == 6 # blank, [0, 1 (hidden)], [2 ,3 (visible)], + trim
1609+
for c, vals in enumerate([(1, False), (2, True), (3, True), ("...", True)]):
1610+
assert ctx["head"][0][c + 2]["value"] == vals[0]
1611+
assert ctx["head"][0][c + 2]["is_visible"] == vals[1]
1612+
1613+
assert len(ctx["body"][0]) == 6 # index + 2 hidden + 2 visible + trimming col

0 commit comments

Comments
 (0)