Search code examples
pythoncallbackplotly-dashdashboard

Multiple Inputs in a Dash application with many Tabs


I'm currently programming a Dash application and I'm facing a little problem:

In a callback, I want to use 2 inputs and 2 outputs but there's a tiny problem, both inputs are not in the same tab. I will show you my callback and the application thus you will understand my problem way better:

The callback:

@app.callback(
    [Output("kpi-viewer-tab","children"),Output("export-alert","is_open")],
    Input("start-calculations-button","n_clicks"),
    Input("dl-button","n_clicks"),
    [State("rack-boolean-switch","on"),
     State("bank-boolean-switch","on"),
     State("project-name","value"),
     State("name","value"),
     State("surname","value"),
     State("html-startdate-picker","value"),
     State("html-enddate-picker","value")]
    )

def viewer_tab (click_calcul,dl_click,on_r,on_b, project_name, user_name, surname, start_dt, end_dt):
    type_request = ""
    files = []
    name = "None"
    kpi_results = dict
    StartDay = pd.Timestamp("2023-04-14T12", tz='utc')
    EndDay = pd.Timestamp("2023-04-28T12", tz='utc')
    dt_start = [StartDay + datetime.timedelta(days = 1*dayIdx) for dayIdx in range(1)]
    dt_end = [EndDay + datetime.timedelta(days = 1*dayIdx) for dayIdx in range(1)]
    tmp_idSite = "eccb5d99-f46a-4780-8853-2124b062f5ca"
    project = ""
    user = ""
    raw_data = {}
    plot_df = {}
    x_axis = []
    global calcul_in_progress
        
    if project_name is None:
        return dash.no_update
    
    if user_name is not None and surname is not None:
        surname = surname.upper()
        user_name = user_name.title()
        user = user_name+" "+surname
    
    if on_r:
        type_request = "RACK"
    elif on_b:
        type_request = "BANK"
    else:
        type_request = "RACK"
        
    for index in range (len(project_name)):
        if project_name[index] == "_":
            name = project_name[:index]
            break
    if name == "None":
        name = project_name
        
    project = name

    files = LSP.load_files(Mapping_path, TimeSeries_path, name)
    
    print(files)

    if dl_click > 0:
        E2P.export2pdf(raw_data, plot_df,x_axis,project,user,str(start_dt),str(end_dt))
        return True
    else:
        if click_calcul > 0:
            calcul_in_progress = 1
            kpi_results = RC.run_kpi_calculations(project, type_request, dt_start, dt_end, tmp_idSite, files, param_dict, kpi_calculations)
            calcul_in_progress = 0
            
            plot_df, raw_data, x_axis = RC.extract_data_from_dict(kpi_results)
            click_calcul = 0

            return TB.inside_viewer(user, plot_df, raw_data, x_axis)
        else :
            return dash.no_update

The application: Screenshot of my application

The first output is used to change the content of the Tabs element. The second is the alert which tells the user that the PDF is created.

The first input is a button at the bottom of the "KPI Config" tab and the second input is the Export to PDF button which is at the top of the "KPI Viewer" tab.

The problem is that when I'm on the tab "KPI Config" the button isn't even recognized so I just can't use it as my application need to be on the "KPI Config" tab at its initialization. (I don't if this can help, but content of tabs are updated with callbacks as well)

Does someone have a clue to solve this? I'm not sure to give enough information for you to help me so if you need anything, ask me I'll answer you

(P.S.: I know there's some errors and not optimized code in this callback, I'll correct it later)

I tried to build 2 different callbacks and it worked as I wanted but, I had to use 6 global variables because of the function E2P.export2pdf which needs a lot of parameters.


