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"):