Skip to content

Commit 67baedd

Browse files
attack68luckyvs1
authored andcommitted
ENH: Styler tooltips feature (pandas-dev#35643)
1 parent 8f0d0d2 commit 67baedd

File tree

4 files changed

+363
-1
lines changed

4 files changed

+363
-1
lines changed

doc/source/reference/style.rst

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Style application
3939
Styler.set_td_classes
4040
Styler.set_table_styles
4141
Styler.set_table_attributes
42+
Styler.set_tooltips
43+
Styler.set_tooltips_class
4244
Styler.set_caption
4345
Styler.set_properties
4446
Styler.set_uuid

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Other enhancements
5252
- :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`)
5353
- :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`)
5454
- :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`)
55+
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes.
5556

5657
.. ---------------------------------------------------------------------------
5758

pandas/io/formats/style.py

+292-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ def __init__(
184184
self.cell_ids = cell_ids
185185
self.na_rep = na_rep
186186

187+
self.tooltips: Optional[_Tooltips] = None
188+
187189
self.cell_context: Dict[str, Any] = {}
188190

189191
# display_funcs maps (row, col) -> formatting function
@@ -207,6 +209,117 @@ def _repr_html_(self) -> str:
207209
"""
208210
return self.render()
209211

212+
def _init_tooltips(self):
213+
"""
214+
Checks parameters compatible with tooltips and creates instance if necessary
215+
"""
216+
if not self.cell_ids:
217+
# tooltips not optimised for individual cell check. requires reasonable
218+
# redesign and more extensive code for a feature that might be rarely used.
219+
raise NotImplementedError(
220+
"Tooltips can only render with 'cell_ids' is True."
221+
)
222+
if self.tooltips is None:
223+
self.tooltips = _Tooltips()
224+
225+
def set_tooltips(self, ttips: DataFrame) -> "Styler":
226+
"""
227+
Add string based tooltips that will appear in the `Styler` HTML result. These
228+
tooltips are applicable only to`<td>` elements.
229+
230+
.. versionadded:: 1.3.0
231+
232+
Parameters
233+
----------
234+
ttips : DataFrame
235+
DataFrame containing strings that will be translated to tooltips, mapped
236+
by identical column and index values that must exist on the underlying
237+
`Styler` data. None, NaN values, and empty strings will be ignored and
238+
not affect the rendered HTML.
239+
240+
Returns
241+
-------
242+
self : Styler
243+
244+
Notes
245+
-----
246+
Tooltips are created by adding `<span class="pd-t"></span>` to each data cell
247+
and then manipulating the table level CSS to attach pseudo hover and pseudo
248+
after selectors to produce the required the results. For styling control
249+
see `:meth:Styler.set_tooltips_class`.
250+
Tooltips are not designed to be efficient, and can add large amounts of
251+
additional HTML for larger tables, since they also require that `cell_ids`
252+
is forced to `True`.
253+
254+
Examples
255+
--------
256+
>>> df = pd.DataFrame(data=[[0, 1], [2, 3]])
257+
>>> ttips = pd.DataFrame(
258+
... data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index
259+
... )
260+
>>> s = df.style.set_tooltips(ttips).render()
261+
"""
262+
self._init_tooltips()
263+
assert self.tooltips is not None # mypy requiremen
264+
self.tooltips.tt_data = ttips
265+
return self
266+
267+
def set_tooltips_class(
268+
self,
269+
name: Optional[str] = None,
270+
properties: Optional[Sequence[Tuple[str, Union[str, int, float]]]] = None,
271+
) -> "Styler":
272+
"""
273+
Manually configure the name and/or properties of the class for
274+
creating tooltips on hover.
275+
276+
.. versionadded:: 1.3.0
277+
278+
Parameters
279+
----------
280+
name : str, default None
281+
Name of the tooltip class used in CSS, should conform to HTML standards.
282+
properties : list-like, default None
283+
List of (attr, value) tuples; see example.
284+
285+
Returns
286+
-------
287+
self : Styler
288+
289+
Notes
290+
-----
291+
If arguments are `None` will not make any changes to the underlying ``Tooltips``
292+
existing values.
293+
294+
The default properties for the tooltip CSS class are:
295+
296+
- visibility: hidden
297+
- position: absolute
298+
- z-index: 1
299+
- background-color: black
300+
- color: white
301+
- transform: translate(-20px, -20px)
302+
303+
The property ('visibility', 'hidden') is a key prerequisite to the hover
304+
functionality, and should always be included in any manual properties
305+
specification.
306+
307+
Examples
308+
--------
309+
>>> df = pd.DataFrame(np.random.randn(10, 4))
310+
>>> df.style.set_tooltips_class(name='tt-add', properties=[
311+
... ('visibility', 'hidden'),
312+
... ('position', 'absolute'),
313+
... ('z-index', 1)])
314+
"""
315+
self._init_tooltips()
316+
assert self.tooltips is not None # mypy requirement
317+
if properties:
318+
self.tooltips.class_properties = properties
319+
if name:
320+
self.tooltips.class_name = name
321+
return self
322+
210323
@doc(
211324
NDFrame.to_excel,
212325
klass="Styler",
@@ -436,7 +549,7 @@ def format_attr(pair):
436549
else:
437550
table_attr += ' class="tex2jax_ignore"'
438551

439-
return {
552+
d = {
440553
"head": head,
441554
"cellstyle": cellstyle,
442555
"body": body,
@@ -446,6 +559,10 @@ def format_attr(pair):
446559
"caption": caption,
447560
"table_attributes": table_attr,
448561
}
562+
if self.tooltips:
563+
d = self.tooltips._translate(self.data, self.uuid, d)
564+
565+
return d
449566

450567
def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> Styler:
451568
"""
@@ -691,6 +808,7 @@ def clear(self) -> None:
691808
Returns None.
692809
"""
693810
self.ctx.clear()
811+
self.tooltips = None
694812
self.cell_context = {}
695813
self._todo = []
696814

@@ -1660,6 +1778,179 @@ def pipe(self, func: Callable, *args, **kwargs):
16601778
return com.pipe(self, func, *args, **kwargs)
16611779

16621780

1781+
class _Tooltips:
1782+
"""
1783+
An extension to ``Styler`` that allows for and manipulates tooltips on hover
1784+
of table data-cells in the HTML result.
1785+
1786+
Parameters
1787+
----------
1788+
css_name: str, default "pd-t"
1789+
Name of the CSS class that controls visualisation of tooltips.
1790+
css_props: list-like, default; see Notes
1791+
List of (attr, value) tuples defining properties of the CSS class.
1792+
tooltips: DataFrame, default empty
1793+
DataFrame of strings aligned with underlying ``Styler`` data for tooltip
1794+
display.
1795+
1796+
Notes
1797+
-----
1798+
The default properties for the tooltip CSS class are:
1799+
1800+
- visibility: hidden
1801+
- position: absolute
1802+
- z-index: 1
1803+
- background-color: black
1804+
- color: white
1805+
- transform: translate(-20px, -20px)
1806+
1807+
Hidden visibility is a key prerequisite to the hover functionality, and should
1808+
always be included in any manual properties specification.
1809+
"""
1810+
1811+
def __init__(
1812+
self,
1813+
css_props: Sequence[Tuple[str, Union[str, int, float]]] = [
1814+
("visibility", "hidden"),
1815+
("position", "absolute"),
1816+
("z-index", 1),
1817+
("background-color", "black"),
1818+
("color", "white"),
1819+
("transform", "translate(-20px, -20px)"),
1820+
],
1821+
css_name: str = "pd-t",
1822+
tooltips: DataFrame = DataFrame(),
1823+
):
1824+
self.class_name = css_name
1825+
self.class_properties = css_props
1826+
self.tt_data = tooltips
1827+
self.table_styles: List[Dict[str, Union[str, List[Tuple[str, str]]]]] = []
1828+
1829+
@property
1830+
def _class_styles(self):
1831+
"""
1832+
Combine the ``_Tooltips`` CSS class name and CSS properties to the format
1833+
required to extend the underlying ``Styler`` `table_styles` to allow
1834+
tooltips to render in HTML.
1835+
1836+
Returns
1837+
-------
1838+
styles : List
1839+
"""
1840+
return [{"selector": f".{self.class_name}", "props": self.class_properties}]
1841+
1842+
def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str):
1843+
"""
1844+
For every table data-cell that has a valid tooltip (not None, NaN or
1845+
empty string) must create two pseudo CSS entries for the specific
1846+
<td> element id which are added to overall table styles:
1847+
an on hover visibility change and a content change
1848+
dependent upon the user's chosen display string.
1849+
1850+
For example:
1851+
[{"selector": "T__row1_col1:hover .pd-t",
1852+
"props": [("visibility", "visible")]},
1853+
{"selector": "T__row1_col1 .pd-t::after",
1854+
"props": [("content", "Some Valid Text String")]}]
1855+
1856+
Parameters
1857+
----------
1858+
uuid: str
1859+
The uuid of the Styler instance
1860+
name: str
1861+
The css-name of the class used for styling tooltips
1862+
row : int
1863+
The row index of the specified tooltip string data
1864+
col : int
1865+
The col index of the specified tooltip string data
1866+
text : str
1867+
The textual content of the tooltip to be displayed in HTML.
1868+
1869+
Returns
1870+
-------
1871+
pseudo_css : List
1872+
"""
1873+
return [
1874+
{
1875+
"selector": "#T_"
1876+
+ uuid
1877+
+ "row"
1878+
+ str(row)
1879+
+ "_col"
1880+
+ str(col)
1881+
+ f":hover .{name}",
1882+
"props": [("visibility", "visible")],
1883+
},
1884+
{
1885+
"selector": "#T_"
1886+
+ uuid
1887+
+ "row"
1888+
+ str(row)
1889+
+ "_col"
1890+
+ str(col)
1891+
+ f" .{name}::after",
1892+
"props": [("content", f'"{text}"')],
1893+
},
1894+
]
1895+
1896+
def _translate(self, styler_data: FrameOrSeriesUnion, uuid: str, d: Dict):
1897+
"""
1898+
Mutate the render dictionary to allow for tooltips:
1899+
1900+
- Add `<span>` HTML element to each data cells `display_value`. Ignores
1901+
headers.
1902+
- Add table level CSS styles to control pseudo classes.
1903+
1904+
Parameters
1905+
----------
1906+
styler_data : DataFrame
1907+
Underlying ``Styler`` DataFrame used for reindexing.
1908+
uuid : str
1909+
The underlying ``Styler`` uuid for CSS id.
1910+
d : dict
1911+
The dictionary prior to final render
1912+
1913+
Returns
1914+
-------
1915+
render_dict : Dict
1916+
"""
1917+
self.tt_data = (
1918+
self.tt_data.reindex_like(styler_data)
1919+
.dropna(how="all", axis=0)
1920+
.dropna(how="all", axis=1)
1921+
)
1922+
if self.tt_data.empty:
1923+
return d
1924+
1925+
name = self.class_name
1926+
1927+
mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip
1928+
self.table_styles = [
1929+
style
1930+
for sublist in [
1931+
self._pseudo_css(uuid, name, i, j, str(self.tt_data.iloc[i, j]))
1932+
for i in range(len(self.tt_data.index))
1933+
for j in range(len(self.tt_data.columns))
1934+
if not mask.iloc[i, j]
1935+
]
1936+
for style in sublist
1937+
]
1938+
1939+
if self.table_styles:
1940+
# add span class to every cell only if at least 1 non-empty tooltip
1941+
for row in d["body"]:
1942+
for item in row:
1943+
if item["type"] == "td":
1944+
item["display_value"] = (
1945+
str(item["display_value"])
1946+
+ f'<span class="{self.class_name}"></span>'
1947+
)
1948+
d["table_styles"].extend(self._class_styles)
1949+
d["table_styles"].extend(self.table_styles)
1950+
1951+
return d
1952+
1953+
16631954
def _is_visible(idx_row, idx_col, lengths) -> bool:
16641955
"""
16651956
Index -> {(idx_row, idx_col): bool}).

0 commit comments

Comments
 (0)