🚀 Gen 5 of the leading AI app deployment platform launches October 6. Click for the livestream.

My callback is slowing down my dashboard a lot, is it possible to create a Client-Side replication?

Greetings,

I’m trying to see if it’s possible to implement my callback using client side javascript. I noticed on the https://dash.plotly.com/clientside-callbacks documentation, at the bottom, it says: Clientside callbacks are not possible if you need to refer to global variables on the server or a DB call is required. My callback gets data from my SQL dB, I was wondering if this would be an issue?

I have an interactive multi-dropdown that feeds inputted values into my graph callback, which pulls required parameters from my dB using SQL to provide X and Y data for my graph. The return generates my graph and some additional surrounding HTML.

@app.callback(
    dash.dependencies.Output("dynamic-dropdown", "options"),
    [dash.dependencies.Input("dynamic-dropdown", "search_value")],
    [dash.dependencies.State("dynamic-dropdown", "value")],
)
def update_multi_options(search_value, value):
    if not search_value:
        raise PreventUpdate
    # Make sure that the set values are in the option list, else they will disappear
    # from the shown select list, but still part of the `value`.
    return [
        o for o in OPTIONS if search_value in o["label"] or o["value"] in (value or [])
    ]



@app.callback(
    Output('graph', 'children'),
    [Input('dynamic-dropdown', 'value')],
    [State('graph','children')])

def tickers(symbols, children):
    conn.rollback()
    if symbols == None:
        conn.rollback()
        return []
    elif symbols == []:
        conn.rollback()
        return []
    else:
        stock_info = {}
        d = {} #dates
        p = {} #prices

        sym_ids = tuple([id for id in symbols])

        stock_info = {}
        stock_info = get_dict_resultset("SELECT symbol, date, adj_close FROM api.security_price WHERE security_price.symbol IN %s AND date > (SELECT MAX(date) FROM api.security_price) - interval '5 years' ORDER by date;", [sym_ids])

        stock_data_by_symbol = defaultdict(list)
        for entry in stock_info:
            symbol = entry['symbol']
            stock_data_by_symbol[symbol].append(entry)

        trace = []
        for stock in symbols:
            d[stock] = [rec['date'] for rec in stock_data_by_symbol[stock]] 
            p[stock] = [rec['adj_close'] for rec in stock_data_by_symbol[stock]]
            trace.append(go.Scatter(x=d[stock],
                                 y=p[stock],
                                 mode='lines',
                                 text = d[stock],
                                 opacity=0.7,
                                 name=stock,
                                 textposition='bottom center'))


        traces = [trace]
        data = [val for sublist in traces for val in sublist]
        figure = {'data': data,
                  'layout': go.Layout(
                      colorway=["#9b5de5", '#00f5d4', '#FFD23F', '#f15bb5', '#f71735', '#d08c60'],
                      paper_bgcolor='rgba(0, 0, 0, 0)',
                      plot_bgcolor='rgba(0, 0, 0, 0)',
                      margin={
                        'l': 40, # left margin, in px
                        'r': 10, # right margin, in px
                        't': 16, # top margin, in px
                        'b': 30,}, # bottom margin, in px
                      hovermode='x',
                      legend={"x" : 0, "y" : 1, 'font': {'size': 10}},
                      xaxis={'rangeselector': {'buttons': list([
                                                            {'count': 1, 'label': '1M', 
                                                             'step': 'month', 
                                                             'stepmode': 'backward'},
                                                            {'count': 3, 'label': '3M', 
                                                             'step': 'month', 
                                                             'stepmode': 'backward'},
                                                            {'count': 6, 'label': '6M', 
                                                             'step': 'month',
                                                             'stepmode': 'backward'},
                                                            {'count': 1, 'label': '1Y', 
                                                             'step': 'year',
                                                             'stepmode': 'backward'},
                                                            {'count': 3, 'label': '3Y', 
                                                             'step': 'year',
                                                             'stepmode': 'backward'},
                                                            {'count': 1, 'label': 'YTD', 
                                                             'step': 'year', 
                                                             'stepmode': 'todate'},
                                                            {'step': 'all','label':'MAX'},

                                                            ])},
                         'rangeslider': {'visible': True}, 'type': 'date'},

                      
                      
                  ),


                  }

        
        children = [    
               dbc.Row(dbc.Col(html.P("As of {}, {}".format(d[stock][-1].strftime('%A'),d[stock][-1].strftime('%d %m-%Y')),style={'font-size':'12px','font_family':'Helvetica Neue', 'margin-top':'10px'}),style={'position':'relative','float':'right'}, width={"offset": 3}), justify='end'),
               html.Div(style={"width":"100%", "height":'30px'}),
               html.H4('Historical Price Chart:', style={'color':'#474747','margin-left':'30px'}),
               html.P('Interactive graph for end-of-day prices', style={'color':'#A2A2A2','margin-left':'30px'}), 
               dcc.Graph(id='output-graph', figure=figure ,config={'displayModeBar': False}, animate=True,style = {'width': '100%',  'touch-action':'none'}), 
               html.P('* Drag sliders to create your own custom date range', style={'font-size':'12px','color':'#A2A2A2','margin-left':'40px'}),
               html.P('** Double-tap or double-click the main graph to reset axis', style={'font-size':'12px','color':'#A2A2A2','margin-left':'40px'}),
               html.Div(style={"width":"100%", "height":'10px'}),
        ]


        return dcc.Loading(id='graph',children=children,type='graph')

