From 3e83fc6ab8e74d4a7ec20492f88f6cf59ea70223 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 13:53:32 +0200 Subject: [PATCH 01/37] ENH: Styler can add tooltips directly from a DataFrame of strings --- pandas/io/formats/style.py | 129 +++++++++++++++++++++++++- pandas/tests/io/formats/test_style.py | 12 +++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3bbb5271bce61..ecd12c496a597 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -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[:6] + "_") 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 = None # 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): @@ -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" @@ -802,6 +807,124 @@ 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 `` 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`. + + :param ttips: + :return: + """ + 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)') + ] + self.tooltip_class = name + + self.tooltip_class_styles = [ + {'selector': f'.{self.tooltip_class}', + 'props': properties + } + ] + return self + + def _render_tooltips(self, d): + """ + 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 + ---------- + 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'' + 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. diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 6025649e9dbec..e9aba017cac39 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1691,6 +1691,18 @@ def test_no_cell_ids(self): s = styler.render() # render twice to ensure ctx is not updated assert s.find('') != -1 + def test_tooltip_render(self): + # GH XXXXX + 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) + # test tooltip table level class + assert "#T__ .pd-t {" in s.render() + # 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.render() + # 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.render() + @td.skip_if_no_mpl class TestStylerMatplotlibDep: From f8ffbbe2f81506a507cfcbf6735bc7dbec36d594 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 13:57:14 +0200 Subject: [PATCH 02/37] ENH: black fix --- pandas/io/formats/style.py | 70 ++++++++++++++++++--------- pandas/tests/io/formats/test_style.py | 14 ++++-- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ecd12c496a597..2fe953dce1773 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -174,7 +174,7 @@ def __init__( self.tooltip_styles = None # VERSION ADDED 1.X self.tooltip_class = None self.tooltip_class_styles = None - self.set_tooltip_class(name='pd-t', properties=None) + self.set_tooltip_class(name="pd-t", properties=None) # display_funcs maps (row, col) -> formatting function @@ -835,25 +835,46 @@ def set_tooltips(self, ttips: DataFrame): :return: """ 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.') + 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]: + 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])}"')]}]) + 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): + 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. @@ -888,20 +909,18 @@ def set_tooltip_class(self, name='pd-t', properties=None): ... ('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)') + properties = [ # set default + ("visibility", "hidden"), + ("position", "absolute"), + ("z-index", 1), + ("background-color", "black"), + ("color", "white"), + ("transform", "translate(-20px, -20px)"), ] self.tooltip_class = name self.tooltip_class_styles = [ - {'selector': f'.{self.tooltip_class}', - 'props': properties - } + {"selector": f".{self.tooltip_class}", "props": properties} ] return self @@ -918,12 +937,15 @@ def _render_tooltips(self, d): The dictionary prior to rendering """ if self.tooltip_styles: - for row in d['body']: + 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.tooltip_class_styles) - d['table_styles'].extend(self.tooltip_styles) + if item["type"] == "td": + item["display_value"] = ( + str(item["display_value"]) + + f'' + ) + d["table_styles"].extend(self.tooltip_class_styles) + d["table_styles"].extend(self.tooltip_styles) def set_precision(self, precision: int) -> "Styler": """ diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index e9aba017cac39..15e070d89b117 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1694,14 +1694,22 @@ def test_no_cell_ids(self): def test_tooltip_render(self): # GH XXXXX df = pd.DataFrame(data=[[0, 1], [2, 3]]) - ttips = pd.DataFrame(data=[['Min', ''], [np.nan, 'Max']], columns=df.columns, index=df.index) + ttips = pd.DataFrame( + data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index + ) s = Styler(df, uuid="_").set_tooltips(ttips) # test tooltip table level class assert "#T__ .pd-t {" in s.render() # 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.render() + 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() + ) # 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.render() + 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() + ) @td.skip_if_no_mpl From b92d2558fb0c897373e081a51373aa397b12802b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 14:21:24 +0200 Subject: [PATCH 03/37] ENH: test to ensure tooltip class ignored if set_tooltips not called. --- pandas/tests/io/formats/test_style.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 15e070d89b117..7c6b8418de42f 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1702,15 +1702,23 @@ def test_tooltip_render(self): assert "#T__ .pd-t {" in s.render() # 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 }' + "#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() ) # 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 }' + "#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() ) + def test_tooltip_ignored(self): + # GH XXXXX + df = pd.DataFrame(data=[[0, 1], [2, 3]]) + s = Styler(df, uuid="_") # no set_tooltips() + assert '' in s.render() + @td.skip_if_no_mpl class TestStylerMatplotlibDep: From cd6c5f22cbfe797e98ab9bc2e90f0aeb80359f26 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 14:44:46 +0200 Subject: [PATCH 04/37] ENH: pep8 line length --- pandas/io/formats/style.py | 12 +++++++----- pandas/tests/io/formats/test_style.py | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2fe953dce1773..3ba6b97afb4a1 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -825,11 +825,12 @@ def set_tooltips(self, ttips: DataFrame): 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 + 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`. + 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`. :param ttips: :return: @@ -876,7 +877,8 @@ def set_tooltips(self, ttips: DataFrame): 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. + Method to set the name and properties of the class for creating tooltips on + hover. Parameters ---------- diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 7c6b8418de42f..c219d14f46753 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1702,14 +1702,14 @@ def test_tooltip_render(self): assert "#T__ .pd-t {" in s.render() # 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 }' + "#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() ) # 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 }' + "#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() ) From 1f61e874024072b6763d8a1e7a1b1d65610bdcec Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 14:46:58 +0200 Subject: [PATCH 05/37] ENH: pep8 line length --- pandas/io/formats/style.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3ba6b97afb4a1..07b7d48c31b33 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -837,7 +837,8 @@ def set_tooltips(self, ttips: DataFrame): """ 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." + "Tooltips DataFrame must have identical column and index labelling" + " to underlying." ) self.cell_ids = True # tooltips only work with individual cell_ids From af83833a42f189a0149c3789687b82e01cb6914d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 15:42:10 +0200 Subject: [PATCH 06/37] ENH: test fixes: Linting and Typing --- pandas/io/formats/style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 07b7d48c31b33..2934e183fe617 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,7 +171,7 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep - self.tooltip_styles = None # VERSION ADDED 1.X + self.tooltip_styles = [] # VERSION ADDED 1.X self.tooltip_class = None self.tooltip_class_styles = None self.set_tooltip_class(name="pd-t", properties=None) @@ -837,8 +837,8 @@ def set_tooltips(self, ttips: DataFrame): """ 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." + "Tooltips DataFrame must have identical column and index labelling " + "to underlying." ) self.cell_ids = True # tooltips only work with individual cell_ids From 078c0bbf3ee42d5b091fc06e62ebcedc619ecf67 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 16:05:40 +0200 Subject: [PATCH 07/37] ENH: add in calling the render step! --- pandas/io/formats/style.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2934e183fe617..e6d36ac28f43c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -550,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) d.update(kwargs) return self.template.render(**d) From eeddd19e3e638415e377c8487463c68fab3376f4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Aug 2020 16:36:20 +0200 Subject: [PATCH 08/37] ENH: mypy Type specification changes --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e6d36ac28f43c..57d34778266ce 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,7 +171,7 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep - self.tooltip_styles = [] # VERSION ADDED 1.X + 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) From 6389811712d095434a75257366944e99e5d904ed Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 18 Aug 2020 08:05:35 +0200 Subject: [PATCH 09/37] improve tests, integrate into styler.clear() --- pandas/io/formats/style.py | 2 ++ pandas/tests/io/formats/test_style.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 57d34778266ce..70c3ee24eddb9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -615,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): diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index c219d14f46753..1d476c508ffd0 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1697,20 +1697,28 @@ def test_tooltip_render(self): ttips = pd.DataFrame( data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index ) - s = Styler(df, uuid="_").set_tooltips(ttips) + s = Styler(df, uuid="_").set_tooltips(ttips).render() # test tooltip table level class - assert "#T__ .pd-t {" in s.render() - # test 'min' tooltip added + 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.render() + in s ) - # test 'max' tooltip added + assert ( + '0' + 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.render() + in s + ) + assert ( + '3' + in s ) def test_tooltip_ignored(self): From e7557f55617a68bd26c97695d04bf1fec6d03391 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 18 Aug 2020 11:51:30 +0200 Subject: [PATCH 10/37] add GH --- pandas/tests/io/formats/test_style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 1d476c508ffd0..647f5c1aed5d4 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1692,7 +1692,7 @@ def test_no_cell_ids(self): assert s.find('') != -1 def test_tooltip_render(self): - # GH XXXXX + # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) ttips = pd.DataFrame( data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index @@ -1722,7 +1722,7 @@ def test_tooltip_render(self): ) def test_tooltip_ignored(self): - # GH XXXXX + # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) s = Styler(df, uuid="_") # no set_tooltips() assert '' in s.render() From 64f789a067b5fb255333eb70aa18974282df6d08 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 18 Aug 2020 13:18:26 +0200 Subject: [PATCH 11/37] black fix --- pandas/tests/io/formats/test_style.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 647f5c1aed5d4..a6020897f3de4 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1707,7 +1707,8 @@ def test_tooltip_render(self): in s ) assert ( - '0' + '0' + + "" in s ) # test 'Max' tooltip added @@ -1717,7 +1718,8 @@ def test_tooltip_render(self): in s ) assert ( - '3' + '3' + + "" in s ) From 0b1e93054a63364738dafa31535287cd7b30b515 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 18 Aug 2020 16:29:15 +0200 Subject: [PATCH 12/37] update docs --- doc/source/user_guide/style.ipynb | 2 +- pandas/io/formats/style.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 77a1fef28f373..640f06e58fab0 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -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 16-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." ] diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 70c3ee24eddb9..00edb25c104f7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -159,7 +159,7 @@ def __init__( self.index = data.index self.columns = data.columns - self.uuid = uuid or (uuid4().hex[:6] + "_") + self.uuid = uuid or (uuid4().hex[:4] + "_") self.table_styles = table_styles self.caption = caption if precision is None: From 5ca86a5037d0056b03ea62709e1285cc6a91b118 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 18 Aug 2020 16:44:21 +0200 Subject: [PATCH 13/37] update docs --- doc/source/user_guide/style.ipynb | 4 ++-- pandas/io/formats/style.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 640f06e58fab0..ada1ef73da577 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -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 part of a universally unique identifier (UUID) having 16-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", + "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." ] @@ -1241,4 +1241,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} +} \ No newline at end of file diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 00edb25c104f7..a6cfa7155e0fa 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -159,7 +159,7 @@ def __init__( self.index = data.index self.columns = data.columns - self.uuid = uuid or (uuid4().hex[:4] + "_") + self.uuid = uuid or (uuid4().hex[:5] + "_") self.table_styles = table_styles self.caption = caption if precision is None: @@ -834,9 +834,6 @@ def set_tooltips(self, ttips: DataFrame): 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`. - - :param ttips: - :return: """ if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)): raise AttributeError( From f7988520f9cc97f2dd5a28cfb5d47aab4eb5b64f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 20 Aug 2020 07:47:00 +0200 Subject: [PATCH 14/37] improve test --- pandas/tests/io/formats/test_style.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index a6020897f3de4..9cad38293b262 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1726,8 +1726,9 @@ def test_tooltip_render(self): def test_tooltip_ignored(self): # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) - s = Styler(df, uuid="_") # no set_tooltips() - assert '' in s.render() + s = Styler(df, uuid="_").set_tooltip_class("pd-t").render() # no set_tooltips() + assert '' in s + assert '' not in s @td.skip_if_no_mpl From 1e782b7c787acd71816242d84905ef3ec369e271 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 6 Sep 2020 10:37:14 +0200 Subject: [PATCH 15/37] whats new, typing, and list comps --- doc/source/whatsnew/v1.2.0.rst | 2 +- pandas/io/formats/style.py | 92 ++++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 55570341cf4e8..d96c76f67c39b 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -54,7 +54,7 @@ For example: Other enhancements ^^^^^^^^^^^^^^^^^^ - :class:`Index` with object dtype supports division and multiplication (:issue:`34160`) -- +- `Styler` now has the ability to add tooltips to styled dataframes from strings (:issue:`35643`) - .. _whatsnew_120.api_breaking.python: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a6cfa7155e0fa..cef8ec6d7e547 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,9 +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.tooltip_styles: List[Dict[str, object]] = [] + self.tooltip_class: str = "" + self.tooltip_class_styles: List[ + Union[Dict[str, str], Dict[str, List[Tuple[str, str]]]] + ] = [] self.set_tooltip_class(name="pd-t", properties=None) # display_funcs maps (row, col) -> formatting function @@ -810,7 +812,7 @@ def where( lambda val: value if cond(val) else other, subset=subset, **kwargs ) - def set_tooltips(self, ttips: DataFrame): + def set_tooltips(self, ttips: DataFrame) -> "Styler": """ Add string based tooltips that will appear in the `Styler` HTML result. @@ -834,49 +836,63 @@ def set_tooltips(self, ttips: DataFrame): 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 = Styler(df, uuid="_").set_tooltips(ttips).render() """ if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)): - raise AttributeError( + raise ValueError( "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])}"')], - }, - ] - ) + if not self.cell_ids: + # tooltips not optimised for individual cell check. + raise NotImplementedError( + "Tooltips can only render with 'cell_ids' is True." + ) + + mask = (ttips.isna()) | (ttips.eq("")) + self.tooltip_styles = [ + d + for sublist in [ + [ + { + "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])}"')], + }, + ] + for i, rn in enumerate(ttips.index) + for j, cn in enumerate(ttips.columns) + if not mask.iloc[i, j] + ] + for d in sublist + ] return self - def set_tooltip_class(self, name="pd-t", properties=None): + def set_tooltip_class(self, name="pd-t", properties=None) -> "Styler": """ Method to set the name and properties of the class for creating tooltips on hover. From 2c36d4bd7925eaacd8fb20c2488a2293a321b9e4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Sep 2020 08:23:18 +0200 Subject: [PATCH 16/37] reindex instead of same shape. add test cases. --- pandas/io/formats/style.py | 18 ++++++++---------- pandas/tests/io/formats/test_style.py | 25 +++++++++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0e5f81a5af0b2..30bec2fdc7d9a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -814,14 +814,16 @@ def where( def set_tooltips(self, ttips: DataFrame) -> "Styler": """ - Add string based tooltips that will appear in the `Styler` HTML result. + Add string based tooltips that will appear in the `Styler` HTML result. These + tooltips are applicable only to`` elements. 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. + 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 ------- @@ -845,18 +847,14 @@ def set_tooltips(self, ttips: DataFrame) -> "Styler": ... ) >>> s = Styler(df, uuid="_").set_tooltips(ttips).render() """ - if not (self.columns.equals(ttips.columns) and self.index.equals(ttips.index)): - raise ValueError( - "Tooltips DataFrame must have identical column and index labelling " - "to underlying." - ) - if not self.cell_ids: # tooltips not optimised for individual cell check. raise NotImplementedError( "Tooltips can only render with 'cell_ids' is True." ) + ttips = ttips.reindex_like(self.data) + mask = (ttips.isna()) | (ttips.eq("")) self.tooltip_styles = [ d diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 3d4aaac8190e5..cee7dbac4cc06 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1697,12 +1697,21 @@ def test_colspan_w3(self): s = Styler(df, uuid="_", cell_ids=False) assert 'l0' in s.render() - def test_tooltip_render(self): + @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 = pd.DataFrame(data=[[0, 1], [2, 3]]) - ttips = pd.DataFrame( - data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index - ) + df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=['A', 'B'], index=['a', 'b']) s = Styler(df, uuid="_").set_tooltips(ttips).render() # test tooltip table level class assert "#T__ .pd-t {\n visibility: hidden;\n" in s @@ -1719,12 +1728,12 @@ def test_tooltip_render(self): ) # 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 }' + "#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' + '3' + "" in s ) From 4aed112a427cb29e2c63b6e86022f972ef885290 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 9 Sep 2020 08:27:57 +0200 Subject: [PATCH 17/37] black fix --- pandas/tests/io/formats/test_style.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index cee7dbac4cc06..f9f711d9d8e9a 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1706,12 +1706,14 @@ def test_colspan_w3(self): index=["a", "b"], ), DataFrame(data=[["Max", "Min"]], columns=["B", "A"], index=["a"]), - DataFrame(data=[["Min", "Max", None]], columns=["A", "B", "C"], index=["a"]) + DataFrame( + data=[["Min", "Max", None]], columns=["A", "B", "C"], index=["a"] + ), ], ) def test_tooltip_render(self, ttips): # GH 21266 - df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=['A', 'B'], index=['a', 'b']) + df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=["A", "B"], index=["a", "b"]) s = Styler(df, uuid="_").set_tooltips(ttips).render() # test tooltip table level class assert "#T__ .pd-t {\n visibility: hidden;\n" in s From 3510adabf7c187872c33ed898f6892a01357b478 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Sep 2020 13:47:41 +0200 Subject: [PATCH 18/37] revert uuid --- pandas/io/formats/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 30bec2fdc7d9a..3d5facbb5fcc5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -18,7 +18,7 @@ Tuple, Union, ) -from uuid import uuid4 +from uuid import uuid1 import numpy as np @@ -159,7 +159,7 @@ def __init__( self.index = data.index self.columns = data.columns - self.uuid = uuid or (uuid4().hex[:5] + "_") + self.uuid = uuid or str(uuid1()).replace("-", "_") self.table_styles = table_styles self.caption = caption if precision is None: From 7f76e8ed35ca7d5b649f8db25cebf6b6230cb023 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 13 Sep 2020 14:16:16 +0200 Subject: [PATCH 19/37] revert docs on uuid --- doc/source/user_guide/style.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index ada1ef73da577..44d0130f29b43 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -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 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", + "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", "\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." ] From aa090038095f98fddf91cb2a353bbdd5262960b6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 14 Sep 2020 13:51:01 +0200 Subject: [PATCH 20/37] tooltip in individual class architecture --- pandas/io/formats/style.py | 391 +++++++++++++++----------- pandas/tests/io/formats/test_style.py | 22 +- 2 files changed, 251 insertions(+), 162 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3d5facbb5fcc5..40b829239967f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -171,12 +171,7 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep - self.tooltip_styles: List[Dict[str, object]] = [] - self.tooltip_class: str = "" - self.tooltip_class_styles: List[ - Union[Dict[str, str], Dict[str, List[Tuple[str, str]]]] - ] = [] - self.set_tooltip_class(name="pd-t", properties=None) + self.tooltips = _Tooltips() # display_funcs maps (row, col) -> formatting function @@ -199,6 +194,91 @@ def _repr_html_(self) -> str: """ return self.render() + 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. + + 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 = Styler(df, uuid="_").set_tooltips(ttips).render() + """ + if not self.cell_ids: + # tooltips not optimised for individual cell check. + raise NotImplementedError( + "Tooltips can only render with 'cell_ids' is True." + ) + self.tooltips.tt_data = ttips + return self + + def set_tooltips_class( + self, + name: Optional[str] = None, + properties: Optional[List[Tuple[str, Union[str, int, float]]]] = None, + ) -> "Styler": + """ + Manually configure the name and/or properties of the class for + creating tooltips on hover. + + 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 property ('visibility', 'hidden') should always be included in any manual + properties specification, to allow for HTML on hover functionality. + + 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)]) + """ + if properties: + self.tooltips.class_properties = properties + if name: + self.tooltips.class_name = name + return self + @doc(NDFrame.to_excel, klass="Styler") def to_excel( self, @@ -547,12 +627,12 @@ def render(self, **kwargs) -> str: self._compute() # TODO: namespace all the pandas keys d = self._translate() + d = self.tooltips._translate_tooltips(self.data, self.uuid, d) # filter out empty styles, every cell will have a class # but the list of props may just be [['', '']]. # 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) d.update(kwargs) return self.template.render(**d) @@ -617,8 +697,7 @@ def clear(self) -> None: Returns None. """ self.ctx.clear() - self.tooltip_styles = [] - self.set_tooltip_class(name="pd-t", properties=None) + self.tooltips = _Tooltips() self._todo = [] def _compute(self): @@ -812,158 +891,6 @@ def where( lambda val: value if cond(val) else other, subset=subset, **kwargs ) - 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. - - 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 = Styler(df, uuid="_").set_tooltips(ttips).render() - """ - if not self.cell_ids: - # tooltips not optimised for individual cell check. - raise NotImplementedError( - "Tooltips can only render with 'cell_ids' is True." - ) - - ttips = ttips.reindex_like(self.data) - - mask = (ttips.isna()) | (ttips.eq("")) - self.tooltip_styles = [ - d - for sublist in [ - [ - { - "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])}"')], - }, - ] - for i, rn in enumerate(ttips.index) - for j, cn in enumerate(ttips.columns) - if not mask.iloc[i, j] - ] - for d in sublist - ] - - return self - - def set_tooltip_class(self, name="pd-t", properties=None) -> "Styler": - """ - 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)"), - ] - self.tooltip_class = name - - self.tooltip_class_styles = [ - {"selector": f".{self.tooltip_class}", "props": properties} - ] - return self - - def _render_tooltips(self, d): - """ - 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 - ---------- - 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'' - ) - 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. @@ -1678,6 +1605,148 @@ 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: List[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): + return [{"selector": f".{self.class_name}", "props": self.class_properties}] + + def _translate_tooltips(self, styler_data, uuid, d): + """ + 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. + d : dict + The dictionary prior to final render + """ + 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 + + def pseudo_css(uuid, row, col, name, text): + """ + 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")]}] + + 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}"')], + }, + ] + + mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip + self.table_styles = [ + d + for sublist in [ + pseudo_css(uuid, i, j, self.class_name, str(self.tt_data.iloc[i, j]),) + for i, rn in enumerate(self.tt_data.index) + for j, cn in enumerate(self.tt_data.columns) + if not mask.iloc[i, j] + ] + for d in sublist + ] + + if self.table_styles: + 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 f9f711d9d8e9a..5de900198dfdc 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1715,8 +1715,10 @@ def test_tooltip_render(self, ttips): # GH 21266 df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=["A", "B"], index=["a", "b"]) 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 } " @@ -1728,6 +1730,7 @@ def test_tooltip_render(self, ttips): + "" in s ) + # test 'Max' tooltip added assert ( "#T__ #T__row0_col1:hover .pd-t {\n visibility: visible;\n } " @@ -1743,10 +1746,27 @@ def test_tooltip_render(self, ttips): 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() + s = ( + Styler(df, uuid="_").set_tooltips_class("pd-t").render() + ) # no set_tooltips() assert '' in s assert '' not in s + def test_tooltip_class(self): + # GH 21266 + df = pd.DataFrame(data=[[0, 1], [2, 3]]) + s = ( + Styler(df, uuid="_") + .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: From 4055e1b934ce87214506ff09217c762474b5ed56 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 20 Sep 2020 09:28:13 +0200 Subject: [PATCH 21/37] fix test after recent merge master --- pandas/tests/io/formats/test_style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index e015b63139722..d6a3bd3d24bec 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1755,7 +1755,7 @@ def test_uuid_len_raises(self, len_): def test_tooltip_render(self, ttips): # GH 21266 df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=["A", "B"], index=["a", "b"]) - s = Styler(df, uuid="_").set_tooltips(ttips).render() + 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 @@ -1797,7 +1797,7 @@ def test_tooltip_class(self): # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) s = ( - Styler(df, uuid="_") + Styler(df, uuid_len=0) .set_tooltips(DataFrame([["tooltip"]])) .set_tooltips_class(name="other-class", properties=[("color", "green")]) .render() From 8fc8437a93e87f19adf169bd307df0f5d30d4be8 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Sep 2020 08:28:45 +0200 Subject: [PATCH 22/37] revert style.ipynb --- doc/source/user_guide/style.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 44d0130f29b43..77a1fef28f373 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1241,4 +1241,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} \ No newline at end of file +} From a96acf8729b802a069977afc3cd20bc3481129e8 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Sep 2020 09:11:39 +0200 Subject: [PATCH 23/37] annotate --- pandas/io/formats/style.py | 28 +++++++++++++++++++++++---- pandas/tests/io/formats/test_style.py | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6f66ac505764d..aa28c791963b9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1727,7 +1727,16 @@ def __init__( self.table_styles: List[Dict[str, Union[str, List[Tuple[str, str]]]]] = [] @property - def class_styles(self): + 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 _translate_tooltips(self, styler_data, uuid, d): @@ -1755,7 +1764,9 @@ def _translate_tooltips(self, styler_data, uuid, d): if self.tt_data.empty: return d - def pseudo_css(uuid, row, col, name, text): + name = self.class_name + + def _pseudo_css(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 @@ -1769,6 +1780,15 @@ def pseudo_css(uuid, row, col, name, text): {"selector": "T__row1_col1 .pd-t::after", "props": [("content", "Some Valid Text String")]}] + Parameters + ---------- + 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 @@ -1800,7 +1820,7 @@ def pseudo_css(uuid, row, col, name, text): self.table_styles = [ d for sublist in [ - pseudo_css(uuid, i, j, self.class_name, str(self.tt_data.iloc[i, j]),) + _pseudo_css(i, j, str(self.tt_data.iloc[i, j])) for i, rn in enumerate(self.tt_data.index) for j, cn in enumerate(self.tt_data.columns) if not mask.iloc[i, j] @@ -1816,7 +1836,7 @@ def pseudo_css(uuid, row, col, name, text): str(item["display_value"]) + f'' ) - d["table_styles"].extend(self.class_styles) + d["table_styles"].extend(self._class_styles) d["table_styles"].extend(self.table_styles) return d diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index d6a3bd3d24bec..f355e63b319c7 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1788,7 +1788,7 @@ def test_tooltip_ignored(self): # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) s = ( - Styler(df, uuid="_").set_tooltips_class("pd-t").render() + Styler(df).set_tooltips_class("pd-t").render() ) # no set_tooltips() assert '' in s assert '' not in s From 50c2aa9894cadbedf3f55d433d2b43f04690eb30 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Sep 2020 09:18:23 +0200 Subject: [PATCH 24/37] improve docs example --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index aa28c791963b9..2b8eaf8457b4b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -239,7 +239,7 @@ def set_tooltips(self, ttips: DataFrame) -> "Styler": >>> ttips = pd.DataFrame( ... data=[["Min", ""], [np.nan, "Max"]], columns=df.columns, index=df.index ... ) - >>> s = Styler(df, uuid="_").set_tooltips(ttips).render() + >>> s = df.style.set_tooltips(ttips).render() """ if not self.cell_ids: # tooltips not optimised for individual cell check. From 607cfe61e345411b30b5b4f5e3785d13d4e9c3e6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Sep 2020 09:43:28 +0200 Subject: [PATCH 25/37] black pandas --- pandas/tests/io/formats/test_style.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index f355e63b319c7..997270e965dff 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1787,9 +1787,7 @@ def test_tooltip_render(self, ttips): def test_tooltip_ignored(self): # GH 21266 df = pd.DataFrame(data=[[0, 1], [2, 3]]) - s = ( - Styler(df).set_tooltips_class("pd-t").render() - ) # no set_tooltips() + s = Styler(df).set_tooltips_class("pd-t").render() # no set_tooltips() assert '' in s assert '' not in s From ab079b02c6ccc7702a4f03e0fbe468b5d38ad757 Mon Sep 17 00:00:00 2001 From: attack68 <24256554+attack68@users.noreply.github.com> Date: Wed, 23 Sep 2020 07:06:03 +0200 Subject: [PATCH 26/37] Update pandas/io/formats/style.py list to seq for set_tooltips_class Co-authored-by: William Ayd --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2b8eaf8457b4b..71af6d47c6bed 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -252,7 +252,7 @@ def set_tooltips(self, ttips: DataFrame) -> "Styler": def set_tooltips_class( self, name: Optional[str] = None, - properties: Optional[List[Tuple[str, Union[str, int, float]]]] = None, + properties: Optional[Sequence[Tuple[str, Union[str, int, float]]]] = None, ) -> "Styler": """ Manually configure the name and/or properties of the class for From f6e066efa02e08a4fb458d638c2be53fbacb4765 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Sep 2020 07:54:52 +0200 Subject: [PATCH 27/37] sequence type change --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 71af6d47c6bed..4b9dc6ed46ce9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1710,7 +1710,7 @@ class _Tooltips: def __init__( self, - css_props: List[Tuple[str, Union[str, int, float]]] = [ + css_props: Sequence[Tuple[str, Union[str, int, float]]] = [ ("visibility", "hidden"), ("position", "absolute"), ("z-index", 1), From af6401a74ca239bb921089016216697965cd938e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Sep 2020 08:02:53 +0200 Subject: [PATCH 28/37] doc notes for users --- pandas/io/formats/style.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4b9dc6ed46ce9..a3d4d114de7d3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -274,8 +274,18 @@ def set_tooltips_class( If arguments are `None` will not make any changes to the underlying ``Tooltips`` existing values. - The property ('visibility', 'hidden') should always be included in any manual - properties specification, to allow for HTML on hover functionality. + 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 -------- From 0e6b1a3bd96415158b8e49b4cc55fa4515157a28 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Sep 2020 08:49:17 +0200 Subject: [PATCH 29/37] avoid var name clash --- pandas/io/formats/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a3d4d114de7d3..10bcd5b641122 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1828,14 +1828,14 @@ def _pseudo_css(row: int, col: int, text: str): mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip self.table_styles = [ - d + style for sublist in [ _pseudo_css(i, j, str(self.tt_data.iloc[i, j])) for i, rn in enumerate(self.tt_data.index) for j, cn in enumerate(self.tt_data.columns) if not mask.iloc[i, j] ] - for d in sublist + for style in sublist ] if self.table_styles: From 5829546ef2ea8f90f159b20254ce3b27306cff66 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 25 Sep 2020 17:31:41 +0200 Subject: [PATCH 30/37] enumerate to range --- pandas/io/formats/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 10bcd5b641122..c2d18863247c5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1831,8 +1831,8 @@ def _pseudo_css(row: int, col: int, text: str): style for sublist in [ _pseudo_css(i, j, str(self.tt_data.iloc[i, j])) - for i, rn in enumerate(self.tt_data.index) - for j, cn in enumerate(self.tt_data.columns) + 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 From 5507c4f0e0b75ca4bc50624b502ec91cb9f36c2c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 28 Sep 2020 23:17:19 +0200 Subject: [PATCH 31/37] pre commit errors --- doc/source/whatsnew/v1.2.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index cbf6abc8c2f53..d4233cf986126 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -117,8 +117,8 @@ Other enhancements - :meth:`DataFrame.applymap` now supports ``na_action`` (:issue:`23803`) - :class:`Index` with object dtype supports division and multiplication (:issue:`34160`) - :meth:`DataFrame.explode` and :meth:`Series.explode` now support exploding of sets (:issue:`35614`) -- `Styler` now allows direct CSS class name addition to individual data cells (:issue:`36159`) -- `Styler` now has the ability to add tooltips to styled dataframes from strings (:issue:`35643`) +- ``Styler`` now allows direct CSS class name addition to individual data cells (:issue:`36159`) +- ``Styler`` now has the ability to add tooltips to styled dataframes from strings (:issue:`35643`) - :meth:`Rolling.mean()` and :meth:`Rolling.sum()` use Kahan summation to calculate the mean to avoid numerical problems (:issue:`10319`, :issue:`11645`, :issue:`13254`, :issue:`32761`, :issue:`36031`) - :meth:`DatetimeIndex.searchsorted`, :meth:`TimedeltaIndex.searchsorted`, :meth:`PeriodIndex.searchsorted`, and :meth:`Series.searchsorted` with datetimelike dtypes will now try to cast string arguments (listlike and scalar) to the matching datetimelike type (:issue:`36346`) From e4a5e2010f11ed6bf5bb8f4a6aa06f248a717819 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 5 Jan 2021 12:10:44 +0100 Subject: [PATCH 32/37] requested changes jreback --- pandas/io/formats/style.py | 141 +++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 61 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6f1b17e958770..73c8aec2ea75c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -181,7 +181,7 @@ def __init__( self.cell_ids = cell_ids self.na_rep = na_rep - self.tooltips = _Tooltips() + self.tooltips: Optional[_Tooltips] = None self.cell_context: Dict[str, Any] = {} @@ -206,6 +206,19 @@ 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 @@ -241,11 +254,8 @@ def set_tooltips(self, ttips: DataFrame) -> "Styler": ... ) >>> s = df.style.set_tooltips(ttips).render() """ - if not self.cell_ids: - # tooltips not optimised for individual cell check. - raise NotImplementedError( - "Tooltips can only render with 'cell_ids' is True." - ) + self._init_tooltips() + assert self.tooltips is not None # mypy requiremen self.tooltips.tt_data = ttips return self @@ -295,6 +305,8 @@ def set_tooltips_class( ... ('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: @@ -530,7 +542,7 @@ def format_attr(pair): else: table_attr += ' class="tex2jax_ignore"' - return { + d = { "head": head, "cellstyle": cellstyle, "body": body, @@ -540,6 +552,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": """ @@ -716,7 +732,6 @@ def render(self, **kwargs) -> str: self._compute() # TODO: namespace all the pandas keys d = self._translate() - d = self.tooltips._translate_tooltips(self.data, self.uuid, d) # filter out empty styles, every cell will have a class # but the list of props may just be [['', '']]. # so we have the nested anys below @@ -786,7 +801,7 @@ def clear(self) -> None: Returns None. """ self.ctx.clear() - self.tooltips = _Tooltips() + self.tooltips = None self.cell_context = {} self._todo = [] @@ -1817,7 +1832,61 @@ def _class_styles(self): """ return [{"selector": f".{self.class_name}", "props": self.class_properties}] - def _translate_tooltips(self, styler_data, uuid, d): + 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, uuid, d): """ Mutate the render dictionary to allow for tooltips: @@ -1844,61 +1913,11 @@ def _translate_tooltips(self, styler_data, uuid, d): name = self.class_name - def _pseudo_css(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 - ---------- - 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}"')], - }, - ] - mask = (self.tt_data.isna()) | (self.tt_data.eq("")) # empty string = no ttip self.table_styles = [ style for sublist in [ - _pseudo_css(i, j, str(self.tt_data.iloc[i, j])) + 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] From 849b1f4307e583842cc5dca8b94f42a6f49424e9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 5 Jan 2021 12:16:06 +0100 Subject: [PATCH 33/37] requested changes jreback --- doc/source/reference/style.rst | 2 ++ pandas/io/formats/style.py | 4 ++++ 2 files changed, 6 insertions(+) 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/pandas/io/formats/style.py b/pandas/io/formats/style.py index 73c8aec2ea75c..c24ce7cc380e0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -224,6 +224,8 @@ 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 @@ -268,6 +270,8 @@ def set_tooltips_class( Manually configure the name and/or properties of the class for creating tooltips on hover. + .. versionadded:: 1.3.0 + Parameters ---------- name : str, default None From 950b3b1fec13d4931dbc0d3aed505fb4908b8f31 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 5 Jan 2021 12:38:55 +0100 Subject: [PATCH 34/37] requested changes jreback --- pandas/io/formats/style.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index c24ce7cc380e0..201f87167a572 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1890,7 +1890,7 @@ def _pseudo_css(self, uuid: str, name: str, row: int, col: int, text: str): }, ] - def _translate(self, styler_data, uuid, d): + def _translate(self, styler_data: FrameOrSeriesUnion, uuid: str, d: Dict): """ Mutate the render dictionary to allow for tooltips: @@ -1906,6 +1906,10 @@ def _translate(self, styler_data, uuid, d): 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) From efc7fca6b5f57a2068a422f52edf846a1e6343e2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 5 Jan 2021 12:44:32 +0100 Subject: [PATCH 35/37] pre commit fails fix --- pandas/tests/io/formats/test_style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index eccf442796ea8..c61d81d565459 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1785,7 +1785,7 @@ def test_uuid_len_raises(self, len_): ) def test_tooltip_render(self, ttips): # GH 21266 - df = pd.DataFrame(data=[[0, 3], [1, 2]], columns=["A", "B"], index=["a", "b"]) + 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 @@ -1817,14 +1817,14 @@ def test_tooltip_render(self, ttips): def test_tooltip_ignored(self): # GH 21266 - df = pd.DataFrame(data=[[0, 1], [2, 3]]) + 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 = pd.DataFrame(data=[[0, 1], [2, 3]]) + df = DataFrame(data=[[0, 1], [2, 3]]) s = ( Styler(df, uuid_len=0) .set_tooltips(DataFrame([["tooltip"]])) From eb7fe682e515a271d85f4566ff30705f1b5847c5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 13 Jan 2021 16:45:09 +0100 Subject: [PATCH 36/37] minor requests --- doc/source/whatsnew/v1.3.0.rst | 1 + pandas/io/formats/style.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index ac3b5dcaf53ae..3e6596522bb00 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -54,6 +54,7 @@ Other enhancements - Add support for dict-like names in :class:`MultiIndex.set_names` and :class:`MultiIndex.rename` (:issue:`20421`) - :func:`pandas.read_excel` can now auto detect .xlsb files (:issue:`35416`) - :meth:`.Rolling.sum`, :meth:`.Expanding.sum`, :meth:`.Rolling.mean`, :meth:`.Expanding.mean`, :meth:`.Rolling.median`, :meth:`.Expanding.median`, :meth:`.Rolling.max`, :meth:`.Expanding.max`, :meth:`.Rolling.min`, and :meth:`.Expanding.min` now support ``Numba`` execution with the ``engine`` keyword (:issue:`38895`) +- :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 201f87167a572..f3fbefc4617f3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -805,7 +805,7 @@ def clear(self) -> None: Returns None. """ self.ctx.clear() - self.tooltips = None + self.tooltips: Optional[_Tooltips] = None self.cell_context = {} self._todo = [] @@ -1934,6 +1934,7 @@ def _translate(self, styler_data: FrameOrSeriesUnion, uuid: str, d: Dict): ] 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": From 5a377b872ac09797674555602c0b932eedc152cc Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 16 Jan 2021 11:37:00 +0100 Subject: [PATCH 37/37] resolve duplicate typing --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f3fbefc4617f3..7090127602d8b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -805,7 +805,7 @@ def clear(self) -> None: Returns None. """ self.ctx.clear() - self.tooltips: Optional[_Tooltips] = None + self.tooltips = None self.cell_context = {} self._todo = []