diff --git a/doc/source/whatsnew/v1.3.2.rst b/doc/source/whatsnew/v1.3.2.rst
index bcb096e630d85..0539d17526579 100644
--- a/doc/source/whatsnew/v1.3.2.rst
+++ b/doc/source/whatsnew/v1.3.2.rst
@@ -33,6 +33,7 @@ Bug fixes
- Bug in :meth:`pandas.read_excel` modifies the dtypes dictionary when reading a file with duplicate columns (:issue:`42462`)
- 1D slices over extension types turn into N-dimensional slices over ExtensionArrays (:issue:`42430`)
- :meth:`.Styler.hide_columns` now hides the index name header row as well as column headers (:issue:`42101`)
+- :meth:`.Styler.set_sticky` has amended CSS to control the column/index names and ensure the correct sticky positions (:issue:`42537`)
- Bug in de-serializing datetime indexes in PYTHONOPTIMIZED mode (:issue:`42866`)
-
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index 53ae2daa31235..525e55290af17 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -1534,24 +1534,24 @@ def set_sticky(
may produce strange behaviour due to CSS controls with missing elements.
"""
if axis in [0, "index"]:
- axis, obj, tag, pos = 0, self.data.index, "tbody", "left"
+ axis, obj = 0, self.data.index
pixel_size = 75 if not pixel_size else pixel_size
elif axis in [1, "columns"]:
- axis, obj, tag, pos = 1, self.data.columns, "thead", "top"
+ axis, obj = 1, self.data.columns
pixel_size = 25 if not pixel_size else pixel_size
else:
raise ValueError("`axis` must be one of {0, 1, 'index', 'columns'}")
+ props = "position:sticky; background-color:white;"
if not isinstance(obj, pd.MultiIndex):
# handling MultiIndexes requires different CSS
- props = "position:sticky; background-color:white;"
if axis == 1:
# stick the first
of and, if index names, the second
# if self._hide_columns then no here will exist: no conflict
styles: CSSStyles = [
{
- "selector": "thead tr:first-child",
+ "selector": "thead tr:nth-child(1) th",
"props": props + "top:0px; z-index:2;",
}
]
@@ -1561,7 +1561,7 @@ def set_sticky(
)
styles.append(
{
- "selector": "thead tr:nth-child(2)",
+ "selector": "thead tr:nth-child(2) th",
"props": props
+ f"top:{pixel_size}px; z-index:2; height:{pixel_size}px; ",
}
@@ -1572,34 +1572,67 @@ def set_sticky(
# but will exist in : conflict with initial element
styles = [
{
- "selector": "tr th:first-child",
+ "selector": "thead tr th:nth-child(1)",
+ "props": props + "left:0px; z-index:3 !important;",
+ },
+ {
+ "selector": "tbody tr th:nth-child(1)",
"props": props + "left:0px; z-index:1;",
- }
+ },
]
- return self.set_table_styles(styles, overwrite=False)
-
else:
+ # handle the MultiIndex case
range_idx = list(range(obj.nlevels))
+ levels = sorted(levels) if levels else range_idx
- levels = sorted(levels) if levels else range_idx
- for i, level in enumerate(levels):
- self.set_table_styles(
- [
- {
- "selector": f"{tag} th.level{level}",
- "props": f"position: sticky; "
- f"{pos}: {i * pixel_size}px; "
- f"{f'height: {pixel_size}px; ' if axis == 1 else ''}"
- f"{f'min-width: {pixel_size}px; ' if axis == 0 else ''}"
- f"{f'max-width: {pixel_size}px; ' if axis == 0 else ''}"
- f"background-color: white;",
- }
- ],
- overwrite=False,
- )
+ if axis == 1:
+ styles = []
+ for i, level in enumerate(levels):
+ styles.append(
+ {
+ "selector": f"thead tr:nth-child({level+1}) th",
+ "props": props
+ + (
+ f"top:{i * pixel_size}px; height:{pixel_size}px; "
+ "z-index:2;"
+ ),
+ }
+ )
+ if not all(name is None for name in self.index.names):
+ styles.append(
+ {
+ "selector": f"thead tr:nth-child({obj.nlevels+1}) th",
+ "props": props
+ + (
+ f"top:{(i+1) * pixel_size}px; height:{pixel_size}px; "
+ "z-index:2;"
+ ),
+ }
+ )
- return self
+ else:
+ styles = []
+ for i, level in enumerate(levels):
+ props_ = props + (
+ f"left:{i * pixel_size}px; "
+ f"min-width:{pixel_size}px; "
+ f"max-width:{pixel_size}px; "
+ )
+ styles.extend(
+ [
+ {
+ "selector": f"thead tr th:nth-child({level+1})",
+ "props": props_ + "z-index:3 !important;",
+ },
+ {
+ "selector": f"tbody tr th.level{level}",
+ "props": props_ + "z-index:1;",
+ },
+ ]
+ )
+
+ return self.set_table_styles(styles, overwrite=False)
def set_table_styles(
self,
diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py
index 2657370bf8258..9983017652919 100644
--- a/pandas/tests/io/formats/style/test_html.py
+++ b/pandas/tests/io/formats/style/test_html.py
@@ -282,26 +282,32 @@ def test_sticky_basic(styler, index, columns, index_name):
if columns:
styler.set_sticky(axis=1)
- res = styler.set_uuid("").to_html()
-
- css_for_index = (
- "tr th:first-child {\n position: sticky;\n background-color: white;\n "
- "left: 0px;\n z-index: 1;\n}"
+ left_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " left: 0px;\n z-index: {1};\n}}"
)
- assert (css_for_index in res) is index
-
- css_for_cols_1 = (
- "thead tr:first-child {\n position: sticky;\n background-color: white;\n "
- "top: 0px;\n z-index: 2;\n"
+ top_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " top: {1}px;\n z-index: {2};\n{3}}}"
)
- css_for_cols_1 += " height: 25px;\n}" if index_name else "}"
- assert (css_for_cols_1 in res) is columns
- css_for_cols_2 = (
- "thead tr:nth-child(2) {\n position: sticky;\n background-color: white;\n "
- "top: 25px;\n z-index: 2;\n height: 25px;\n}"
+ res = styler.set_uuid("").to_html()
+
+ # test index stickys over thead and tbody
+ assert (left_css.format("thead tr th:nth-child(1)", "3 !important") in res) is index
+ assert (left_css.format("tbody tr th:nth-child(1)", "1") in res) is index
+
+ # test column stickys including if name row
+ assert (
+ top_css.format("thead tr:nth-child(1) th", "0", "2", " height: 25px;\n") in res
+ ) is (columns and index_name)
+ assert (
+ top_css.format("thead tr:nth-child(2) th", "25", "2", " height: 25px;\n")
+ in res
+ ) is (columns and index_name)
+ assert (top_css.format("thead tr:nth-child(1) th", "0", "2", "") in res) is (
+ columns and not index_name
)
- assert (css_for_cols_2 in res) is (index_name and columns)
@pytest.mark.parametrize("index", [False, True])
@@ -312,73 +318,30 @@ def test_sticky_mi(styler_mi, index, columns):
if columns:
styler_mi.set_sticky(axis=1)
- res = styler_mi.set_uuid("").to_html()
- assert (
- (
- dedent(
- """\
- #T_ tbody th.level0 {
- position: sticky;
- left: 0px;
- min-width: 75px;
- max-width: 75px;
- background-color: white;
- }
- """
- )
- in res
- )
- is index
+ left_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
)
- assert (
- (
- dedent(
- """\
- #T_ tbody th.level1 {
- position: sticky;
- left: 75px;
- min-width: 75px;
- max-width: 75px;
- background-color: white;
- }
- """
- )
- in res
- )
- is index
+ top_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
)
+
+ res = styler_mi.set_uuid("").to_html()
+
+ # test the index stickys for thead and tbody over both levels
assert (
- (
- dedent(
- """\
- #T_ thead th.level0 {
- position: sticky;
- top: 0px;
- height: 25px;
- background-color: white;
- }
- """
- )
- in res
- )
- is columns
- )
+ left_css.format("thead tr th:nth-child(1)", "0", "3 !important") in res
+ ) is index
+ assert (left_css.format("tbody tr th.level0", "0", "1") in res) is index
assert (
- (
- dedent(
- """\
- #T_ thead th.level1 {
- position: sticky;
- top: 25px;
- height: 25px;
- background-color: white;
- }
- """
- )
- in res
- )
- is columns
- )
+ left_css.format("thead tr th:nth-child(2)", "75", "3 !important") in res
+ ) is index
+ assert (left_css.format("tbody tr th.level1", "75", "1") in res) is index
+
+ # test the column stickys for each level row
+ assert (top_css.format("thead tr:nth-child(1) th", "0", "2") in res) is columns
+ assert (top_css.format("thead tr:nth-child(2) th", "25", "2") in res) is columns
@pytest.mark.parametrize("index", [False, True])
@@ -389,43 +352,29 @@ def test_sticky_levels(styler_mi, index, columns):
if columns:
styler_mi.set_sticky(axis=1, levels=[1])
- res = styler_mi.set_uuid("").to_html()
- assert "#T_ tbody th.level0 {" not in res
- assert "#T_ thead th.level0 {" not in res
- assert (
- (
- dedent(
- """\
- #T_ tbody th.level1 {
- position: sticky;
- left: 0px;
- min-width: 75px;
- max-width: 75px;
- background-color: white;
- }
- """
- )
- in res
- )
- is index
+ left_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
)
- assert (
- (
- dedent(
- """\
- #T_ thead th.level1 {
- position: sticky;
- top: 0px;
- height: 25px;
- background-color: white;
- }
- """
- )
- in res
- )
- is columns
+ top_css = (
+ "#T_ {0} {{\n position: sticky;\n background-color: white;\n"
+ " top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
)
+ res = styler_mi.set_uuid("").to_html()
+
+ # test no sticking of level0
+ assert "#T_ thead tr th:nth-child(1)" not in res
+ assert "#T_ tbody tr th.level0" not in res
+ assert "#T_ thead tr:nth-child(1) th" not in res
+
+ # test sticking level1
+ assert (
+ left_css.format("thead tr th:nth-child(2)", "0", "3 !important") in res
+ ) is index
+ assert (left_css.format("tbody tr th.level1", "0", "1") in res) is index
+ assert (top_css.format("thead tr:nth-child(2) th", "0", "2") in res) is columns
+
def test_sticky_raises(styler):
with pytest.raises(ValueError, match="`axis` must be"):
|