diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 012fe47c476d1..470099c0b5cc4 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -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`)
-
@@ -269,7 +270,6 @@ ExtensionArray
Styler
^^^^^^
-
--
Other
^^^^^
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index 31e025ace4b03..3f607ef63e2f7 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -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.
@@ -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
element. If True
+ then props and css_class arguments are ignored.
Returns
-------
@@ -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 | title attribute. This will cause no CSS
+ to be generated nor will the elements. Storing tooltips through
+ the title attribute will mean that tooltip styling effects do not apply.
+
Examples
--------
Basic application
@@ -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
@@ -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
diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py
index 1cf54dc2cc756..fe03ba519629d 100644
--- a/pandas/io/formats/style_render.py
+++ b/pandas/io/formats/style_render.py
@@ -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 based
+ tooltips (False).
+ Add the tooltip text as title attribute to resultant | element. If
+ True, no CSS is generated and styling effects do not apply.
Notes
-----
@@ -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):
@@ -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 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''
- )
- 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''
+ )
+ d["table_styles"].extend(self._class_styles)
+ d["table_styles"].extend(self.table_styles)
+ # this conditional adds tooltips as extra "title" attribute on a | 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
diff --git a/pandas/tests/io/formats/style/test_tooltip.py b/pandas/tests/io/formats/style/test_tooltip.py
index c49a0e05c6700..f1071c949299e 100644
--- a/pandas/tests/io/formats/style/test_tooltip.py
+++ b/pandas/tests/io/formats/style/test_tooltip.py
@@ -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
@@ -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()
@@ -64,6 +65,7 @@ def test_tooltip_ignored(styler):
result = styler.to_html() # no set_tooltips() creates no
assert '' in result
assert '' not in result
+ assert 'title="' not in result
def test_tooltip_css_class(styler):
@@ -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 | ' 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' 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' in result
+ assert 'class="data row1 col0" >3' in result
+ assert 'class="data row1 col1" >4' in result
+ assert 'class="data row1 col2" >5' in result
+ assert 'class="data row2 col0" >6' in result
+ assert 'class="data row2 col1" >7' in result
+ assert 'class="data row2 col2" >8' 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' 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' 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' in result
+ assert 'class="data row1 col0" >3' in result
+ assert 'class="data row1 col1" >4' in result
+ assert 'class="data row1 col2" >5' in result
+ assert 'class="data row2 col0" >6' in result
+ assert 'class="data row2 col1" >7' in result
+ assert 'class="data row2 col2" >8' in result