Tabs awaiting data

Hello,

I made a Dash app using tabs. There is a tab “Download data” where data will be downloaded by an API with a parameter “customer_id”. This can last several minutes (simulated by a time.sleep). Then the data is plotted in other tabs.

Issues

  1. If I click on the tab “Download data” it resets everything (dropdown and other plots)

  2. I think the user experience is confusing. I can’t prevent the user from going to other tabs while the data is downloading. So when the user decides to change the data source the old data still exists in the plot tabs until the new data is downloaded. I’ve tried adding a message on top of the tabs showing which customer id’s data is beeing displayed but it did not work.
    Also I can’t remove the message “Download is finished” after the first time it is displayed in the “download tab”.

I’d be glad if you could provide any suggestions or workarounds for those issues :).

Code

The code is available as text below or you can see it in a jupyter notebook and test it live within mybinder (it will be much more readable IMO, you’ll need to wait a few seconds for it to load):

https://mybinder.org/v2/gh/ThibTrip/thib-dash/master?filepath=tabs_test.ipynb

(Binder created from my repo there: https://github.com/ThibTrip/thib-dash)

#!/usr/bin/env python
# coding: utf-8
# dash/plotly imports
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Output, Input, State
import plotly.graph_objs as go

# other
import pandas as pd
import numpy as np
import random
import time


# # Data collection function




def slowly_collect_data_from_API(customer_id:int)->pd.DataFrame:
    """Simulates a collection of data from an API.
    Returns a random DataFrame.
    In a dash app the data must be collected within a callback.
    
    Args:
        customer_id: only a placeholder, can be any value
            (in a real use we would use the variable customer_id upon requesting the API, get a response and parse it)
    
    """
    
        
    # generate random DataFrame
    random_start_id = random.randint(0,1000000)
    nb_rows = random.randint(10,500)
    df = pd.DataFrame(data = {"action":[random.choice(["purchase","click_url","watch_video"]) for i in range(nb_rows)]},
                      index = np.arange(start = random_start_id,stop = random_start_id + nb_rows))
    df.index.name = "userid"
    
    
    
    time.sleep(10)
    
    print('Data sucessfully collected')
    return df


# # App configuration

# ## Define variables for div ids
# 
# Since we need to suppress callback exceptions (because we will reference objects that may not yet exist in the callbacks) this is a good way to test ids before launching the app (can't remember where I read this information sorry).




# tabs
tab_container_id = "tabs-container"
graph_tab_id = "graph-tab"
table_tab_id = "table-tab"
download_tab_id= "download-tab"
output_of_tab_id = "tabs-output"

# inputs
customer_id_dropdown_id = "customer-id-input"
submit_button_id = "submit-button"

# graphs and tables
graph_id = "graph"
table_id = "table"

# other
storage_div_id = 'data-storage-div'
dtype_storage_div_id = 'dtype-storage-div'
loading_div_id = "loading"
loading_children_div_id = "loading-output"
download_completed_div_id = "download-complete-md"


# ## app object




app = dash.Dash(__name__)


# ## creating content in dash tabs (dcc.Tab)




# placeholder for when no data has been collected and the user tries to display graphs
placeholder_for_missing_data = html.Div([dcc.Markdown(('No data available, please load the data under the tab **"Download data"**'
                                                      'enter a customer id and click submit.'))])


# ### tab to download data (static)




_instructions_md = """

# Instructions:

1. Select a customer id from which you want to download data.
2. Press on submit
3. Wait for the message "Download is finished..." to appear (this may take up to a minute depending on the quantity of data).

"""



_customer_id_dropdown = dcc.Dropdown(id= customer_id_dropdown_id, 
                                     placeholder = "Please select a customer id", 
                                     multi = False, 
                                     options = [{"label":i,"value":i} for i in range(1,10)])



downloadtab = html.Div([dcc.Markdown(_instructions_md),
                        _customer_id_dropdown,
                        html.Button(id= submit_button_id, n_clicks=0, children='Submit'),
                        dcc.Loading(id= loading_div_id,children=[html.Div([html.Div(id= loading_children_div_id)])],type="circle"),
                        dcc.Markdown(id = download_completed_div_id)])


# ### tab to plot graph using downloaded data (dynamic, with function)




def create_graph(json_data, json_data_dtypes):
    """Creates graph for the "graph" tab."""
    
    if json_data is not None:
        df = pd.read_json(json_data, dtype = pd.read_json(json_data_dtypes))
        
        data = df["action"].value_counts()
        """ Example of data (values may vary since the data is random):
        click_url      24
        purchase       21
        watch_video    18
        Name: action, dtype: int64
        """
        x = data.index
        y = data.values.tolist()
        
        # construct plotly object
        trace0 = go.Bar(x = x, y = y)
        
        data = [trace0]
        layout = go.Layout(title='Graph tab 1')
        fig = go.Figure(data=data, layout=layout)
        
        
        return html.Div([html.H3('Tab graph content'),
                  dcc.Graph(id= graph_id, figure = fig)])
        
        
        
    else:
        return placeholder_for_missing_data


# ### tab to display table using downloaded data (dynamic, with function)




def create_table(json_data, json_data_dtypes):
    """Creates table for the "table" tab"""
    if json_data is not None:
        df = pd.read_json(json_data, dtype = pd.read_json(json_data_dtypes))
        s_data = df["action"].value_counts().rename("count")
        s_data.index.name = "action"
        df_data = s_data.reset_index()
        """ Example of df_data (values may vary since the data is random):
                action  count
        0    click_url     56
        1     purchase     54
        2  watch_video     51
        """
        data = df_data.to_dict(orient = "rows")
        columns = [{'name': i, 'id': i} for i in df_data.columns]
        
        table = dash_table.DataTable(id = table_id,data = data, columns = columns)
        
        return html.Div([html.H3('Tab table content'),table])

    else:
        return placeholder_for_missing_data


# ## Assembling the app's layout




app.layout = html.Div([# divs used for storing data (must be JSON serializible)
                       ## we'll store a DataFrame in the div below
                       html.Div(id= storage_div_id, style={'display': 'none'}),
                       ## we'll store the dtypes of the DataFrame in the div below (we'll use the dtypes when reading from the jsonified DtaFrame)
                       ## this way we can make for instance sure a leading zero in a string like "01234" is preserved and not casted to integer 1234
                       html.Div(id= dtype_storage_div_id, style={'display': 'none'}), 
                       # defining the tabs, start with the download tab by default
                       dcc.Tabs(id= tab_container_id, value= download_tab_id, children=[dcc.Tab(label = 'Download data',value = download_tab_id),    
                                                                                        dcc.Tab(label='Graph', value= graph_tab_id),
                                                                                        dcc.Tab(label='Table', value= table_tab_id)]),
                       # div used for displaying tab content
                       html.Div(id= output_of_tab_id)])


# some callbacks are going to refer to divs that have not been created yet
app.config['suppress_callback_exceptions'] = True


# ## Callbacks




# IMPORTANT: Output -> Input -> State must be provided in this order!
# Else following error is raised: 
# IncorrectTypeException: The input argument `data-storage-div.children` is not of type `dash.Input`.
# Also if there is only one output it cannot be in a list

@app.callback(Output(output_of_tab_id, 'children'),
              [Input(tab_container_id,'value')],
              [State(storage_div_id,'children'),
               State(dtype_storage_div_id, 'children')])

# render the layout of a tab (defined above)
def render_tab_layout(tab,json_data,json_data_dtypes):
    """Returns the corresponding tab layout to the value stored in "tabs-container" Div"""
    
    # download tab is the default one (and the one at launch) or the one when no data is available
    if tab == download_tab_id:        
        return downloadtab
    
    elif tab == graph_tab_id:
        return create_graph(json_data, json_data_dtypes)

    elif tab == table_tab_id:
        return create_table(json_data, json_data_dtypes)
    
    else:
        raise NotImplementedError(f"Unexpected value for component '{tab_container_id}': '{tab}'")

    
# on submit-button press, collect data from the API and store it in an invisible div, store its data types, display loading circle and display text when finished
@app.callback([Output(storage_div_id, 'children'),
               Output(dtype_storage_div_id, 'children'),
               Output(loading_children_div_id,'children'),
               Output(download_completed_div_id, 'children')],
              [Input(submit_button_id,'n_clicks'),
               Input(customer_id_dropdown_id,'value')])
def collect_data(n_clicks,customer_id):
    """Collects data from a customer if customer_id has been provided and the button
    in the div "submit-button" has been clicked.
    This operation can only be done from the tab with the value "tab-download".
    """
    
    # "the callbacks are always going to fire on page load. So, you’ll just need to check if n_clicks is 0 or None in your callback"
    # source: https://community.plotly.com/t/button-is-firing-on-load-and-i-want-it-to-fire-only-on-click/11906
    if n_clicks == 0:
        return None,None,None,""
    
    df = slowly_collect_data_from_API(customer_id  = customer_id)
    
    return df.to_json(), df.dtypes.to_json(),"","Download is finished! You can now view the vizualisations in the other tabs."


# # Launch the app
# 
# If executing with Jupyter debug=True will not work for some reason. You can save the notebook to .py (manually or via a post-save-hook in jupyter-config file) and execute the python script instead.




if __name__ == '__main__':
    app.run_server(debug=False)

Thanks in advance for your help, I know it’s a lot but hopefully that will be useful to other people as well.

Thibault