Dynamically user generated layout and figures with later callbacks

Hello everyone,

I have started using dash for couple for couple of weeks to bring some of my data scientist work to the next level.

I am currently working on a little (simple) application with many user input based features. Overall what my application is doing / needs to do is the following:

1: The application is initialized with some features: a title, a drop-down list, a date-range selector, a container

output = []

output.extend([html.H1(children='Custom time-series visualization',
                    style={'textAlign': 'center',
                        #'color': colors['text']
                    }
               ),
    
    html.Div(children='Dynamic and interactive visualization is awesome!',
        style={'textAlign': 'center',
        #'color': colors['text']
        }
    ),

     html.Div([
        dcc.Dropdown(id='dropdown-input-filename',
            options=[{'label': i, 'value': i} for i in files],
            searchable=False,
            clearable=True,
            placeholder="Select an input configuration file",
            ),],
        style={"width": "25%", 
            "padding": 50,
        },
    ),

    html.Div(children=[html.Div(children="Date Range",
                            className="menu-title"
                       ),

            dcc.DatePickerRange(id="date-range",
                #min_date_allowed=cache_data_object.data.index.min().date(),
                #max_date_allowed=cache_data_object.data.index.max().date(),
                #start_date=cache_data_object.data.index.min().date(),
                #end_date=cache_data_object.data.index.max().date(),
            ),],
    ),

    html.Div(id='container', children=[]),

])


app.layout = html.Div(id="layout-div", children=output)

2: The user selects a configuration file from a drop-down list which updates the content of the Div with id:"container"

@app.callback(
    # Ouputs
    [
    Output("container", "children"),
    ],
    
    # Inputs
    [Input("dropdown-input-filename", "value")],
    
    # State
    [
    State("container", "children"),
    ],

    # Properties
    prevent_initial_call=True,
)

def initialize_layout(file_name, children):

    if file_name is None:

        children=[]

    else:

        CWD = os.getcwd()
        input_data_path = CWD + "/Inputs"
        file_path = input_data_path + "/{0}".format(file_name)

        cache_data_object =  DataCachingClassV2(input_csv=file_path, start_date=default_start_date, stop_date=default_stop_date)

        blocks_count = 1 
        for block in cache_data_object.data.keys():


            figure = make_graph_figure(cache_data_object=cache_data_object, block=block, block_number=blocks_count, start_date=default_start_date, stop_date=default_stop_date)

            new_element = html.Div(children=figure)
            children.append(new_element)

            blocks_count += 1   

    return children

3: The aforementioned triggers the generation/building of the application layout with several figures.

The problem I am facing is updating the figures with the dates selected afterwards from the date-range selector. I use a custom made class (DataCachingClassV2()) which queries the data from a DB according to the configuration file chosen by the user, formats the data and store it in a nice data-structure at the class level. This results in a cache_data_object which I can’t access anymore once this initial callback has been performed.

Therefore, I am wondering how I could structure my application to update the dynamically generated figures (I do not know how many of them will be before hand), based on the date-range selector?
Maybe I am doing everything completely wrong, so I am also open to re-thinking the entire structure of my application.

EDIT 1

For those wondering what the make_graph_figure function looks like, here it is

