Skip to content

Commit 06a7251

Browse files
authored
ENH: Add ability to store tooltips as title attribute through styler (#56981)
1 parent 1ca9b58 commit 06a7251

File tree

4 files changed

+178
-42
lines changed

4 files changed

+178
-42
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Other enhancements
3030
^^^^^^^^^^^^^^^^^^
3131
- :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`)
3232
- :func:`read_stata` now returns ``datetime64`` resolutions better matching those natively stored in the stata format (:issue:`55642`)
33+
- :meth:`Styler.set_tooltips` provides alternative method to storing tooltips by using title attribute of td elements. (:issue:`56981`)
3334
- Allow dictionaries to be passed to :meth:`pandas.Series.str.replace` via ``pat`` parameter (:issue:`51748`)
3435
- Support passing a :class:`Series` input to :func:`json_normalize` that retains the :class:`Series` :class:`Index` (:issue:`51452`)
3536
-
@@ -274,7 +275,6 @@ ExtensionArray
274275
Styler
275276
^^^^^^
276277
-
277-
-
278278

279279
Other
280280
^^^^^

pandas/io/formats/style.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def set_tooltips(
424424
ttips: DataFrame,
425425
props: CSSProperties | None = None,
426426
css_class: str | None = None,
427+
as_title_attribute: bool = False,
427428
) -> Styler:
428429
"""
429430
Set the DataFrame of strings on ``Styler`` generating ``:hover`` tooltips.
@@ -447,6 +448,9 @@ def set_tooltips(
447448
Name of the tooltip class used in CSS, should conform to HTML standards.
448449
Only useful if integrating tooltips with external CSS. If ``None`` uses the
449450
internal default value 'pd-t'.
451+
as_title_attribute : bool, default False
452+
Add the tooltip text as title attribute to resultant <td> element. If True
453+
then props and css_class arguments are ignored.
450454
451455
Returns
452456
-------
@@ -475,6 +479,12 @@ def set_tooltips(
475479
additional HTML for larger tables, since they also require that ``cell_ids``
476480
is forced to `True`.
477481
482+
If multiline tooltips are required, or if styling is not required and/or
483+
space is of concern, then utilizing as_title_attribute as True will store
484+
the tooltip on the <td> title attribute. This will cause no CSS
485+
to be generated nor will the <span> elements. Storing tooltips through
486+
the title attribute will mean that tooltip styling effects do not apply.
487+
478488
Examples
479489
--------
480490
Basic application
@@ -502,6 +512,10 @@ def set_tooltips(
502512
... props="visibility:hidden; position:absolute; z-index:1;",
503513
... )
504514
... # doctest: +SKIP
515+
516+
Multiline tooltips with smaller size footprint
517+
518+
>>> df.style.set_tooltips(ttips, as_title_attribute=True) # doctest: +SKIP
505519
"""
506520
if not self.cell_ids:
507521
# tooltips not optimised for individual cell check. requires reasonable
@@ -516,10 +530,13 @@ def set_tooltips(
516530
if self.tooltips is None: # create a default instance if necessary
517531
self.tooltips = Tooltips()
518532
self.tooltips.tt_data = ttips
519-
if props:
520-
self.tooltips.class_properties = props
521-
if css_class:
522-
self.tooltips.class_name = css_class
533+
if not as_title_attribute:
534+
if props:
535+
self.tooltips.class_properties = props
536+
if css_class:
537+
self.tooltips.class_name = css_class
538+
else:
539+
self.tooltips.as_title_attribute = as_title_attribute
523540

524541
return self
525542

pandas/io/formats/style_render.py

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,11 @@ class Tooltips:
19791979
tooltips: DataFrame, default empty
19801980
DataFrame of strings aligned with underlying Styler data for tooltip
19811981
display.
1982+
as_title_attribute: bool, default False
1983+
Flag to use title attribute based tooltips (True) or <span> based
1984+
tooltips (False).
1985+
Add the tooltip text as title attribute to resultant <td> element. If
1986+
True, no CSS is generated and styling effects do not apply.
19821987
19831988
Notes
19841989
-----
@@ -2007,11 +2012,13 @@ def __init__(
20072012
],
20082013
css_name: str = "pd-t",
20092014
tooltips: DataFrame = DataFrame(),
2015+
as_title_attribute: bool = False,
20102016
) -> None:
20112017
self.class_name = css_name
20122018
self.class_properties = css_props
20132019
self.tt_data = tooltips
20142020
self.table_styles: CSSStyles = []
2021+
self.as_title_attribute = as_title_attribute
20152022

20162023
@property
20172024
def _class_styles(self):
@@ -2101,35 +2108,53 @@ def _translate(self, styler: StylerRenderer, d: dict):
21012108
if self.tt_data.empty:
21022109
return d
21032110

2104-
name = self.class_name
21052111
mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip
2106-
self.table_styles = [
2107-
style
2108-
for sublist in [
2109-
self._pseudo_css(styler.uuid, name, i, j, str(self.tt_data.iloc[i, j]))
2110-
for i in range(len(self.tt_data.index))
2111-
for j in range(len(self.tt_data.columns))
2112-
if not (
2113-
mask.iloc[i, j]
2114-
or i in styler.hidden_rows
2115-
or j in styler.hidden_columns
2116-
)
2112+
# this conditional adds tooltips via pseudo css and <span> elements.
2113+
if not self.as_title_attribute:
2114+
name = self.class_name
2115+
self.table_styles = [
2116+
style
2117+
for sublist in [
2118+
self._pseudo_css(
2119+
styler.uuid, name, i, j, str(self.tt_data.iloc[i, j])
2120+
)
2121+
for i in range(len(self.tt_data.index))
2122+
for j in range(len(self.tt_data.columns))
2123+
if not (
2124+
mask.iloc[i, j]
2125+
or i in styler.hidden_rows
2126+
or j in styler.hidden_columns
2127+
)
2128+
]
2129+
for style in sublist
21172130
]
2118-
for style in sublist
2119-
]
2120-
2121-
if self.table_styles:
2122-
# add span class to every cell only if at least 1 non-empty tooltip
2123-
for row in d["body"]:
2124-
for item in row:
2125-
if item["type"] == "td":
2126-
item["display_value"] = (
2127-
str(item["display_value"])
2128-
+ f'<span class="{self.class_name}"></span>'
2129-
)
2130-
d["table_styles"].extend(self._class_styles)
2131-
d["table_styles"].extend(self.table_styles)
21322131

2132+
# add span class to every cell since there is at least 1 non-empty tooltip
2133+
if self.table_styles:
2134+
for row in d["body"]:
2135+
for item in row:
2136+
if item["type"] == "td":
2137+
item["display_value"] = (
2138+
str(item["display_value"])
2139+
+ f'<span class="{self.class_name}"></span>'
2140+
)
2141+
d["table_styles"].extend(self._class_styles)
2142+
d["table_styles"].extend(self.table_styles)
2143+
# this conditional adds tooltips as extra "title" attribute on a <td> element
2144+
else:
2145+
index_offset = self.tt_data.index.nlevels
2146+
body = d["body"]
2147+
for i in range(len(self.tt_data.index)):
2148+
for j in range(len(self.tt_data.columns)):
2149+
if (
2150+
not mask.iloc[i, j]
2151+
or i in styler.hidden_rows
2152+
or j in styler.hidden_columns
2153+
):
2154+
row = body[i]
2155+
item = row[j + index_offset]
2156+
value = self.tt_data.iloc[i, j]
2157+
item["attributes"] += f' title="{value}"'
21332158
return d
21342159

21352160

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

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import numpy as np
22
import pytest
33

4-
from pandas import DataFrame
4+
from pandas import (
5+
DataFrame,
6+
MultiIndex,
7+
)
58

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

2326

2427
@pytest.mark.parametrize(
25-
"ttips",
28+
"data, columns, index",
2629
[
27-
DataFrame( # Test basic reindex and ignoring blank
28-
data=[["Min", "Max"], [np.nan, ""]],
29-
columns=["A", "C"],
30-
index=["x", "y"],
31-
),
32-
DataFrame( # Test non-referenced columns, reversed col names, short index
33-
data=[["Max", "Min", "Bad-Col"]], columns=["C", "A", "D"], index=["x"]
34-
),
30+
# Test basic reindex and ignoring blank
31+
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
32+
# Test non-referenced columns, reversed col names, short index
33+
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
3534
],
3635
)
37-
def test_tooltip_render(ttips, styler):
36+
def test_tooltip_render(data, columns, index, styler):
37+
ttips = DataFrame(data=data, columns=columns, index=index)
38+
3839
# GH 21266
3940
result = styler.set_tooltips(ttips).to_html()
4041

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

6870

6971
def test_tooltip_css_class(styler):
@@ -83,3 +85,95 @@ def test_tooltip_css_class(styler):
8385
props="color:green;color:red;",
8486
).to_html()
8587
assert "#T_ .another-class {\n color: green;\n color: red;\n}" in result
88+
89+
90+
@pytest.mark.parametrize(
91+
"data, columns, index",
92+
[
93+
# Test basic reindex and ignoring blank
94+
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
95+
# Test non-referenced columns, reversed col names, short index
96+
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
97+
],
98+
)
99+
def test_tooltip_render_as_title(data, columns, index, styler):
100+
ttips = DataFrame(data=data, columns=columns, index=index)
101+
# GH 56605
102+
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()
103+
104+
# test css not added
105+
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result
106+
107+
# test 'Min' tooltip added as title attribute and css does not exist
108+
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
109+
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
110+
assert 'class="data row0 col0" title="Min">0</td>' in result
111+
112+
# test 'Max' tooltip added as title attribute and css does not exist
113+
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
114+
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
115+
assert 'class="data row0 col2" title="Max">2</td>' in result
116+
117+
# test Nan, empty string and bad column ignored
118+
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
119+
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
120+
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
121+
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
122+
assert "Bad-Col" not in result
123+
assert 'class="data row0 col1" >1</td>' in result
124+
assert 'class="data row1 col0" >3</td>' in result
125+
assert 'class="data row1 col1" >4</td>' in result
126+
assert 'class="data row1 col2" >5</td>' in result
127+
assert 'class="data row2 col0" >6</td>' in result
128+
assert 'class="data row2 col1" >7</td>' in result
129+
assert 'class="data row2 col2" >8</td>' in result
130+
131+
132+
def test_tooltip_render_as_title_with_hidden_index_level():
133+
df = DataFrame(
134+
data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
135+
columns=["A", "B", "C"],
136+
index=MultiIndex.from_arrays(
137+
[["x", "y", "z"], [1, 2, 3], ["aa", "bb", "cc"]],
138+
names=["alpha", "num", "char"],
139+
),
140+
)
141+
ttips = DataFrame(
142+
# Test basic reindex and ignoring blank, and hide level 2 (num) from index
143+
data=[["Min", "Max"], [np.nan, ""]],
144+
columns=["A", "C"],
145+
index=MultiIndex.from_arrays(
146+
[["x", "y"], [1, 2], ["aa", "bb"]], names=["alpha", "num", "char"]
147+
),
148+
)
149+
styler = Styler(df, uuid_len=0)
150+
styler = styler.hide(axis=0, level=-1, names=True)
151+
# GH 56605
152+
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()
153+
154+
# test css not added
155+
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result
156+
157+
# test 'Min' tooltip added as title attribute and css does not exist
158+
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
159+
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
160+
assert 'class="data row0 col0" title="Min">0</td>' in result
161+
162+
# test 'Max' tooltip added as title attribute and css does not exist
163+
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
164+
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
165+
assert 'class="data row0 col2" title="Max">2</td>' in result
166+
167+
# test Nan, empty string and bad column ignored
168+
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
169+
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
170+
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
171+
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
172+
assert "Bad-Col" not in result
173+
assert 'class="data row0 col1" >1</td>' in result
174+
assert 'class="data row1 col0" >3</td>' in result
175+
assert 'class="data row1 col1" >4</td>' in result
176+
assert 'class="data row1 col2" >5</td>' in result
177+
assert 'class="data row2 col0" >6</td>' in result
178+
assert 'class="data row2 col1" >7</td>' in result
179+
assert 'class="data row2 col2" >8</td>' in result

0 commit comments

Comments
 (0)