Because users can input multiple values in my dropdown, things can get really slow really fast. Having to re-download all the data for each callback fire makes no sense. Is there any way to fix this issue a callback design? The disclaimer at the bottom of the documentation has given me doubts.

If you are re-downloading the same data from the DB multiple time, you could cache the results to avoid multiple (slow) queries. However, as i read your code, the SQL query depends on the drop down selection?

Hi Emil, that is correct, the SQL query is based off the input. More specifically, a user inputs the symbols for stock data they want to see.

In that case I am not sure why you are unhappy “re-downloading the data” every time; if it’s not the same data, there is no way to avoid the download? Assuming the SQL query is the slow part (please correct me if i am wrong), i don’t think it would make any significant performance difference moving the query client side.

For example. If a user has one input, they download the data for that value. The same user decides to add another value into the drop. Their browser will download data for the new value, and re-download the same data from the first value. The SQL does not affect performance, what I don’t understand is the disclaimer in the bottom of the callback documentation that says limitations don’t allow for dB calls. In response to your comment about performance with client side callback, I want to make the graph client side.

Ah, now i understand :slight_smile: . In that case i would suggest creating a caching layer that holds the current data. You could then create one callback that does a delta update (i.e. download values that are missing as compared to the previous selections) and another one that draws the graph (based on the cached data + the current selections).

Sorry for the confusion :), what would you suggest for a caching layer? and the delta callback, would it be a server side or client side callback?

From an implementation point of view, the easiest solution would probably be a Store component (client side storage) for the cache and a server side callback for the for the delta update.

If the amount of data is large (~ MB), it might be worth thinking about reducing data transfer by keeping the data either server side or client side. Which solution will be the most performant depends on the network speeds between the different entities (client, server, and SQL server). However, as you note, with Dash the latter might not be possible (I haven’t tried).

Hence, if performance is an issue, i would try with a server side solution, i.e. server side callbacks both for the delta update and for drawing the graph. And server side storage, e.g. a file or a redis cache; possibly via a ServersideOutput.

1 Like

Thank you so much for the help. Do you think implementing my graph as a JS client side callback will help preformance?

When the data are on the server, i don’t think it would make a difference. Keep in mind that graphs in Dash are represented as a JSON structure, which is only rendered to a graph on the client anyway.

Thank you, you saved me from going down a road where I would end up being disappointed :).

1 Like

I keep running into an error when I try to use dcc.Store()

