diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst
index e80dc1b57ff80..3a8d912fa6ffe 100644
--- a/doc/source/reference/style.rst
+++ b/doc/source/reference/style.rst
@@ -39,6 +39,8 @@ Style application
Styler.set_td_classes
Styler.set_table_styles
Styler.set_table_attributes
+ Styler.set_tooltips
+ Styler.set_tooltips_class
Styler.set_caption
Styler.set_properties
Styler.set_uuid
diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst
index ab00b749d5725..6a85bfd852e19 100644
--- a/doc/source/whatsnew/v1.3.0.rst
+++ b/doc/source/whatsnew/v1.3.0.rst
@@ -52,6 +52,7 @@ Other enhancements
- :meth:`DataFrame.apply` can now accept NumPy unary operators as strings, e.g. ``df.apply("sqrt")``, which was already the case for :meth:`Series.apply` (:issue:`39116`)
- :meth:`DataFrame.apply` can now accept non-callable DataFrame properties as strings, e.g. ``df.apply("size")``, which was already the case for :meth:`Series.apply` (:issue:`39116`)
- :meth:`Series.apply` can now accept list-like or dictionary-like arguments that aren't lists or dictionaries, e.g. ``ser.apply(np.array(["sum", "mean"]))``, which was already the case for :meth:`DataFrame.apply` (:issue:`39140`)
+- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes.
.. ---------------------------------------------------------------------------
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index b6c1336ede597..49eb579f9bd99 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -182,6 +182,8 @@ def __init__(
self.cell_ids = cell_ids
self.na_rep = na_rep
+ self.tooltips: Optional[_Tooltips] = None
+
self.cell_context: Dict[str, Any] = {}
# display_funcs maps (row, col) -> formatting function
@@ -205,6 +207,117 @@ def _repr_html_(self) -> str:
"""
return self.render()
+ def _init_tooltips(self):
+ """
+ Checks parameters compatible with tooltips and creates instance if necessary
+ """
+ if not self.cell_ids:
+ # tooltips not optimised for individual cell check. requires reasonable
+ # redesign and more extensive code for a feature that might be rarely used.
+ raise NotImplementedError(
+ "Tooltips can only render with 'cell_ids' is True."
+ )
+ if self.tooltips is None:
+ self.tooltips = _Tooltips()
+
+ def set_tooltips(self, ttips: DataFrame) -> "Styler":
+ """
+ Add string based tooltips that will appear in the `Styler` HTML result. These
+ tooltips are applicable only to`
` elements.
+
+ .. versionadded:: 1.3.0
+
+ Parameters
+ ----------
+ ttips : DataFrame
+ DataFrame containing strings that will be translated to tooltips, mapped
+ by identical column and index values that must exist on the underlying
+ `Styler` data. None, NaN values, and empty strings will be ignored and
+ not affect the rendered HTML.
+
+ Returns
+ -------
+ self : Styler
+
+ Notes
+ -----
+ Tooltips are created by adding `` to each data cell
+ and then manipulating the table level CSS to attach pseudo hover and pseudo
+ after selectors to produce the required the results. For styling control
+ see `:meth:Styler.set_tooltips_class`.
+ Tooltips are not designed to be efficient, and can add large amounts of
+ additional HTML for larger tables, since they also require that `cell_ids`
+ is forced to `True`.
+
+ Examples
+ --------
+ >>> df = pd.DataFrame(data=[[0, 1], [2, 3]])
+ >>> ttips = pd.DataFrame(
+ ... data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index
+ ... )
+ >>> s = df.style.set_tooltips(ttips).render()
+ """
+ self._init_tooltips()
+ assert self.tooltips is not None # mypy requiremen
+ self.tooltips.tt_data = ttips
+ return self
+
+ def set_tooltips_class(
+ self,
+ name: Optional[str] = None,
+ properties: Optional[Sequence[Tuple[str, Union[str, int, float]]]] = None,
+ ) -> "Styler":
+ """
+ Manually configure the name and/or properties of the class for
+ creating tooltips on hover.
+
+ .. versionadded:: 1.3.0
+
+ Parameters
+ ----------
+ name : str, default None
+ Name of the tooltip class used in CSS, should conform to HTML standards.
+ properties : list-like, default None
+ List of (attr, value) tuples; see example.
+
+ Returns
+ -------
+ self : Styler
+
+ Notes
+ -----
+ If arguments are `None` will not make any changes to the underlying ``Tooltips``
+ existing values.
+
+ The default properties for the tooltip CSS class are:
+
+ - visibility: hidden
+ - position: absolute
+ - z-index: 1
+ - background-color: black
+ - color: white
+ - transform: translate(-20px, -20px)
+
+ The property ('visibility', 'hidden') is a key prerequisite to the hover
+ functionality, and should always be included in any manual properties
+ specification.
+
+ Examples
+ --------
+ >>> df = pd.DataFrame(np.random.randn(10, 4))
+ >>> df.style.set_tooltips_class(name='tt-add', properties=[
+ ... ('visibility', 'hidden'),
+ ... ('position', 'absolute'),
+ ... ('z-index', 1)])
+ """
+ self._init_tooltips()
+ assert self.tooltips is not None # mypy requirement
+ if properties:
+ self.tooltips.class_properties = properties
+ if name:
+ self.tooltips.class_name = name
+ return self
+
@doc(
NDFrame.to_excel,
klass="Styler",
@@ -434,7 +547,7 @@ def format_attr(pair):
else:
table_attr += ' class="tex2jax_ignore"'
- return {
+ d = {
"head": head,
"cellstyle": cellstyle,
"body": body,
@@ -444,6 +557,10 @@ def format_attr(pair):
"caption": caption,
"table_attributes": table_attr,
}
+ if self.tooltips:
+ d = self.tooltips._translate(self.data, self.uuid, d)
+
+ return d
def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> "Styler":
"""
@@ -689,6 +806,7 @@ def clear(self) -> None:
Returns None.
"""
self.ctx.clear()
+ self.tooltips = None
self.cell_context = {}
self._todo = []
@@ -1658,6 +1776,179 @@ def pipe(self, func: Callable, *args, **kwargs):
return com.pipe(self, func, *args, **kwargs)
+class _Tooltips:
+ """
+ An extension to ``Styler`` that allows for and manipulates tooltips on hover
+ of table data-cells in the HTML result.
+
+ Parameters
+ ----------
+ css_name: str, default "pd-t"
+ Name of the CSS class that controls visualisation of tooltips.
+ css_props: list-like, default; see Notes
+ List of (attr, value) tuples defining properties of the CSS class.
+ tooltips: DataFrame, default empty
+ DataFrame of strings aligned with underlying ``Styler`` data for tooltip
+ display.
+
+ Notes
+ -----
+ The default properties for the tooltip CSS class are:
+
+ - visibility: hidden
+ - position: absolute
+ - z-index: 1
+ - background-color: black
+ - color: white
+ - transform: translate(-20px, -20px)
+
+ Hidden visibility is a key prerequisite to the hover functionality, and should
+ always be included in any manual properties specification.
+ """
+
+ def __init__(
+ self,
+ css_props: Sequence[Tuple[str, Union[str, int, float]]] = [
+ ("visibility", "hidden"),
+ ("position", "absolute"),
+ ("z-index", 1),
+ ("background-color", "black"),
+ ("color", "white"),
+ ("transform", "translate(-20px, -20px)"),
+ ],
+ css_name: str = "pd-t",
+ tooltips: DataFrame = DataFrame(),
+ ):
+ self.class_name = css_name
+ self.class_properties = css_props
+ self.tt_data = tooltips
+ self.table_styles: List[Dict[str, Union[str, List[Tuple[str, str]]]]] = []
+
+ @property
+ def _class_styles(self):
+ """
+ Combine the ``_Tooltips`` CSS class name and CSS properties to the format
+ required to extend the underlying ``Styler`` `table_styles` to allow
+ tooltips to render in HTML.
+
+ Returns
+ -------
+ styles : List
+ """
+ return [{"selector": f".{self.class_name}", "props": self.class_properties}]
+
+ def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str):
+ """
+ For every table data-cell that has a valid tooltip (not None, NaN or
+ empty string) must create two pseudo CSS entries for the specific
+ | element id which are added to overall table styles:
+ an on hover visibility change and a content change
+ dependent upon the user's chosen display string.
+
+ For example:
+ [{"selector": "T__row1_col1:hover .pd-t",
+ "props": [("visibility", "visible")]},
+ {"selector": "T__row1_col1 .pd-t::after",
+ "props": [("content", "Some Valid Text String")]}]
+
+ Parameters
+ ----------
+ uuid: str
+ The uuid of the Styler instance
+ name: str
+ The css-name of the class used for styling tooltips
+ row : int
+ The row index of the specified tooltip string data
+ col : int
+ The col index of the specified tooltip string data
+ text : str
+ The textual content of the tooltip to be displayed in HTML.
+
+ Returns
+ -------
+ pseudo_css : List
+ """
+ return [
+ {
+ "selector": "#T_"
+ + uuid
+ + "row"
+ + str(row)
+ + "_col"
+ + str(col)
+ + f":hover .{name}",
+ "props": [("visibility", "visible")],
+ },
+ {
+ "selector": "#T_"
+ + uuid
+ + "row"
+ + str(row)
+ + "_col"
+ + str(col)
+ + f" .{name}::after",
+ "props": [("content", f'"{text}"')],
+ },
+ ]
+
+ def _translate(self, styler_data: FrameOrSeriesUnion, uuid: str, d: Dict):
+ """
+ Mutate the render dictionary to allow for tooltips:
+
+ - Add `` HTML element to each data cells `display_value`. Ignores
+ headers.
+ - Add table level CSS styles to control pseudo classes.
+
+ Parameters
+ ----------
+ styler_data : DataFrame
+ Underlying ``Styler`` DataFrame used for reindexing.
+ uuid : str
+ The underlying ``Styler`` uuid for CSS id.
+ d : dict
+ The dictionary prior to final render
+
+ Returns
+ -------
+ render_dict : Dict
+ """
+ self.tt_data = (
+ self.tt_data.reindex_like(styler_data)
+ .dropna(how="all", axis=0)
+ .dropna(how="all", axis=1)
+ )
+ 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(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]
+ ]
+ 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)
+
+ return d
+
+
def _is_visible(idx_row, idx_col, lengths) -> bool:
"""
Index -> {(idx_row, idx_col): bool}).
diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py
index 0bb422658df25..c61d81d565459 100644
--- a/pandas/tests/io/formats/test_style.py
+++ b/pandas/tests/io/formats/test_style.py
@@ -1769,6 +1769,74 @@ def test_uuid_len_raises(self, len_):
with pytest.raises(TypeError, match=msg):
Styler(df, uuid_len=len_, cell_ids=False).render()
+ @pytest.mark.parametrize(
+ "ttips",
+ [
+ DataFrame(
+ data=[["Min", "Max"], [np.nan, ""]],
+ columns=["A", "B"],
+ index=["a", "b"],
+ ),
+ DataFrame(data=[["Max", "Min"]], columns=["B", "A"], index=["a"]),
+ DataFrame(
+ data=[["Min", "Max", None]], columns=["A", "B", "C"], index=["a"]
+ ),
+ ],
+ )
+ def test_tooltip_render(self, ttips):
+ # GH 21266
+ df = DataFrame(data=[[0, 3], [1, 2]], columns=["A", "B"], index=["a", "b"])
+ s = Styler(df, uuid_len=0).set_tooltips(ttips).render()
+
+ # test tooltip table level class
+ assert "#T__ .pd-t {\n visibility: hidden;\n" in s
+
+ # test 'Min' tooltip added
+ assert (
+ "#T__ #T__row0_col0:hover .pd-t {\n visibility: visible;\n } "
+ + ' #T__ #T__row0_col0 .pd-t::after {\n content: "Min";\n }'
+ in s
+ )
+ assert (
+ ' | 0'
+ + " | "
+ in s
+ )
+
+ # test 'Max' tooltip added
+ assert (
+ "#T__ #T__row0_col1:hover .pd-t {\n visibility: visible;\n } "
+ + ' #T__ #T__row0_col1 .pd-t::after {\n content: "Max";\n }'
+ in s
+ )
+ assert (
+ '3'
+ + " | "
+ in s
+ )
+
+ def test_tooltip_ignored(self):
+ # GH 21266
+ df = DataFrame(data=[[0, 1], [2, 3]])
+ s = Styler(df).set_tooltips_class("pd-t").render() # no set_tooltips()
+ assert '' in s
+ assert '' not in s
+
+ def test_tooltip_class(self):
+ # GH 21266
+ df = DataFrame(data=[[0, 1], [2, 3]])
+ s = (
+ Styler(df, uuid_len=0)
+ .set_tooltips(DataFrame([["tooltip"]]))
+ .set_tooltips_class(name="other-class", properties=[("color", "green")])
+ .render()
+ )
+ assert "#T__ .other-class {\n color: green;\n" in s
+ assert (
+ '#T__ #T__row0_col0 .other-class::after {\n content: "tooltip";\n'
+ in s
+ )
+
@td.skip_if_no_mpl
class TestStylerMatplotlibDep: