Skip to content

ENH: Adds ability to store tooltips as title attribute through pandas styler #56981

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8c3535e
Adds ability to store tooltips as title attribute through pandas styler
Delengowski Jan 20, 2024
9eec254
Adds entry to whatsnew
Delengowski Jan 20, 2024
17f54f8
Updates GH issue number comment
Delengowski Jan 20, 2024
a2abc10
Fixes trailing whitespace precommit check
Delengowski Jan 20, 2024
ccaae39
Performs ruff to pass precommit checks
Delengowski Jan 20, 2024
ce0bc5a
Resolves mypy errors
Delengowski Jan 20, 2024
eba4719
Fixes grammar in whatsnew
Delengowski Jan 20, 2024
67c563d
Another grammar fix in whatsnew
Delengowski Jan 20, 2024
55e5af2
Changes loop to not utilize regex
Delengowski Jan 28, 2024
80eeb57
Formatting, and simplified whatsnew
Delengowski Jan 28, 2024
9ffebee
Adds check for types of quotes used
Delengowski Jan 28, 2024
a0ba24b
Removes quotation mark check
Delengowski Jan 28, 2024
942b089
Adds additional test to convince myself of functionality
Delengowski Feb 3, 2024
c248e4d
Update pandas/io/formats/style_render.py
Delengowski Feb 3, 2024
cd28f59
Update pandas/io/formats/style_render.py
Delengowski Feb 3, 2024
2d8c292
Resolves grammar issue
Delengowski Feb 4, 2024
36a489e
Merge branch 'main' of github.com:Delengowski/pandas into Add-alterna…
Delengowski Feb 4, 2024
a0630db
Merge branch 'Add-alternative-html-for-Styler-tooltips' of github.com…
Delengowski Feb 4, 2024
78c448e
Isort to pass precommit
Delengowski Feb 4, 2024
743dc93
Ruff format on merged files
Delengowski Feb 4, 2024
d083cde
Update doc/source/whatsnew/v3.0.0.rst
Delengowski Feb 12, 2024
6cdce61
Changes whatsnew section
Delengowski Feb 12, 2024
1095864
Resolves grammar error in docstring
Delengowski Feb 12, 2024
6f6a036
Updates comment with suggestion
Delengowski Feb 12, 2024
1297ea5
Reverts accidental style formatting
Delengowski Feb 12, 2024
0d03fa0
Reverts parametrization -> fixture back to parametrization
Delengowski Feb 12, 2024
f7939ef
Changes parametrization per request
Delengowski Feb 13, 2024
f52c33a
Fixes ruff format error
Delengowski Feb 13, 2024
e98af18
merged main
Delengowski Feb 14, 2024
0d1e1e8
Merge branch 'main' into Add-alternative-html-for-Styler-tooltips
Delengowski Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Other enhancements
^^^^^^^^^^^^^^^^^^
- :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`)
- :func:`read_stata` now returns ``datetime64`` resolutions better matching those natively stored in the stata format (:issue:`55642`)
- :meth:`Styler.set_tooltips` provides alternative method to storing tooltips by using title attribute of td elements. (:issue:`56981`)
- Allow dictionaries to be passed to :meth:`pandas.Series.str.replace` via ``pat`` parameter (:issue:`51748`)
-

Expand Down Expand Up @@ -269,7 +270,6 @@ ExtensionArray
Styler
^^^^^^
-
-

Other
^^^^^
Expand Down
25 changes: 21 additions & 4 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def set_tooltips(
ttips: DataFrame,
props: CSSProperties | None = None,
css_class: str | None = None,
as_title_attribute: bool = False,
) -> Styler:
"""
Set the DataFrame of strings on ``Styler`` generating ``:hover`` tooltips.
Expand All @@ -447,6 +448,9 @@ def set_tooltips(
Name of the tooltip class used in CSS, should conform to HTML standards.
Only useful if integrating tooltips with external CSS. If ``None`` uses the
internal default value 'pd-t'.
as_title_attribute : bool, default False
Add the tooltip text as title attribute to resultant <td> element. If True
then props and css_class arguments are ignored.

Returns
-------
Expand Down Expand Up @@ -475,6 +479,12 @@ def set_tooltips(
additional HTML for larger tables, since they also require that ``cell_ids``
is forced to `True`.

If multiline tooltips are required, or if styling is not required and/or
space is of concern, then utilizing as_title_attribute as True will store
the tooltip on the <td> title attribute. This will cause no CSS
to be generated nor will the <span> elements. Storing tooltips through
the title attribute will mean that tooltip styling effects do not apply.

Examples
--------
Basic application
Expand Down Expand Up @@ -502,6 +512,10 @@ def set_tooltips(
... props="visibility:hidden; position:absolute; z-index:1;",
... )
... # doctest: +SKIP

Multiline tooltips with smaller size footprint

>>> df.style.set_tooltips(ttips, as_title_attribute=True) # doctest: +SKIP
"""
if not self.cell_ids:
# tooltips not optimised for individual cell check. requires reasonable
Expand All @@ -516,10 +530,13 @@ def set_tooltips(
if self.tooltips is None: # create a default instance if necessary
self.tooltips = Tooltips()
self.tooltips.tt_data = ttips
if props:
self.tooltips.class_properties = props
if css_class:
self.tooltips.class_name = css_class
if not as_title_attribute:
if props:
self.tooltips.class_properties = props
if css_class:
self.tooltips.class_name = css_class
else:
self.tooltips.as_title_attribute = as_title_attribute

return self

Expand Down
77 changes: 51 additions & 26 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,11 @@ class Tooltips:
tooltips: DataFrame, default empty
DataFrame of strings aligned with underlying Styler data for tooltip
display.
as_title_attribute: bool, default False
Flag to use title attribute based tooltips (True) or <span> based
tooltips (False).
Add the tooltip text as title attribute to resultant <td> element. If
True, no CSS is generated and styling effects do not apply.

Notes
-----
Expand Down Expand Up @@ -2007,11 +2012,13 @@ def __init__(
],
css_name: str = "pd-t",
tooltips: DataFrame = DataFrame(),
as_title_attribute: bool = False,
) -> None:
self.class_name = css_name
self.class_properties = css_props
self.tt_data = tooltips
self.table_styles: CSSStyles = []
self.as_title_attribute = as_title_attribute

@property
def _class_styles(self):
Expand Down Expand Up @@ -2101,35 +2108,53 @@ def _translate(self, styler: StylerRenderer, d: dict):
if self.tt_data.empty:
return d

name = self.class_name
mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip
self.table_styles = [
style
for sublist in [
self._pseudo_css(styler.uuid, name, i, j, str(self.tt_data.iloc[i, j]))
for i in range(len(self.tt_data.index))
for j in range(len(self.tt_data.columns))
if not (
mask.iloc[i, j]
or i in styler.hidden_rows
or j in styler.hidden_columns
)
# this conditional adds tooltips via pseudo css and <span> elements.
if not self.as_title_attribute:
name = self.class_name
self.table_styles = [
style
for sublist in [
self._pseudo_css(
styler.uuid, name, i, j, str(self.tt_data.iloc[i, j])
)
for i in range(len(self.tt_data.index))
for j in range(len(self.tt_data.columns))
if not (
mask.iloc[i, j]
or i in styler.hidden_rows
or j in styler.hidden_columns
)
]
for style in sublist
]
for style in sublist
]

if self.table_styles:
# add span class to every cell only if at least 1 non-empty tooltip
for row in d["body"]:
for item in row:
if item["type"] == "td":
item["display_value"] = (
str(item["display_value"])
+ f'<span class="{self.class_name}"></span>'
)
d["table_styles"].extend(self._class_styles)
d["table_styles"].extend(self.table_styles)

# add span class to every cell since there is at least 1 non-empty tooltip
if self.table_styles:
for row in d["body"]:
for item in row:
if item["type"] == "td":
item["display_value"] = (
str(item["display_value"])
+ f'<span class="{self.class_name}"></span>'
)
d["table_styles"].extend(self._class_styles)
d["table_styles"].extend(self.table_styles)
# this conditional adds tooltips as extra "title" attribute on a <td> element
else:
index_offset = self.tt_data.index.nlevels
body = d["body"]
for i in range(len(self.tt_data.index)):
for j in range(len(self.tt_data.columns)):
if (
not mask.iloc[i, j]
or i in styler.hidden_rows
or j in styler.hidden_columns
):
row = body[i]
item = row[j + index_offset]
value = self.tt_data.iloc[i, j]
item["attributes"] += f' title="{value}"'
return d


Expand Down
116 changes: 105 additions & 11 deletions pandas/tests/io/formats/style/test_tooltip.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import numpy as np
import pytest

from pandas import DataFrame
from pandas import (
DataFrame,
MultiIndex,
)

pytest.importorskip("jinja2")
from pandas.io.formats.style import Styler
Expand All @@ -22,19 +25,17 @@ def styler(df):


@pytest.mark.parametrize(
"ttips",
"data, columns, index",
[
DataFrame( # Test basic reindex and ignoring blank
data=[["Min", "Max"], [np.nan, ""]],
columns=["A", "C"],
index=["x", "y"],
),
DataFrame( # Test non-referenced columns, reversed col names, short index
data=[["Max", "Min", "Bad-Col"]], columns=["C", "A", "D"], index=["x"]
),
# Test basic reindex and ignoring blank
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
# Test non-referenced columns, reversed col names, short index
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
],
)
def test_tooltip_render(ttips, styler):
def test_tooltip_render(data, columns, index, styler):
ttips = DataFrame(data=data, columns=columns, index=index)

# GH 21266
result = styler.set_tooltips(ttips).to_html()

Expand Down Expand Up @@ -64,6 +65,7 @@ def test_tooltip_ignored(styler):
result = styler.to_html() # no set_tooltips() creates no <span>
assert '<style type="text/css">\n</style>' in result
assert '<span class="pd-t"></span>' not in result
assert 'title="' not in result


def test_tooltip_css_class(styler):
Expand All @@ -83,3 +85,95 @@ def test_tooltip_css_class(styler):
props="color:green;color:red;",
).to_html()
assert "#T_ .another-class {\n color: green;\n color: red;\n}" in result


@pytest.mark.parametrize(
"data, columns, index",
[
# Test basic reindex and ignoring blank
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
# Test non-referenced columns, reversed col names, short index
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
],
)
def test_tooltip_render_as_title(data, columns, index, styler):
ttips = DataFrame(data=data, columns=columns, index=index)
# GH 56605
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()

# test css not added
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result

# test 'Min' tooltip added as title attribute and css does not exist
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
assert 'class="data row0 col0" title="Min">0</td>' in result

# test 'Max' tooltip added as title attribute and css does not exist
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
assert 'class="data row0 col2" title="Max">2</td>' in result

# test Nan, empty string and bad column ignored
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
assert "Bad-Col" not in result
assert 'class="data row0 col1" >1</td>' in result
assert 'class="data row1 col0" >3</td>' in result
assert 'class="data row1 col1" >4</td>' in result
assert 'class="data row1 col2" >5</td>' in result
assert 'class="data row2 col0" >6</td>' in result
assert 'class="data row2 col1" >7</td>' in result
assert 'class="data row2 col2" >8</td>' in result


def test_tooltip_render_as_title_with_hidden_index_level():
df = DataFrame(
data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
columns=["A", "B", "C"],
index=MultiIndex.from_arrays(
[["x", "y", "z"], [1, 2, 3], ["aa", "bb", "cc"]],
names=["alpha", "num", "char"],
),
)
ttips = DataFrame(
# Test basic reindex and ignoring blank, and hide level 2 (num) from index
data=[["Min", "Max"], [np.nan, ""]],
columns=["A", "C"],
index=MultiIndex.from_arrays(
[["x", "y"], [1, 2], ["aa", "bb"]], names=["alpha", "num", "char"]
),
)
styler = Styler(df, uuid_len=0)
styler = styler.hide(axis=0, level=-1, names=True)
# GH 56605
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()

# test css not added
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result

# test 'Min' tooltip added as title attribute and css does not exist
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
assert 'class="data row0 col0" title="Min">0</td>' in result

# test 'Max' tooltip added as title attribute and css does not exist
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
assert 'class="data row0 col2" title="Max">2</td>' in result

# test Nan, empty string and bad column ignored
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
assert "Bad-Col" not in result
assert 'class="data row0 col1" >1</td>' in result
assert 'class="data row1 col0" >3</td>' in result
assert 'class="data row1 col1" >4</td>' in result
assert 'class="data row1 col2" >5</td>' in result
assert 'class="data row2 col0" >6</td>' in result
assert 'class="data row2 col1" >7</td>' in result
assert 'class="data row2 col2" >8</td>' in result