Python dash return several values inside for loop

Question:

For my dash app, in order to update some graphs dynamically, I have to use a function that I named update_graphs inside a for loop. Some of the graphs contain several traces while some others only have one. The update_graphs function is called inside a callback and returns a dict and an int to update the extendData property of the graph object. However, since I am using a return statement inside a for loop, I only get the first trace.

I am not familiar with the generators and the yield keyword, maybe this is an option. But I haven’t been able to make it work.

I have also tried to store the results of the update_graphs inside a list but it is not working.

Any help is appreciated!

Here is the code for the app:

import dash
from dash.dependencies import Output, Input, State, MATCH, ALL
from dash import dcc, html, ctx
import plotly
import plotly.express as px
import random
import plotly.graph_objs as go
import pandas as pd
  
# Initializing the data with the correct format
init_store = {}
n=3

init_df = pd.DataFrame({'a':pd.Series(dtype='int'), 'b':pd.Series(dtype='int'), 'c':pd.Series(dtype='int'), 'd':pd.Series(dtype='int')}, index=range(50))
init_df['a'] = init_df.index
init_store['0'] = init_df

for i in range(n):
    init_df = pd.DataFrame({'a':pd.Series(dtype='int'), 'b':pd.Series(dtype='int')}, index=range(50))
    init_df['a'] = init_df.index
    init_store[f'{i+1}'] = init_df

# Function to update the dataframes with the new observations
def get_data(json_data):
    df = pd.read_json(json_data)
    compteur = df['a'][len(df['a'])-1]
    if len(df.columns) > 2:
        new_row = {'a':compteur + 1, 'b':random.randint(13,26), 'c':random.randint(13,26), 'd':random.randint(13,26)}
    else:
        new_row = {'a':compteur + 1, 'b':random.randint(13,26)}
    df = df.shift(periods=-1)
    df.iloc[len(df)-1] = new_row

    return(df.to_json())

# Function to update the graphs based on the dataframes
def update_graphs(json_data, column, index=0):
    
    df = pd.read_json(json_data)
    nb_obs = df.shape[0]
    x_new = df['a'][len(df)-1]        
    y_new = df[column][nb_obs-1]

    return dict(x=[[x_new]], y=[[y_new]]), index

colors = px.colors.qualitative.G10

def generate_graph_containers(index, json_data):

    dataframe = pd.read_json(json_data)
    X = dataframe['a']
    Y = dataframe.loc[:, dataframe.columns != 'a']
    graph_id = {'type': 'graph-', 'index': index}

    
    return( 
        html.Div(
            html.Div(
            dcc.Graph(
                id=graph_id,
                style={"height": "8rem"},
                config={
                    "staticPlot": False,
                    "editable": False,
                    "displayModeBar": False,
                },
                figure=go.Figure(
                            {
                                "data": [
                                    {
                                        "x": list(X),
                                        "y": list(Y[Y.columns[i]]),
                                        "mode": "lines",
                                        "name": Y.columns[i],
                                        "line": {"color": colors[i+2]},
                                    }
                                    for i in range(len(Y.columns))
                                ],
                                "layout": {
                                    "uirevision": True,
                                    "margin": dict(l=0, r=0, t=4, b=4, pad=0),
                                    "xaxis": dict(
                                        showline=False,
                                        showgrid=False,
                                        zeroline=False,
                                        showticklabels=False,
                                    ),
                                    "yaxis": dict(
                                        showline=False,
                                        showgrid=False,
                                        zeroline=False,
                                        showticklabels=False,
                                    ),
                                    "paper_bgcolor": "rgba(0,0,0,0)",
                                    "plot_bgcolor": "rgba(0,0,0,0)",
                                }
                            }
                        )
            )
        )
        )
    )

app = dash.Dash(__name__)

store = [dcc.Store(id={'type':'store-', 'index':i}, data=init_store[str(i)].to_json()) for i in range(n)]

def make_layout(): 
    return(
            html.Div(
        [   
            html.Div(
                store
            ),

            dcc.Interval(
                id = 'interval',
                interval = 1000,
                n_intervals = 0
            ),

            html.Div(
                [
                    generate_graph_containers(str(i), store[i].data) for i in range(n)
                ] 
            )
            
        ]
    )
)

app.layout = make_layout

@app.callback(
    Output(component_id={'type':'store-', 'index':MATCH}, component_property='data'),
    [ 
        Input('interval', 'n_intervals'),
        State(component_id={'type':'store-', 'index':MATCH}, component_property='data') 
    ]
)
def update_data(time, data):
    return(get_data(data))


@app.callback(
    Output(component_id={'type':'graph-', 'index':MATCH}, component_property='extendData'),
    Input(component_id={'type':'store-', 'index':MATCH}, component_property="data")
)
def update_graphs_callback(data):
    triggered_id = ctx.triggered_id
    print(triggered_id['index'])
    columns = ['b', 'c', 'd']

    if triggered_id['index'] == 0:
        for i in range(len(columns)):
            return(update_graphs(data, columns[i], i))
    else:
        return(update_graphs(data, 'b'))
  
if __name__ == '__main__':
    app.run_server(debug=True)
Asked By: The Governor

||

Answers:

I figured it out. The trick is in the format expected to update the extendData property of a figure. When trying to update several traces, the format should be a dictionary with a key for the x values and one for the y values. The values associated should be an array for each key, containing an array per trace. Don’t forget to add the trace indices after the dictionary. So for example, in the case of 3 distinct traces, the function should return something like:

dict(x=[[x_0], [x_1], [x_2]], y=[[y_0], [y_1], [y_2]]), [0, 1, 2]

Therefore the update_graphs function should be:

def update_graphs(json_data):
    
    df = pd.read_json(json_data)
    nb_obs = df.shape[0]

    x_new =  []  
    y_new = []
    trace_index = []
    for i in range(len(df.columns)-1):
        x_new.append([df['a'][len(df)-1]])
        y_new.append([df[df.columns[i+1]][nb_obs-1]])
        trace_index.append(i)
        
    return(dict(x=x_new, y=y_new), trace_index)

And the callback to update the graphs should be changed to:

@app.callback(
    Output(component_id={'type':'graph-', 'index':MATCH}, component_property='extendData'),
    Input(component_id={'type':'store-', 'index':MATCH}, component_property="data")
)
def update_graphs_callback(data):
    return(update_graphs(data))
Answered By: The Governor