It keeps saying `Output object is not iterable’? I’m just trying to share the input data from users between callbacks. Here is my syntax below!

body = html.Div(dcc.Store(id='memory-output') #.... and more divs

@app.callback(
    dash.dependencies.Output("memory-output", "data"),
    dash.dependencies.Output("dynamic-dropdown", "options"),
    [dash.dependencies.Input("dynamic-dropdown", "search_value")],
    [dash.dependencies.State("dynamic-dropdown", "value")],
)
def update_multi_options(search_value, value):
    if not search_value:
        raise PreventUpdate
    # Make sure that the set values are in the option list, else they will disappear
    # from the shown select list, but still part of the `value`.
    return [
        o for o in OPTIONS if search_value in o["label"] or o["value"] in (value or [])
    ]



@app.callback(
    Output('graph', 'children'),
    [Input('memory-output, 'data')],
    [State('graph','children')])

def tickers(symbols, children):
    conn.rollback()
    if symbols == None:
        conn.rollback()
        return []
    elif symbols == []:
        conn.rollback()
        return []
    else:
        stock_info = {}
        d = {} #dates
        p = {} #prices

        sym_ids = tuple([id for id in symbols])

        stock_info = {}
        stock_info = get_dict_resultset("SELECT symbol, date, adj_close FROM api.security_price WHERE security_price.symbol IN %s AND date > (SELECT MAX(date) FROM api.security_price) - interval '5 years' ORDER by date;", [sym_ids])

        stock_data_by_symbol = defaultdict(list)
        for entry in stock_info:
            symbol = entry['symbol']
            stock_data_by_symbol[symbol].append(entry)

        trace = []
        for stock in symbols:
            d[stock] = [rec['date'] for rec in stock_data_by_symbol[stock]] 
            p[stock] = [rec['adj_close'] for rec in stock_data_by_symbol[stock]]
            trace.append(go.Scatter(x=d[stock],
                                 y=p[stock],
                                 mode='lines',
                                 text = d[stock],
                                 opacity=0.7,
                                 name=stock,
                                 textposition='bottom center'))


        traces = [trace]
        data = [val for sublist in traces for val in sublist]
        figure = {'data': data,
                  'layout': go.Layout(
                      colorway=["#9b5de5", '#00f5d4', '#FFD23F', '#f15bb5', '#f71735', '#d08c60'],
                      paper_bgcolor='rgba(0, 0, 0, 0)',
                      plot_bgcolor='rgba(0, 0, 0, 0)',
                      margin={
                        'l': 40, # left margin, in px
                        'r': 10, # right margin, in px
                        't': 16, # top margin, in px
                        'b': 30,}, # bottom margin, in px
                      hovermode='x',
                      legend={"x" : 0, "y" : 1, 'font': {'size': 10}},
                      xaxis={'rangeselector': {'buttons': list([
                                                            {'count': 1, 'label': '1M', 
                                                             'step': 'month', 
                                                             'stepmode': 'backward'},
                                                            {'count': 3, 'label': '3M', 
                                                             'step': 'month', 
                                                             'stepmode': 'backward'},
                                                            {'count': 6, 'label': '6M', 
                                                             'step': 'month',
                                                             'stepmode': 'backward'},
                                                            {'count': 1, 'label': '1Y', 
                                                             'step': 'year',
                                                             'stepmode': 'backward'},
                                                            {'count': 3, 'label': '3Y', 
                                                             'step': 'year',
                                                             'stepmode': 'backward'},
                                                            {'count': 1, 'label': 'YTD', 
                                                             'step': 'year', 
                                                             'stepmode': 'todate'},
                                                            {'step': 'all','label':'MAX'},

                                                            ])},
                         'rangeslider': {'visible': True}, 'type': 'date'},

                      
                      
                  ),


                  }

        
        children = [    
               dbc.Row(dbc.Col(html.P("As of {}, {}".format(d[stock][-1].strftime('%A'),d[stock][-1].strftime('%d %m-%Y')),style={'font-size':'12px','font_family':'Helvetica Neue', 'margin-top':'10px'}),style={'position':'relative','float':'right'}, width={"offset": 3}), justify='end'),
               html.Div(style={"width":"100%", "height":'30px'}),
               html.H4('Historical Price Chart:', style={'color':'#474747','margin-left':'30px'}),
               html.P('Interactive graph for end-of-day prices', style={'color':'#A2A2A2','margin-left':'30px'}), 
               dcc.Graph(id='output-graph', figure=figure ,config={'displayModeBar': False}, animate=True,style = {'width': '100%',  'touch-action':'none'}), 
               html.P('* Drag sliders to create your own custom date range', style={'font-size':'12px','color':'#A2A2A2','margin-left':'40px'}),
               html.P('** Double-tap or double-click the main graph to reset axis', style={'font-size':'12px','color':'#A2A2A2','margin-left':'40px'}),
               html.Div(style={"width":"100%", "height":'10px'}),
        ]


        return dcc.Loading(id='graph',children=children,type='graph')


I’ve tried using the dcc.Store for othercall backs and I kept getting the same message, would appreciate if anyone could guide me why?

Have you checked how the Store content looks like (e.g. by printning it)? My immediate guess would be that it is a JSON string that must be deseriazed before you can iterate it.

Could be the datetime format from my SQL query?