Solution

  • You can organize each python dash file for each tab under a local subdirectory relative to where your app.py (or equivalent) main dash app file is, and simply use import statements to import all the local variables from the tabs into the main app file. You should not [need to] use global variables.

    A simple dash app with multiple tabs and a callback which has multiple inputs from multiple tabs

    For example:

    Your file directory structure could look like:

    .
    |-- app.py
    `-- tabs
        |-- kpi_config.py
        `-- kpi_viewer.py
    

    In each tab file, define a list of components creating the layout structure for each tab and name it children.

    E.g., in kpi_config.py (tab #1) I have:

    import dash
    
    from dash import dcc, html
    
    children = [
        html.Div(html.H1("Header Config"), id="header-1"),
        html.Div(
            [
                html.Div("Convert Temperature"),
                "Celsius",
                dcc.Input(id="celsius-1", value=0.0, type="number"),
                " = Fahrenheit",
                dcc.Input(id="fahrenheit-1", value=32.0, type="number",),
            ]
        ),
    ]
    

    and in kpi_viewer.py (tab #2) I have:

    import dash
    
    from dash import dcc, html
    
    children = [
        html.Div(html.H1("Header Viewer"), id="header-2"),
        html.Div(
            [
                html.Div("Convert Temperature"),
                "Celsius",
                dcc.Input(id="celsius-2", value=0.0, type="number"),
                " = Fahrenheit",
                dcc.Input(id="fahrenheit-2", value=32.0, type="number",),
            ]
        ),
    ]
    

    To demonstrate dynamic/reactive callback functionality, I put a simple celsius-fahrenheit bidirectional calculator into both tabs using dash.dcc.Input components. However, they must have unique ids. So, based on which tab the components were in, I correspondingly appended a "1" or "2" to the ids. So, whenever an input on either tab is altered, we want both calculators to change (in this somewhat arbitrary example to simulate having inputs from separate tabs trigger the same callback, which will be shown next).

    And then, in the main app file app.py:

    As shown in the top directory, I have code such as:

    import dash
    from dash import Dash, Input, Output, State, callback, dcc, html, ctx
    
    import tabs
    from tabs.kpi_config import children as kpi_config
    from tabs.kpi_viewer import children as kpi_viewer
    
    
    app = dash.Dash(__name__)
    
    # Use `dash.dcc.Tabs` per docs
    tab_pages = [
        dcc.Tab(label="KPI Config", children=kpi_config),
        dcc.Tab(label="KPI Viewer", children=kpi_viewer),
    ]
    children = html.Div([dcc.Tabs(id="tabs", children=tab_pages)])
    
    app.layout = children
    
    
    @callback(
        [
            Output("celsius-1", "value"),
            Output("fahrenheit-1", "value"),
            Output("celsius-2", "value"),
            Output("fahrenheit-2", "value"),
        ],
        [
            Input("celsius-1", "value"),
            Input("fahrenheit-1", "value"),
            Input("celsius-2", "value"),
            Input("fahrenheit-2", "value"),
        ],
    )
    def sync_input(
        celsius_config, fahrenheit_config, celsius_viewer, fahrenheit_viewer
    ):
        input_id = ctx.triggered[0]["prop_id"].split(".")[0]
    
        def convert_temperature(c, f):
            if input_id.startswith("celsius"):
                f = None if c is None else (float(c) * 9 / 5) + 32
            else:
                c = None if f is None else (float(f) - 32) * 5 / 9
            return c, f
    
        if input_id.endswith("1"):
            c, f = convert_temperature(celsius_config, fahrenheit_config)
        else:
            c, f = convert_temperature(celsius_viewer, fahrenheit_viewer)
    
        return c, f, c, f
    
    
    if __name__ == "__main__":
        app.run_server(debug=True)
    

    Which yields, e.g., the following app behavior: Video screenshots of demonstrated dash app example where you can see that changes in one tab automatically, reactively change the components in both tabs simultaneously (because the callback is triggered by inputs from the components in either tab).

    When only working with a multi-tab app (vs. a multi-page (i.e., url) app [where things get a little more complicated]), you can still define all your callbacks in the same file where the dash.app is declared. Because we've imported the dash components by importing the children of both tab files and assigning those to app.layout (using dash.dcc.Tabs to collect together two dash.dcc.Tab components), dash takes care of the rest and all of component ids in both tab files are now in the namespace of all defined callbacks in the app.py file.