plotly / dash-table

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
420 stars 74 forks source link

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

Open Dekermanjian opened 3 years ago

Dekermanjian commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

magic-lantern commented 3 years ago

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

Dekermanjian commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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?

AtharvaKatre commented 1 year ago

Is there any new update/fix on this issue?

akullenberg-cbp commented 1 year ago

Anything on this?

janfrederik commented 4 months ago

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