Skip to content

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

Merged
merged 50 commits into from
Jan 17, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3e83fc6
ENH: Styler can add tooltips directly from a DataFrame of strings
attack68 Aug 9, 2020
f8ffbbe
ENH: black fix
attack68 Aug 9, 2020
b92d255
ENH: test to ensure tooltip class ignored if set_tooltips not called.
attack68 Aug 9, 2020
cd6c5f2
ENH: pep8 line length
attack68 Aug 9, 2020
1f61e87
ENH: pep8 line length
attack68 Aug 9, 2020
af83833
ENH: test fixes: Linting and Typing
attack68 Aug 9, 2020
078c0bb
ENH: add in calling the render step!
attack68 Aug 9, 2020
eeddd19
ENH: mypy Type specification changes
attack68 Aug 9, 2020
6389811
improve tests, integrate into styler.clear()
attack68 Aug 18, 2020
e7557f5
add GH
attack68 Aug 18, 2020
64f789a
black fix
attack68 Aug 18, 2020
0b1e930
update docs
attack68 Aug 18, 2020
5ca86a5
update docs
attack68 Aug 18, 2020
f798852
improve test
attack68 Aug 20, 2020
1e782b7
whats new, typing, and list comps
attack68 Sep 6, 2020
ee952de
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 6, 2020
71c3cc1
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 9, 2020
2c36d4b
reindex instead of same shape. add test cases.
attack68 Sep 9, 2020
4aed112
black fix
attack68 Sep 9, 2020
49ecb50
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 13, 2020
3510ada
revert uuid
attack68 Sep 13, 2020
7f76e8e
revert docs on uuid
attack68 Sep 13, 2020
aa09003
tooltip in individual class architecture
attack68 Sep 14, 2020
f160297
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 14, 2020
776c434
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 17, 2020
c3c0aba
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 19, 2020
dd69ae6
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 20, 2020
4055e1b
fix test after recent merge master
attack68 Sep 20, 2020
6b27ec2
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 21, 2020
8fc8437
revert style.ipynb
attack68 Sep 22, 2020
a96acf8
annotate
attack68 Sep 22, 2020
50c2aa9
improve docs example
attack68 Sep 22, 2020
607cfe6
black pandas
attack68 Sep 22, 2020
ab079b0
Update pandas/io/formats/style.py
attack68 Sep 23, 2020
f6e066e
sequence type change
attack68 Sep 23, 2020
af6401a
doc notes for users
attack68 Sep 23, 2020
0e6b1a3
avoid var name clash
attack68 Sep 23, 2020
5829546
enumerate to range
attack68 Sep 25, 2020
eaa851e
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 25, 2020
9e8612f
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 28, 2020
5507c4f
pre commit errors
attack68 Sep 28, 2020
d0eefca
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Sep 29, 2020
c0b004e
merged into upstream/master
attack68 Jan 5, 2021
e4a5e20
requested changes jreback
attack68 Jan 5, 2021
849b1f4
requested changes jreback
attack68 Jan 5, 2021
950b3b1
requested changes jreback
attack68 Jan 5, 2021
efc7fca
pre commit fails fix
attack68 Jan 5, 2021
eb7fe68
minor requests
attack68 Jan 13, 2021
5a377b8
resolve duplicate typing
attack68 Jan 16, 2021
54c52da
Merge remote-tracking branch 'upstream/master' into styler_tooltips_f…
attack68 Jan 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The `row0_col2` is the identifier for that particular cell. We've also prepended each row/column identifier with a UUID unique to each DataFrame so that the style from one doesn't collide with the styling from another within the same notebook or page (you can set the `uuid` if you'd like to tie together the styling of two DataFrames).\n",
"The `row0_col2` is the identifier for that particular cell. We've also prepended each row/column identifier with part of a universally unique identifier (UUID) having 20-bits of randomness to each DataFrame so that the style from one doesn't collide with the styling from another within the same notebook or page (you can set the `uuid` if you'd like to tie together the styling of two DataFrames, or if for any reason you need more randomness).\n",
"\n",
"When writing style functions, you take care of producing the CSS attribute / value pairs you want. Pandas matches those up with the CSS classes that identify each cell."
]
Expand Down Expand Up @@ -1241,4 +1241,4 @@
},
"nbformat": 4,
"nbformat_minor": 1
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you revert this

Copy link
Contributor Author

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?

Copy link
Member

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

154 changes: 151 additions & 3 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Tuple,
Union,
)
from uuid import uuid1
from uuid import uuid4

import numpy as np

Expand Down Expand Up @@ -159,7 +159,7 @@ def __init__(
self.index = data.index
self.columns = data.columns

self.uuid = uuid
self.uuid = uuid or (uuid4().hex[:5] + "_")
self.table_styles = table_styles
self.caption = caption
if precision is None:
Expand All @@ -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
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):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why wouldn't this happen after the update?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -802,6 +810,146 @@ def where(
lambda val: value if cond(val) else other, subset=subset, **kwargs
)

def set_tooltips(self, ttips: DataFrame):
"""
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
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`
is forced to `True`.
"""
if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)):
raise AttributeError(
"Tooltips DataFrame must have identical column and index labelling "
"to underlying."
)

self.cell_ids = True # tooltips only work with individual cell_ids
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]:
continue
else:
# add pseudo-class and pseudo-elements to css to create tips
self.tooltip_styles.extend(
[
{
"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):
"""
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)"),
Copy link
Contributor

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Expand Down
39 changes: 39 additions & 0 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,45 @@ def test_no_cell_ids(self):
s = styler.render() # render twice to ensure ctx is not updated
assert s.find('<td class="data row0 col0" >') != -1

def test_tooltip_render(self):
# GH 21266
df = pd.DataFrame(data=[[0, 1], [2, 3]])
ttips = pd.DataFrame(
data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index
)
s = Styler(df, uuid="_").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 (
'<td id="T__row0_col0" class="data row0 col0" >0<span class="pd-t">'
+ "</span></td>"
in s
)
# test 'Max' tooltip added
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
)
assert (
'<td id="T__row1_col1" class="data row1 col1" >3<span class="pd-t">'
+ "</span></td>"
in s
)

def test_tooltip_ignored(self):
# GH 21266
df = pd.DataFrame(data=[[0, 1], [2, 3]])
s = Styler(df, uuid="_").set_tooltip_class("pd-t").render() # no set_tooltips()
assert '<style type="text/css" >\n</style>' in s
assert '<span class="pd-t"></span>' not in s


@td.skip_if_no_mpl
class TestStylerMatplotlibDep:
Expand Down