-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
ENH: Styler tooltips feature #35643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: Styler tooltips feature #35643
Changes from 14 commits
3e83fc6
f8ffbbe
b92d255
cd6c5f2
1f61e87
af83833
078c0bb
eeddd19
6389811
e7557f5
64f789a
0b1e930
5ca86a5
f798852
1e782b7
ee952de
71c3cc1
2c36d4b
4aed112
49ecb50
3510ada
7f76e8e
aa09003
f160297
776c434
c3c0aba
dd69ae6
4055e1b
6b27ec2
8fc8437
a96acf8
50c2aa9
607cfe6
ab079b0
f6e066e
af6401a
0e6b1a3
5829546
eaa851e
9e8612f
5507c4f
d0eefca
c0b004e
e4a5e20
849b1f4
950b3b1
efc7fca
eb7fe68
5a377b8
54c52da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
Tuple, | ||
Union, | ||
) | ||
from uuid import uuid1 | ||
from uuid import uuid4 | ||
|
||
import numpy as np | ||
|
||
|
@@ -159,7 +159,7 @@ def __init__( | |
self.index = data.index | ||
self.columns = data.columns | ||
|
||
self.uuid = uuid | ||
self.uuid = uuid or (uuid4().hex[:5] + "_") | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.table_styles = table_styles | ||
self.caption = caption | ||
if precision is None: | ||
|
@@ -171,6 +171,11 @@ def __init__( | |
self.cell_ids = cell_ids | ||
self.na_rep = na_rep | ||
|
||
self.tooltip_styles: List[Dict[str, object]] = [] # VERSION ADDED 1.X | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.tooltip_class = None | ||
self.tooltip_class_styles = None | ||
self.set_tooltip_class(name="pd-t", properties=None) | ||
|
||
# display_funcs maps (row, col) -> formatting function | ||
|
||
def default_display_func(x): | ||
|
@@ -246,7 +251,7 @@ def _translate(self): | |
precision = self.precision | ||
hidden_index = self.hidden_index | ||
hidden_columns = self.hidden_columns | ||
uuid = self.uuid or str(uuid1()).replace("-", "_") | ||
uuid = self.uuid | ||
ROW_HEADING_CLASS = "row_heading" | ||
COL_HEADING_CLASS = "col_heading" | ||
INDEX_NAME_CLASS = "index_name" | ||
|
@@ -545,6 +550,7 @@ def render(self, **kwargs) -> str: | |
# so we have the nested anys below | ||
trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])] | ||
d["cellstyle"] = trimmed | ||
self._render_tooltips(d) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why wouldn't this happen after the update? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. becuase it is really an extension of the _translate method. |
||
d.update(kwargs) | ||
return self.template.render(**d) | ||
|
||
|
@@ -609,6 +615,8 @@ def clear(self) -> None: | |
Returns None. | ||
""" | ||
self.ctx.clear() | ||
self.tooltip_styles = [] | ||
self.set_tooltip_class(name="pd-t", properties=None) | ||
self._todo = [] | ||
|
||
def _compute(self): | ||
|
@@ -802,6 +810,146 @@ def where( | |
lambda val: value if cond(val) else other, subset=subset, **kwargs | ||
) | ||
|
||
def set_tooltips(self, ttips: DataFrame): | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Add string based tooltips that will appear in the `Styler` HTML result. | ||
|
||
Parameters | ||
---------- | ||
ttips : DataFrame | ||
DataFrame containing strings that will be translated to tooltips. Empty | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
strings, None, or NaN values will be ignored. DataFrame must have | ||
identical rows and columns to the underlying `Styler` data. | ||
|
||
Returns | ||
------- | ||
self : Styler | ||
|
||
Notes | ||
----- | ||
Tooltips are created by adding `<span class="pd-t"></span>` 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` | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
is forced to `True`. | ||
""" | ||
if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)): | ||
raise AttributeError( | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Tooltips DataFrame must have identical column and index labelling " | ||
"to underlying." | ||
) | ||
|
||
self.cell_ids = True # tooltips only work with individual cell_ids | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.tooltip_styles = [] | ||
for i, rn in enumerate(ttips.index): | ||
for j, cn in enumerate(ttips.columns): | ||
if ttips.iloc[i, j] in [np.nan, "", None]: | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
continue | ||
else: | ||
# add pseudo-class and pseudo-elements to css to create tips | ||
self.tooltip_styles.extend( | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[ | ||
{ | ||
"selector": "#T_" | ||
+ self.uuid | ||
+ "row" | ||
+ str(i) | ||
+ "_col" | ||
+ str(j) | ||
+ f":hover .{self.tooltip_class}", | ||
"props": [("visibility", "visible")], | ||
}, | ||
{ | ||
"selector": "#T_" | ||
+ self.uuid | ||
+ "row" | ||
+ str(i) | ||
+ "_col" | ||
+ str(j) | ||
+ f" .{self.tooltip_class}::after", | ||
"props": [("content", f'"{str(ttips.iloc[i, j])}"')], | ||
}, | ||
] | ||
) | ||
|
||
return self | ||
|
||
def set_tooltip_class(self, name="pd-t", properties=None): | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Method to set the name and properties of the class for creating tooltips on | ||
hover. | ||
|
||
Parameters | ||
---------- | ||
name : str, default 'pd-t' | ||
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. If `None` will use defaults. | ||
|
||
Returns | ||
------- | ||
self : Styler | ||
|
||
Notes | ||
----- | ||
Default properties for the tooltip class are as follows: | ||
|
||
- visibility: hidden | ||
- position: absolute | ||
- z-index: 1 | ||
- background-color: black | ||
- color: white | ||
- transform: translate(-20px, -20px) | ||
|
||
Examples | ||
-------- | ||
>>> df = pd.DataFrame(np.random.randn(10, 4)) | ||
>>> df.style.set_tooltip_class(name='tt-add', properties=[ | ||
... ('visibility', 'hidden'), | ||
... ('position', 'absolute'), | ||
... ('z-index', 1)]) | ||
""" | ||
if properties is None: | ||
properties = [ # set default | ||
("visibility", "hidden"), | ||
("position", "absolute"), | ||
("z-index", 1), | ||
("background-color", "black"), | ||
("color", "white"), | ||
("transform", "translate(-20px, -20px)"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we validate the properties? should we? |
||
] | ||
self.tooltip_class = name | ||
|
||
self.tooltip_class_styles = [ | ||
{"selector": f".{self.tooltip_class}", "props": properties} | ||
] | ||
return self | ||
|
||
def _render_tooltips(self, d): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this make sense as a module level free function (where you pass in the styles & d)? |
||
""" | ||
Mutate the render dictionary to allow for tooltips: | ||
|
||
- Add `<span>` HTML element to each data cells `display_value`. Ignores headers. | ||
- Add table level CSS styles to control pseudo classes. | ||
|
||
Parameters | ||
---------- | ||
d : dict | ||
The dictionary prior to rendering | ||
""" | ||
if self.tooltip_styles: | ||
for row in d["body"]: | ||
for item in row: | ||
if item["type"] == "td": | ||
item["display_value"] = ( | ||
str(item["display_value"]) | ||
+ f'<span class="{self.tooltip_class}"></span>' | ||
) | ||
d["table_styles"].extend(self.tooltip_class_styles) | ||
d["table_styles"].extend(self.tooltip_styles) | ||
|
||
def set_precision(self, precision: int) -> "Styler": | ||
""" | ||
Set the precision used to render. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you revert this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i've no real experience with selected git revert, doesn't it just get squashed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
git checkout upstream/master doc/source/user_guide/style.ipynb
and commit the result