Skip to content

BUG: Styler.set_sticky fix the names rows 2/2 #42799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 7, 2021
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
-

Expand Down
85 changes: 59 additions & 26 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tr> of <head> and, if index names, the second <tr>
# if self._hide_columns then no <thead><tr> 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;",
}
]
Expand All @@ -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; ",
}
Expand All @@ -1572,34 +1572,67 @@ def set_sticky(
# but <th> will exist in <thead>: 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,
Expand Down
175 changes: 62 additions & 113 deletions pandas/tests/io/formats/style/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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])
Expand All @@ -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"):
Expand Down