def make_graph_figure(cache_data_object, block, block_number, start_date, stop_date):
    
    ### Unpacking data
    temp_data = cache_data_object.data[block]["DATA"][default_start_date:default_stop_date]

    ### Data binning

    y_axis_bins = {"0_10": {"channels": [],
                        "min": pd.Series(dtype=float),
                        "max": pd.Series(dtype=float),
                        }, # order 0
    
    "10_100": {"channels": [],
               "min": pd.Series(dtype=float),
               "max": pd.Series(dtype=float),
               }, # order 1
    
    "100_1k": {"channels": [],
               "min": pd.Series(dtype=float),
               "max": pd.Series(dtype=float),
               }, # order 2
    
    "1k_10k": {"channels": [],
               "min": pd.Series(dtype=float),
               "max": pd.Series(dtype=float),
               }, # order 3
    
    "10k_100k": {"channels": [],
                 "min": pd.Series(dtype=float),
                 "max": pd.Series(dtype=float),
                 }, # order 4
    
    "100k_1M": {"channels": [],
                "min": pd.Series(dtype=float),
                "max": pd.Series(dtype=float),
                }, # order 5
    
    "1M_": {"channels": [],
            "min": pd.Series(dtype=float),
            "max": pd.Series(dtype=float),
            }, # order 6 or more
    }   


    order_to_key_dict = {0: "0_10",
        1: "10_100",
        2: "100_1k",
        3: "1k_10k",
        4: "10k_100k",
        5: "100k_1M",
        6: "1M_",
    }


    for time_serie_name, time_serie_properties in temp_data.describe().items():

        max_val = time_serie_properties["max"]
        min_val = time_serie_properties["min"]
        mean_val = time_serie_properties["mean"]

        OOM = None

        # Identify order of magnitude
        if max_val <= 0:

            OOM = 0

        else:

            OOM = order_of_magnitude.order_of_magnitude(round(max_val,1))

        y_axis_bins[order_to_key_dict[OOM]]["min"] =  y_axis_bins[order_to_key_dict[OOM]]["min"].append(pd.Series(float(min_val)))
        y_axis_bins[order_to_key_dict[OOM]]["max"] =  y_axis_bins[order_to_key_dict[OOM]]["max"].append(pd.Series(float(max_val)))


    plotly_data = []
    plotly_layout = go.Layout(hovermode  = 'x')

    layout_kwargs = {"title": "Block {0} figure".format(block_number),
                     'xaxis': {'domain': [0, 0.7]},
                     "colorway": px.colors.qualitative.D3}

    y_axes_counter = 0

    for time_serie_name, time_serie_data in temp_data.items():

        OOM = order_of_magnitude.order_of_magnitude(max(time_serie_data))
        y_min_limit = y_axis_bins[order_to_key_dict[OOM]]["min"].min()
        y_max_limit = y_axis_bins[order_to_key_dict[OOM]]["max"].max()

        axis_name = 'yaxis' + str(y_axes_counter + 1) * (y_axes_counter > 0)
        yaxis = 'y' + str(y_axes_counter + 1) * (y_axes_counter > 0)

        plotly_data.append(go.Scatter(
            x=temp_data.index,
            y=time_serie_data.values,
            name=time_serie_name,
            )
        )

        layout_kwargs[axis_name] = {#"title": axis_name,
            #"titlefont": dict(
                #color="#9467bd"
                #),
            "tickfont": dict(
                #color=px.colors.qualitative.Plotly[y_axes_counter]
                color=px.colors.qualitative.D3[y_axes_counter]
                ),
            #"anchor": "free",
            #"side": "right",
            "position": 1 - y_axes_counter * 0.04,
            #"range": [min(time_serie_data.values) * 0.9, max(time_serie_data.values) * 1.1]
            "range": [y_min_limit * 0.9, y_max_limit * 1.1],
            "showline": True
        }

        plotly_data[y_axes_counter]['yaxis'] = yaxis

        if y_axes_counter > 0:
            
            layout_kwargs[axis_name]['overlaying'] = 'y'

        y_axes_counter += 1

    temp_figure = go.Figure(data=plotly_data, layout=go.Layout(**layout_kwargs))


    ### Figure creation

    figure = dcc.Graph(id={
            "name": "block-{0}-figure".format(block_number),
            "type": "dates-dependant-figure",
            "start_date": start_date,
            "stop_date": stop_date 
        }, 
        figure=temp_figure, 
        config={"displaylogo": False,
            "responsive": True
        }
    )

    return figure

Thank you for your time, help and thoughts :slight_smile:

R3s0luti0n