Skip to content

Commit a631294

Browse files
committed
ENH: Styler can add tooltips directly from a DataFrame of strings
1 parent aefae55 commit a631294

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

pandas/io/formats/style.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
Tuple,
1919
Union,
2020
)
21-
from uuid import uuid1
21+
from uuid import uuid4
2222

2323
import numpy as np
2424

@@ -159,7 +159,7 @@ def __init__(
159159
self.index = data.index
160160
self.columns = data.columns
161161

162-
self.uuid = uuid
162+
self.uuid = uuid or (uuid4().hex[:6] + "_")
163163
self.table_styles = table_styles
164164
self.caption = caption
165165
if precision is None:
@@ -171,6 +171,11 @@ def __init__(
171171
self.cell_ids = cell_ids
172172
self.na_rep = na_rep
173173

174+
self.tooltip_styles = None # VERSION ADDED 1.X
175+
self.tooltip_class = None
176+
self.tooltip_class_styles = None
177+
self.set_tooltip_class(name='pd-t', properties=None)
178+
174179
# display_funcs maps (row, col) -> formatting function
175180

176181
def default_display_func(x):
@@ -246,7 +251,7 @@ def _translate(self):
246251
precision = self.precision
247252
hidden_index = self.hidden_index
248253
hidden_columns = self.hidden_columns
249-
uuid = self.uuid or str(uuid1()).replace("-", "_")
254+
uuid = self.uuid
250255
ROW_HEADING_CLASS = "row_heading"
251256
COL_HEADING_CLASS = "col_heading"
252257
INDEX_NAME_CLASS = "index_name"
@@ -802,6 +807,124 @@ def where(
802807
lambda val: value if cond(val) else other, subset=subset, **kwargs
803808
)
804809

810+
def set_tooltips(self, ttips: DataFrame):
811+
"""
812+
Add string based tooltips that will appear in the `Styler` HTML result.
813+
814+
Parameters
815+
----------
816+
ttips : DataFrame
817+
DataFrame containing strings that will be translated to tooltips. Empty
818+
strings, None, or NaN values will be ignored. DataFrame must have
819+
identical rows and columns to the underlying `Styler` data.
820+
821+
Returns
822+
-------
823+
self : Styler
824+
825+
Notes
826+
-----
827+
Tooltips are created by adding `<span class="pd-t"></span>` to each data cell
828+
and then manipulating the table level CSS to attach pseudo hover and pseudo after
829+
selectors to produce the required the results. For styling control
830+
see `:meth:Styler.set_tooltips_class`.
831+
Tooltips are not designed to be efficient, and can add large amounts of additional
832+
HTML for larger tables, since they also require that `cell_ids` is forced to `True`.
833+
834+
:param ttips:
835+
:return:
836+
"""
837+
if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)):
838+
raise AttributeError('Tooltips DataFrame must have identical column and index labelling to underlying.')
839+
840+
self.cell_ids = True # tooltips only work with individual cell_ids
841+
self.tooltip_styles = []
842+
for i, rn in enumerate(ttips.index):
843+
for j, cn in enumerate(ttips.columns):
844+
if ttips.iloc[i, j] in [np.nan, '', None]:
845+
continue
846+
else:
847+
# add pseudo-class and pseudo-elements to css to create tips
848+
self.tooltip_styles.extend([
849+
{'selector': '#T_' + self.uuid + 'row' + str(i) + '_col' + str(j) + f':hover .{self.tooltip_class}',
850+
'props': [('visibility', 'visible')]},
851+
{'selector': '#T_' + self.uuid + 'row' + str(i) + '_col' + str(j) + f' .{self.tooltip_class}::after',
852+
'props': [('content', f'"{str(ttips.iloc[i, j])}"')]}])
853+
854+
return self
855+
856+
def set_tooltip_class(self, name='pd-t', properties=None):
857+
"""
858+
Method to set the name and properties of the class for creating tooltips on hover.
859+
860+
Parameters
861+
----------
862+
name : str, default 'pd-t'
863+
Name of the tooltip class used in CSS, should conform to HTML standards.
864+
properties : list-like, default None
865+
List of (attr, value) tuples; see example. If `None` will use defaults.
866+
867+
Returns
868+
-------
869+
self : Styler
870+
871+
Notes
872+
-----
873+
Default properties for the tooltip class are as follows:
874+
875+
- visibility: hidden
876+
- position: absolute
877+
- z-index: 1
878+
- background-color: black
879+
- color: white
880+
- transform: translate(-20px, -20px)
881+
882+
Examples
883+
--------
884+
>>> df = pd.DataFrame(np.random.randn(10, 4))
885+
>>> df.style.set_tooltip_class(name='tt-add', properties=[
886+
... ('visibility', 'hidden'),
887+
... ('position', 'absolute'),
888+
... ('z-index', 1)])
889+
"""
890+
if properties is None:
891+
properties= [ # set default
892+
('visibility', 'hidden'),
893+
('position', 'absolute'),
894+
('z-index', 1),
895+
('background-color', 'black'),
896+
('color', 'white'),
897+
('transform', 'translate(-20px, -20px)')
898+
]
899+
self.tooltip_class = name
900+
901+
self.tooltip_class_styles = [
902+
{'selector': f'.{self.tooltip_class}',
903+
'props': properties
904+
}
905+
]
906+
return self
907+
908+
def _render_tooltips(self, d):
909+
"""
910+
Mutate the render dictionary to allow for tooltips:
911+
912+
- Add `<span>` HTML element to each data cells `display_value`. Ignores headers.
913+
- Add table level CSS styles to control pseudo classes.
914+
915+
Parameters
916+
----------
917+
d : dict
918+
The dictionary prior to rendering
919+
"""
920+
if self.tooltip_styles:
921+
for row in d['body']:
922+
for item in row:
923+
if item['type'] == 'td':
924+
item['display_value'] = str(item['display_value']) + f'<span class="{self.tooltip_class}"></span>'
925+
d['table_styles'].extend(self.tooltip_class_styles)
926+
d['table_styles'].extend(self.tooltip_styles)
927+
805928
def set_precision(self, precision: int) -> "Styler":
806929
"""
807930
Set the precision used to render.

pandas/tests/io/formats/test_style.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,18 @@ def test_no_cell_ids(self):
16881688
s = Styler(df, uuid="_", cell_ids=False).render()
16891689
assert s.find('<td class="data row0 col0" >') != -1
16901690

1691+
def test_tooltip_render(self):
1692+
# GH XXXXX
1693+
df = pd.DataFrame(data=[[0, 1], [2, 3]])
1694+
ttips = pd.DataFrame(data=[['Min', ''], [np.nan, 'Max']], columns=df.columns, index=df.index)
1695+
s = Styler(df, uuid="_").set_tooltips(ttips)
1696+
# test tooltip table level class
1697+
assert "#T__ .pd-t {" in s.render()
1698+
# test 'min' tooltip added
1699+
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.render()
1700+
# test 'max' tooltip added
1701+
assert '#T__ #T__row1_col1:hover .pd-t {\n visibility: visible;\n } #T__ #T__row1_col1 .pd-t::after {\n content: "Max";\n }' in s.render()
1702+
16911703

16921704
@td.skip_if_no_mpl
16931705
class TestStylerMatplotlibDep:

0 commit comments

Comments
 (0)