Skip to content

Commit 9774008

Browse files
authored
BUG: Styler.set_sticky fix the names rows 2/2 (#42799)
1 parent 2af4ac0 commit 9774008

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
@@ -33,6 +33,7 @@ Bug fixes
3333
- Bug in :meth:`pandas.read_excel` modifies the dtypes dictionary when reading a file with duplicate columns (:issue:`42462`)
3434
- 1D slices over extension types turn into N-dimensional slices over ExtensionArrays (:issue:`42430`)
3535
- :meth:`.Styler.hide_columns` now hides the index name header row as well as column headers (:issue:`42101`)
36+
- :meth:`.Styler.set_sticky` has amended CSS to control the column/index names and ensure the correct sticky positions (:issue:`42537`)
3637
- Bug in de-serializing datetime indexes in PYTHONOPTIMIZED mode (:issue:`42866`)
3738
-
3839

pandas/io/formats/style.py

+59-26
Original file line numberDiff line numberDiff line change
@@ -1534,24 +1534,24 @@ def set_sticky(
15341534
may produce strange behaviour due to CSS controls with missing elements.
15351535
"""
15361536
if axis in [0, "index"]:
1537-
axis, obj, tag, pos = 0, self.data.index, "tbody", "left"
1537+
axis, obj = 0, self.data.index
15381538
pixel_size = 75 if not pixel_size else pixel_size
15391539
elif axis in [1, "columns"]:
1540-
axis, obj, tag, pos = 1, self.data.columns, "thead", "top"
1540+
axis, obj = 1, self.data.columns
15411541
pixel_size = 25 if not pixel_size else pixel_size
15421542
else:
15431543
raise ValueError("`axis` must be one of {0, 1, 'index', 'columns'}")
15441544

1545+
props = "position:sticky; background-color:white;"
15451546
if not isinstance(obj, pd.MultiIndex):
15461547
# handling MultiIndexes requires different CSS
1547-
props = "position:sticky; background-color:white;"
15481548

15491549
if axis == 1:
15501550
# stick the first <tr> of <head> and, if index names, the second <tr>
15511551
# if self._hide_columns then no <thead><tr> here will exist: no conflict
15521552
styles: CSSStyles = [
15531553
{
1554-
"selector": "thead tr:first-child",
1554+
"selector": "thead tr:nth-child(1) th",
15551555
"props": props + "top:0px; z-index:2;",
15561556
}
15571557
]
@@ -1561,7 +1561,7 @@ def set_sticky(
15611561
)
15621562
styles.append(
15631563
{
1564-
"selector": "thead tr:nth-child(2)",
1564+
"selector": "thead tr:nth-child(2) th",
15651565
"props": props
15661566
+ f"top:{pixel_size}px; z-index:2; height:{pixel_size}px; ",
15671567
}
@@ -1572,34 +1572,67 @@ def set_sticky(
15721572
# but <th> will exist in <thead>: conflict with initial element
15731573
styles = [
15741574
{
1575-
"selector": "tr th:first-child",
1575+
"selector": "thead tr th:nth-child(1)",
1576+
"props": props + "left:0px; z-index:3 !important;",
1577+
},
1578+
{
1579+
"selector": "tbody tr th:nth-child(1)",
15761580
"props": props + "left:0px; z-index:1;",
1577-
}
1581+
},
15781582
]
15791583

1580-
return self.set_table_styles(styles, overwrite=False)
1581-
15821584
else:
1585+
# handle the MultiIndex case
15831586
range_idx = list(range(obj.nlevels))
1587+
levels = sorted(levels) if levels else range_idx
15841588

1585-
levels = sorted(levels) if levels else range_idx
1586-
for i, level in enumerate(levels):
1587-
self.set_table_styles(
1588-
[
1589-
{
1590-
"selector": f"{tag} th.level{level}",
1591-
"props": f"position: sticky; "
1592-
f"{pos}: {i * pixel_size}px; "
1593-
f"{f'height: {pixel_size}px; ' if axis == 1 else ''}"
1594-
f"{f'min-width: {pixel_size}px; ' if axis == 0 else ''}"
1595-
f"{f'max-width: {pixel_size}px; ' if axis == 0 else ''}"
1596-
f"background-color: white;",
1597-
}
1598-
],
1599-
overwrite=False,
1600-
)
1589+
if axis == 1:
1590+
styles = []
1591+
for i, level in enumerate(levels):
1592+
styles.append(
1593+
{
1594+
"selector": f"thead tr:nth-child({level+1}) th",
1595+
"props": props
1596+
+ (
1597+
f"top:{i * pixel_size}px; height:{pixel_size}px; "
1598+
"z-index:2;"
1599+
),
1600+
}
1601+
)
1602+
if not all(name is None for name in self.index.names):
1603+
styles.append(
1604+
{
1605+
"selector": f"thead tr:nth-child({obj.nlevels+1}) th",
1606+
"props": props
1607+
+ (
1608+
f"top:{(i+1) * pixel_size}px; height:{pixel_size}px; "
1609+
"z-index:2;"
1610+
),
1611+
}
1612+
)
16011613

1602-
return self
1614+
else:
1615+
styles = []
1616+
for i, level in enumerate(levels):
1617+
props_ = props + (
1618+
f"left:{i * pixel_size}px; "
1619+
f"min-width:{pixel_size}px; "
1620+
f"max-width:{pixel_size}px; "
1621+
)
1622+
styles.extend(
1623+
[
1624+
{
1625+
"selector": f"thead tr th:nth-child({level+1})",
1626+
"props": props_ + "z-index:3 !important;",
1627+
},
1628+
{
1629+
"selector": f"tbody tr th.level{level}",
1630+
"props": props_ + "z-index:1;",
1631+
},
1632+
]
1633+
)
1634+
1635+
return self.set_table_styles(styles, overwrite=False)
16031636

16041637
def set_table_styles(
16051638
self,

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

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

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

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

306312

307313
@pytest.mark.parametrize("index", [False, True])
@@ -312,73 +318,30 @@ def test_sticky_mi(styler_mi, index, columns):
312318
if columns:
313319
styler_mi.set_sticky(axis=1)
314320

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

383346

384347
@pytest.mark.parametrize("index", [False, True])
@@ -389,43 +352,29 @@ def test_sticky_levels(styler_mi, index, columns):
389352
if columns:
390353
styler_mi.set_sticky(axis=1, levels=[1])
391354

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

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

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

0 commit comments

Comments
 (0)