Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

Tooltip position is incorrect after sorting/filtering data table #872

Open
Dekermanjian opened this issue Feb 10, 2021 · 9 comments · May be fixed by #906
Open

Tooltip position is incorrect after sorting/filtering data table #872

Dekermanjian opened this issue Feb 10, 2021 · 9 comments · May be fixed by #906

Comments

@Dekermanjian
Copy link

Dekermanjian commented Feb 10, 2021

Tooltips get messed up after sorting or filtering. Simple reproducible example below.

import dash
import dash_table
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/curran/data/gh-pages/Rdatasets/csv/MASS/Animals.csv')
df.columns = ["Animal", "body", "brain"]
def create_tooltip(cell):
	num = len(cell)
	if num > 7:
		return(cell)
	else:
		return("")
tooltip_data = [{
            column: 
            {'value': create_tooltip(df.loc[i, column]), 'type': 'markdown'} for column in df.columns if column == "Animal"} for i in range(len(df))]

app = dash.Dash(__name__)

app.layout = dash_table.DataTable(
    id='table',
    columns=[{"name": i, "id": i} for i in df.columns],
    data=df.to_dict('records'),
    sort_action = "native",
    filter_action  = 'native',
    style_header={'fontWeight': 'bold','border': 'thin lightgrey solid','backgroundColor': 'rgb(100, 100, 100)','color': 'white'},
    style_cell={'fontFamily': 'Open Sans','textAlign': 'left','maxWidth': '0px','whiteSpace': 'no-wrap','overflow': 'hidden','textOverflow': 'ellipsis'},
    style_table = {'overflowX':'scroll', 'overflowY':'scroll', 'maxHeight':'400px', 'maxWidth':'200px', 'marginLeft':'100px'},
    tooltip_data=tooltip_data,
    tooltip_delay=0,
    tooltip_duration=None
)


if __name__ == '__main__':
    app.run_server(debug=True)
@Dekermanjian
Copy link
Author

Wanted to also add to this the fact that if you have pagination, then on pages not == 1 the tooltips are also misaligned.

@magic-lantern
Copy link

Anyone have a work around to this? I'm seeing the same problem.

@magic-lantern
Copy link

Forgot to mention that the problem also exists if the table has pagination and user navigates to any page other than 1

@Dekermanjian
Copy link
Author

Hi, I haven't found a work around. But I did notice that in the DOM the position of the tooltips are being calculated based on the initial table state, even after filtering/sorting and likely after paginations. I think the fix would be to dynamically calculate the tooltip positions based on the current state of the table

@magic-lantern
Copy link

@Dekermanjian - Thanks for the suggestion! You are right. Tooltip location is calculated initially, and I can't figure out how to get datatable/dash/plotly to recalculate.

My idea is that tooltip_data might need to be recalculated on every table update. However, my idea only works for pagination and requires disabling of the cool sort/filter functionality :( The solution is probably to re-implement sorting/filtering with a callback/custom function but my approach would require quite a bit of custom code to replace default functionality.

Here's the code I tried in case I'm close or it sparks an idea for someone else:

# additional dependencies
from dash.dependencies import Input, Output

# table properties must be
# filter_action="custom",
# sort_action="custom",
# page_action="custom",

@app.callback(
    [
        Output('table', 'tooltip_data'),
        Output('table', 'data')],
    [
        Input('table', 'page_current'),
        Input('table', 'page_size')
    ])
def update_table(page_current,page_size):
    ret_df = orig_df.iloc[page_current*page_size:(page_current+1)*page_size].to_dict(orient='records')
    ret_tooltip = [{
            column: {'value': str(value), 'type': 'markdown'}
            for column, value in row.items()
        } for row in ret_df]
    
    return ret_tooltip, ret_df

I'll play around with this a little more - but I'd really like a baked in solution. Hopefully the plotly dev folks will notice this issue and take pity on us!

@magic-lantern
Copy link

Since my previous comment, I got something working, but am not very happy with the result. I've found that if I follow the section [Backend Paging with Filtering and Multi-Column Sorting](https://dash.plotly.com/datatable/callbacks#backend-paging-with-filtering-and-multi-column-sorting one can re-implement) all the client side code I care about can be replicated with about 50 or so lines of Python code. By dynamically generating the view of the dataframe and associated tooltips on each user interaction, the tooltips always work.

As an alternative, I investigated using dash-bootstrap-components but found that solution to be quite a mess as well - probably due to my inexperience with Plotly Dash and React.

It seems that DataTable cells do not have an id which is how dash-bootstrap-components attach their tooltips. I couldn't figure out how to add it via Python or any native Plotly Dash api. Additionally, since dash_bootstrap_components.Tooltip requires an id when added to the layout, it cannot be present in the initial app.layout()

Through a combination of client side callbacks I found I can add id attributes where I want, then I can dynamically add the dash_bootstrap_components.Tooltip

As this was just testing, I have a button to start everything, plus my table - something like this:

app.layout = html.Div(children=[
    html.Button('Click Me', id='submit-btn', n_clicks=0, className='start'),
    dash_table.DataTable(
        id='my-table'
        # more stuff here
    ),
    html.Div(id="layout")
])

When the button is clicked, some client side code adds the desired id

app.clientside_callback(
    ClientsideFunction(
        namespace='clientside',
        function_name='myfunc'
    ),
    Output('submit-btn', 'className'),
    Input('submit-btn', 'n_clicks')
)
window.dash_clientside = Object.assign({}, window.dash_clientside, {
  clientside: {
    myfunc: function () {
      elem = document.getElementById('my-table')
      if (elem) {
        child = elem.getElementsByClassName('dash-cell column-0')
        child[0].id = 'cell1'
      }
    }
  }
});

Then I have another callback that adds the Tooltip:

@app.callback(
    Output('layout', 'children'),
    [Input('submit-btn', 'n_clicks')],
    [State('layout', 'children')],
)
def add_strategy_divison(val, children):
    if val:
        el = dbc.Tooltip(
            f"Tooltip on cell",
            id='cell1',
            target='cell1'
        )
        return el
    else:
        raise PreventUpdate

I'm sure these callbacks could be combined into one.

I've noticed issue #736 describes a related problem. Perhaps @Marc-Andre-Rivet has some better insight on how to make DataTable tooltips work with "native" functionality?

@adament adament linked a pull request May 31, 2021 that will close this issue
@AtharvaKatre
Copy link

Is there any new update/fix on this issue?

@akullenberg-cbp
Copy link

Anything on this?

@janfrederik
Copy link

The issue is still there after three years. Anything in the planning about this?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants
@magic-lantern @janfrederik @Dekermanjian @AtharvaKatre @akullenberg-cbp and others