Is it possible to add traces to a dcc.Graph dinamically?

I will try to explain it clearly.

I am performing a dash application, and I want to dinamically add new traces to a scatter plot. Here is an example of the code.

app = dash.Dash(name, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.scripts.config.serve_locally = True
app.config[‘suppress_callback_exceptions’] = True

app.layout = html.Div([
html.Div([
dcc.Dropdown(id=‘scatterXVar’, value=None, style={‘display’: ‘none’}),
dcc.Dropdown(id=‘scatterYVar’, value=None, style={‘display’: ‘none’}),
], id=‘scatterVariables’),

html.Button(‘Add Trace’, id=‘addTrace’)
)]),

dcc.Graph(id=‘scatterGraph’)

I have two dropdowns in the layout, one corresponds to the X-Variable I want to scatter, and the other one to the Y-Variable.

The idea is, once I have selected which two variables I wanna scatter, whenever I click the button, a trace is added to the graph. My problem is that after I scatter the first trace, whenever I want to add another trace to the graph, the previous traces are erased, and it only adds the new trace, and what I want is to preserve all the traces together.

Here is my callback:

@app.callback(Output(‘scatterGraph’, ‘figure’),
[Input(‘addTrace’, ‘n_clicks’)],
[State(‘scatterXVar’, ‘value’),
State(‘scatterYVar’, ‘value’)])
def sccatterGr(nclicks, scatterXval, scatterYval): # da):
if scatterXval is None or scatterYval is None or scatterXval == or scatterYval == :
return {}
else:
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df[scatterXval.encode(‘utf-8’)],
y=df[scatterYval.encode(‘utf-8’)],
mode=‘markers’,
marker={
‘size’: 15,
‘opacity’: 0.5,
‘line’: {‘width’: 0.5, ‘color’: ‘white’}
}))
return fig

Please, I need help so badly.

I need help please, it’s important. Someone who let me know if it is possible? Thanks

hmm… it seems like what you are trying to do is store the history or traces for a figure so that with each incremental drop down value pair passed in, the graph is updated with the new trace as well as all the previous traces that were generated.

The problem with your current code is that each time the callback is triggered, you are initializing a new empty figure with fig = go.Figure() which is why you are only getting the newly added trace.

I’m not sure how to you can somehow append the traces to a growing list of traces that you can reference each time a callback is generated. I don’t know if this would work but have you tried to not initialize a new empty Figure() in your callback each time.

also try passing in figure as State and then mutating it

Thanks both! I found a solution a few days ago, and it is the one that @chriddyp comments. Passing the figure as State and mutating whenever the callback is generated.

I know it’s a year later but can you link the solution you found? What do you mean by “mutate”?

Interested to see the solution as well

@chriddyp can you please explain your answer in more detail? I’m having the same issue. Thank you.

I could be wrong, but if you add State(‘scatterGraph’, ‘figure’) to the callback, then the function can take in the figure and add to it as required with your_variable.add_trace()
This is the same as taking it as Input(‘scatterGraph’, ‘figure’) except it is not trigerred by a change in the element, therefore avoiding circular callbacks.

This is what I did. You can pass the figure as a State, then either convert it into a figure object by passing the figure data into graph_objects.Figure(), or append the new trace to figure[‘data’] as a dictionary.

@riskfree Here is some sample code that might help you with taking in an object as State and acting upon it, as i think @chriddyp was alluding to. In this case, each time you click the button a random point is added to the current figure:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
import random

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Graph(
        id='graph',
        figure=go.Figure()
    ),
    html.Button(
        id='button',
        name="Add Random",
        children="Add Random",
    ),
])


@app.callback(
    Output('graph', 'figure'),
    Input('button', 'n_clicks'),
    State('graph', 'figure'),
)
def update_figure(dummy, fig):
    # print(fig)
    fig = go.Figure(fig)
    fig.add_trace(go.Scatter(x=[random.random()], y=[random.random()]))
    return fig


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

Hope this helps!

@Parakiwi thank you. I’m trying to add a Scattermapbox trace to my Choroplethmapbox by using fig.add_trace(go.Scattermapbox()) and it still takes quite a long time for the scatter point to show (2-3 seconds). It’s only one point when the user clicks on the map. I’m not sure what is causing this delay and how to mitigate it. Here is the code if you may be able to help. The choropleth file is approx 4mb after minifying, but I don’t think it matters if the callback is in fact not redrawing the trace.

@app.callback(

    Output("graph", "figure"),

    [Input("graph", "clickData")],

    [State("graph", "figure")],

)

def update_figure(clickData, graph):

    if clickData is not None:

        location = clickData["points"][0]["location"]
 
        a = go.Figure(graph)

        a.add_trace(

            go.Scattermapbox(

                lat=center_file[center_file["geoid"] == location]["lat"],

                lon=center_file[center_file["geoid"] == location]["lon"],

                mode="markers",

                marker=dict(

                    size=15,

                    opacity=0.85,

                    color="red",

                ),

            )

        )

        a.update_layout(margin=dict(t=0, l=0, b=0, r=0))

    else:

        a = initial_graph()

    return a

@aboyher did you see a big jump in performance after doing so? Would you be able to share your code?

Will probably need to see some more of your code to try and help with that mate, depending on how the layout is set up and your initial_graph() function

Thanks @Parakiwi - This is it exactly :relieved: