Skip to content

Commit cb52dfd

Browse files
Backport PR #42799: BUG: Styler.set_sticky fix the names rows 2/2 (#42928)
Co-authored-by: attack68 <[email protected]>
1 parent 37f1c93 commit cb52dfd

File tree

3 files changed

+122
-139
lines changed

3 files changed

+122
-139
lines changed

doc/source/whatsnew/v1.3.2.rst

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Bug fixes
3434
- 1D slices over extension types turn into N-dimensional slices over ExtensionArrays (:issue:`42430`)
3535
- Fixed bug in :meth:`Series.rolling` and :meth:`DataFrame.rolling` not calculating window bounds correctly for the first row when ``center=True`` and ``window`` is an offset that covers all the rows (:issue:`42753`)
3636
- :meth:`.Styler.hide_columns` now hides the index name header row as well as column headers (:issue:`42101`)
37+
- :meth:`.Styler.set_sticky` has amended CSS to control the column/index names and ensure the correct sticky positions (:issue:`42537`)
3738
- Bug in de-serializing datetime indexes in PYTHONOPTIMIZED mode (:issue:`42866`)
3839
-
3940

pandas/io/formats/style.py

+59-26
Original file line numberDiff line numberDiff line change
@@ -1473,24 +1473,24 @@ def set_sticky(
14731473
may produce strange behaviour due to CSS controls with missing elements.
14741474
"""
14751475
if axis in [0, "index"]:
1476-
axis, obj, tag, pos = 0, self.data.index, "tbody", "left"
1476+
axis, obj = 0, self.data.index
14771477
pixel_size = 75 if not pixel_size else pixel_size
14781478
elif axis in [1, "columns"]:
1479-
axis, obj, tag, pos = 1, self.data.columns, "thead", "top"
1479+
axis, obj = 1, self.data.columns
14801480
pixel_size = 25 if not pixel_size else pixel_size
14811481
else:
14821482
raise ValueError("`axis` must be one of {0, 1, 'index', 'columns'}")
14831483

1484+
props = "position:sticky; background-color:white;"
14841485
if not isinstance(obj, pd.MultiIndex):
14851486
# handling MultiIndexes requires different CSS
1486-
props = "position:sticky; background-color:white;"
14871487

14881488
if axis == 1:
14891489
# stick the first <tr> of <head> and, if index names, the second <tr>
14901490
# if self._hide_columns then no <thead><tr> here will exist: no conflict
14911491
styles: CSSStyles = [
14921492
{
1493-
"selector": "thead tr:first-child",
1493+
"selector": "thead tr:nth-child(1) th",
14941494
"props": props + "top:0px; z-index:2;",
14951495
}
14961496
]
@@ -1500,7 +1500,7 @@ def set_sticky(
15001500
)
15011501
styles.append(
15021502
{
1503-
"selector": "thead tr:nth-child(2)",
1503+
"selector": "thead tr:nth-child(2) th",
15041504
"props": props
15051505
+ f"top:{pixel_size}px; z-index:2; height:{pixel_size}px; ",
15061506
}
@@ -1511,34 +1511,67 @@ def set_sticky(
15111511
# but <th> will exist in <thead>: conflict with initial element
15121512
styles = [
15131513
{
1514-
"selector": "tr th:first-child",
1514+
"selector": "thead tr th:nth-child(1)",
1515+
"props": props + "left:0px; z-index:3 !important;",
1516+
},
1517+
{
1518+
"selector": "tbody tr th:nth-child(1)",
15151519
"props": props + "left:0px; z-index:1;",
1516-
}
1520+
},
15171521
]
15181522

1519-
return self.set_table_styles(styles, overwrite=False)
1520-
15211523
else:
1524+
# handle the MultiIndex case
15221525
range_idx = list(range(obj.nlevels))
1526+
levels = sorted(levels) if levels else range_idx
15231527

1524-
levels = sorted(levels) if levels else range_idx
1525-
for i, level in enumerate(levels):
1526-
self.set_table_styles(
1527-
[
1528-
{
1529-
"selector": f"{tag} th.level{level}",
1530-
"props": f"position: sticky; "
1531-
f"{pos}: {i * pixel_size}px; "
1532-
f"{f'height: {pixel_size}px; ' if axis == 1 else ''}"
1533-
f"{f'min-width: {pixel_size}px; ' if axis == 0 else ''}"
1534-
f"{f'max-width: {pixel_size}px; ' if axis == 0 else ''}"
1535-
f"background-color: white;",
1536-
}
1537-
],
1538-
overwrite=False,
1539-
)
1528+
if axis == 1:
1529+
styles = []
1530+
for i, level in enumerate(levels):
1531+
styles.append(
1532+
{
1533+
"selector": f"thead tr:nth-child({level+1}) th",
1534+
"props": props
1535+
+ (
1536+
f"top:{i * pixel_size}px; height:{pixel_size}px; "
1537+
"z-index:2;"
1538+
),
1539+
}
1540+
)
1541+
if not all(name is None for name in self.index.names):
1542+
styles.append(
1543+
{
1544+
"selector": f"thead tr:nth-child({obj.nlevels+1}) th",
1545+
"props": props
1546+
+ (
1547+
f"top:{(i+1) * pixel_size}px; height:{pixel_size}px; "
1548+
"z-index:2;"
1549+
),
1550+
}
1551+
)
15401552

1541-
return self
1553+
else:
1554+
styles = []
1555+
for i, level in enumerate(levels):
1556+
props_ = props + (
1557+
f"left:{i * pixel_size}px; "
1558+
f"min-width:{pixel_size}px; "
1559+
f"max-width:{pixel_size}px; "
1560+
)
1561+
styles.extend(
1562+
[
1563+
{
1564+
"selector": f"thead tr th:nth-child({level+1})",
1565+
"props": props_ + "z-index:3 !important;",
1566+
},
1567+
{
1568+
"selector": f"tbody tr th.level{level}",
1569+
"props": props_ + "z-index:1;",
1570+
},
1571+
]
1572+
)
1573+
1574+
return self.set_table_styles(styles, overwrite=False)
15421575

15431576
def set_table_styles(
15441577
self,

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

+62-113
Original file line numberDiff line numberDiff line change
@@ -281,26 +281,32 @@ def test_sticky_basic(styler, index, columns, index_name):
281281
if columns:
282282
styler.set_sticky(axis=1)
283283

284-
res = styler.set_uuid("").to_html()
285-
286-
css_for_index = (
287-
"tr th:first-child {\n position: sticky;\n background-color: white;\n "
288-
"left: 0px;\n z-index: 1;\n}"
284+
left_css = (
285+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
286+
" left: 0px;\n z-index: {1};\n}}"
289287
)
290-
assert (css_for_index in res) is index
291-
292-
css_for_cols_1 = (
293-
"thead tr:first-child {\n position: sticky;\n background-color: white;\n "
294-
"top: 0px;\n z-index: 2;\n"
288+
top_css = (
289+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
290+
" top: {1}px;\n z-index: {2};\n{3}}}"
295291
)
296-
css_for_cols_1 += " height: 25px;\n}" if index_name else "}"
297-
assert (css_for_cols_1 in res) is columns
298292

299-
css_for_cols_2 = (
300-
"thead tr:nth-child(2) {\n position: sticky;\n background-color: white;\n "
301-
"top: 25px;\n z-index: 2;\n height: 25px;\n}"
293+
res = styler.set_uuid("").to_html()
294+
295+
# test index stickys over thead and tbody
296+
assert (left_css.format("thead tr th:nth-child(1)", "3 !important") in res) is index
297+
assert (left_css.format("tbody tr th:nth-child(1)", "1") in res) is index
298+
299+
# test column stickys including if name row
300+
assert (
301+
top_css.format("thead tr:nth-child(1) th", "0", "2", " height: 25px;\n") in res
302+
) is (columns and index_name)
303+
assert (
304+
top_css.format("thead tr:nth-child(2) th", "25", "2", " height: 25px;\n")
305+
in res
306+
) is (columns and index_name)
307+
assert (top_css.format("thead tr:nth-child(1) th", "0", "2", "") in res) is (
308+
columns and not index_name
302309
)
303-
assert (css_for_cols_2 in res) is (index_name and columns)
304310

305311

306312
@pytest.mark.parametrize("index", [False, True])
@@ -311,73 +317,30 @@ def test_sticky_mi(styler_mi, index, columns):
311317
if columns:
312318
styler_mi.set_sticky(axis=1)
313319

314-
res = styler_mi.set_uuid("").to_html()
315-
assert (
316-
(
317-
dedent(
318-
"""\
319-
#T_ tbody th.level0 {
320-
position: sticky;
321-
left: 0px;
322-
min-width: 75px;
323-
max-width: 75px;
324-
background-color: white;
325-
}
326-
"""
327-
)
328-
in res
329-
)
330-
is index
320+
left_css = (
321+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
322+
" left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
331323
)
332-
assert (
333-
(
334-
dedent(
335-
"""\
336-
#T_ tbody th.level1 {
337-
position: sticky;
338-
left: 75px;
339-
min-width: 75px;
340-
max-width: 75px;
341-
background-color: white;
342-
}
343-
"""
344-
)
345-
in res
346-
)
347-
is index
324+
top_css = (
325+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
326+
" top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
348327
)
328+
329+
res = styler_mi.set_uuid("").to_html()
330+
331+
# test the index stickys for thead and tbody over both levels
349332
assert (
350-
(
351-
dedent(
352-
"""\
353-
#T_ thead th.level0 {
354-
position: sticky;
355-
top: 0px;
356-
height: 25px;
357-
background-color: white;
358-
}
359-
"""
360-
)
361-
in res
362-
)
363-
is columns
364-
)
333+
left_css.format("thead tr th:nth-child(1)", "0", "3 !important") in res
334+
) is index
335+
assert (left_css.format("tbody tr th.level0", "0", "1") in res) is index
365336
assert (
366-
(
367-
dedent(
368-
"""\
369-
#T_ thead th.level1 {
370-
position: sticky;
371-
top: 25px;
372-
height: 25px;
373-
background-color: white;
374-
}
375-
"""
376-
)
377-
in res
378-
)
379-
is columns
380-
)
337+
left_css.format("thead tr th:nth-child(2)", "75", "3 !important") in res
338+
) is index
339+
assert (left_css.format("tbody tr th.level1", "75", "1") in res) is index
340+
341+
# test the column stickys for each level row
342+
assert (top_css.format("thead tr:nth-child(1) th", "0", "2") in res) is columns
343+
assert (top_css.format("thead tr:nth-child(2) th", "25", "2") in res) is columns
381344

382345

383346
@pytest.mark.parametrize("index", [False, True])
@@ -388,43 +351,29 @@ def test_sticky_levels(styler_mi, index, columns):
388351
if columns:
389352
styler_mi.set_sticky(axis=1, levels=[1])
390353

391-
res = styler_mi.set_uuid("").to_html()
392-
assert "#T_ tbody th.level0 {" not in res
393-
assert "#T_ thead th.level0 {" not in res
394-
assert (
395-
(
396-
dedent(
397-
"""\
398-
#T_ tbody th.level1 {
399-
position: sticky;
400-
left: 0px;
401-
min-width: 75px;
402-
max-width: 75px;
403-
background-color: white;
404-
}
405-
"""
406-
)
407-
in res
408-
)
409-
is index
354+
left_css = (
355+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
356+
" left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
410357
)
411-
assert (
412-
(
413-
dedent(
414-
"""\
415-
#T_ thead th.level1 {
416-
position: sticky;
417-
top: 0px;
418-
height: 25px;
419-
background-color: white;
420-
}
421-
"""
422-
)
423-
in res
424-
)
425-
is columns
358+
top_css = (
359+
"#T_ {0} {{\n position: sticky;\n background-color: white;\n"
360+
" top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
426361
)
427362

363+
res = styler_mi.set_uuid("").to_html()
364+
365+
# test no sticking of level0
366+
assert "#T_ thead tr th:nth-child(1)" not in res
367+
assert "#T_ tbody tr th.level0" not in res
368+
assert "#T_ thead tr:nth-child(1) th" not in res
369+
370+
# test sticking level1
371+
assert (
372+
left_css.format("thead tr th:nth-child(2)", "0", "3 !important") in res
373+
) is index
374+
assert (left_css.format("tbody tr th.level1", "0", "1") in res) is index
375+
assert (top_css.format("thead tr:nth-child(2) th", "0", "2") in res) is columns
376+
428377

429378
def test_sticky_raises(styler):
430379
with pytest.raises(ValueError, match="`axis` must be"):

0 commit comments

Comments
 (0)