Black Lives Matter. Please consider donating to Black Girls Code today.

Avoid callback method to fire one time for each output

Hi,

Imagine a dash web page with three numeric inputs with id=‘x’, ‘y’, and ‘z’. When the user changes the numeric value in any of them, a div with id=‘result’ should be updated with the result of some heavy calculation that depends on those three values. This is easily achieved by a method decorated with x, y, and z as input and result as output. So far everything is good. But now, imagine that we want a drop down menu where the user can load some hard coded values for x, y, and z. That can be done by a method which takes the drop down menu as input and x, y, and z as output.

Now to the problem: when the user selects one of the alternatives in the drop down menu, then the heavy calculation method will be fired three times instead of just once. How do you solve this? One solution would of course be to have a “start calculation” button so that the user would have to press it after changing one of the inputs. But I would like the heavy calculation to fire off as soon as user changes one value. I have thought about solutions involving hidden divs and global variables, but I can still not get it to work. Am I missing something?

In my actual real problem, the “heavy” calculation method takes around one second to run, but I have many more than 3 inputs, and it therefore takes a long time when user uses the dropdown compared to when user updates one of the inputs.

Below is a small example.


import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import os

app = dash.Dash(__name__)

app.layout = html.Div(
    children=[dcc.Input(id='x',
                        type="number",
                        value=0),
              dcc.Input(id='y',
                        type="number",
                        value=0),
              dcc.Input(id='z',
                        type="number",
                        value=0),
              dcc.Dropdown(id='dropdown',
                           options=[{'label': f'Alt {i}',
                                     'value': i} for i in range(2)],
                           value=0,
                           multi=False,
                           clearable=False
                           ),
              html.Div(id='result',
                       children='')
              ]
    )

# CALLBACKS
@app.callback(
    [Output(component_id='x',
            component_property='value'),
     Output(component_id='y',
            component_property='value'),
     Output(component_id='z',
            component_property='value')],
    [Input(component_id='dropdown',
           component_property='value')])
def load_hard_coded_values(alt_index):
    # Set x, y, and z to hard coded predefined values
    if alt_index == 0:
        return 1, 2, 3
    else:
        return 3, 2, 1


@app.callback(
    Output(component_id='result',
           component_property='children'),
    [Input(component_id='x',
            component_property='value'),
     Input(component_id='y',
            component_property='value'),
     Input(component_id='z',
            component_property='value')])
def update_calculation_output(x, y, z):
    print(f'Performing lengthy calculation with ({x}, {y}, {z})....')
    return x*y*z


if __name__ == '__main__':
    app.run_server(debug=True, host=os.environ['COMPUTERNAME'], port=8050)

Hi, from your function log we can see that the function is executing 3 times with same variables,

Screenshot_2020-03-30_16-21-39
which leads me to the approach you mention about using global variables, you can have the verification either inside the function of the heavy calculation or in the callback function like this :


import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
import os

app = dash.Dash(__name__)

XX = 1
YY = 2
ZZ = 3
app.layout = html.Div(
    children=[dcc.Input(id='x',
                        type="number",
                        value=0),
              dcc.Input(id='y',
                        type="number",
                        value=0),
              dcc.Input(id='z',
                        type="number",
                        value=0),
              dcc.Dropdown(id='dropdown',
                           options=[{'label': f'Alt {i}',
                                     'value': i} for i in range(2)],
                           value=0,
                           multi=False,
                           clearable=False
                           ),
              html.Div(id='result',
                       children='')
              ]
    )

# CALLBACKS
@app.callback(
    [Output(component_id='x',
            component_property='value'),
     Output(component_id='y',
            component_property='value'),
     Output(component_id='z',
            component_property='value')],
    [Input(component_id='dropdown',
           component_property='value')])
def load_hard_coded_values(alt_index):
    # Set x, y, and z to hard coded predefined values
    if alt_index == 0:
        return 1, 2, 3
    else:
        return 3, 2, 1


@app.callback(
    Output(component_id='result',
           component_property='children'),
    [Input(component_id='x',
            component_property='value'),
     Input(component_id='y',
            component_property='value'),
     Input(component_id='z',
            component_property='value')])
def update_calculation_output(x, y, z):
    global XX
    global YY
    global ZZ
    if(x != XX or y != YY or z != ZZ):
        XX = x 
        YY = y 
        ZZ = z
        print(f'Performing lengthy calculation with ({x}, {y}, {z})....')
        return x*y*z
    else:
        print("No calculation")
        raise PreventUpdate
    


if __name__ == '__main__':
    app.run_server(debug=True, host="127.0.0.1", port=8050)

Now if there will be many users at the same time using your app, it maybe a probleme having global variables manipulated like this since it may create conflicts, if not then you’re good to go.

As noted by @TimoDZ, the use of global variables is generally not recommended. For your use case, another option could be to use a Store component to keep track of state. It would look something like this,

import json
import dash
import dash_core_components as dcc
import dash_html_components as html

from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

# Create components.
input_keys = ["x", "y", "z"]
inputs = [dcc.Input(id=key, type="number", value=i + 1) for i, key in enumerate(input_keys)]
options = [{'label': f'Alt {i}', 'value': i} for i in range(2)]
selector = dcc.Dropdown(id='dropdown', value=0, multi=False, clearable=False, options=options)
result = html.Div(id='result')
# Create a store for storing previous inputs.
cache = dcc.Store(id='previous_inputs')
# Initialize the app.
app = dash.Dash(__name__)
app.layout = html.Div(inputs + [selector, result, cache])


@app.callback([Output(key, 'value') for key in input_keys],
              [Input('dropdown', 'value')])
def load_hard_coded_values(alt_index):
    if alt_index == 0:
        return 1, 2, 3
    return 3, 2, 1


@app.callback([Output('result', 'children'), Output('previous_inputs', 'data')],
              [Input(key, 'value') for key in input_keys],
              [State('previous_inputs', 'data')])
def update_calculation_output(x, y, z, cache):
    # Check if current inputs match previous inputs.
    if json.dumps([x, y, z]) == cache:
        print("No calculation")
        raise PreventUpdate
    # Otherwise, do calc and update the store.
    print(f'Performing lengthy calculation with ({x}, {y}, {z})....')
    return x * y * z, json.dumps([x, y, z])


if __name__ == '__main__':
    app.run_server(debug=True, host="127.0.0.1", port=8050)
1 Like

This looks like a very short and simple solution, and thanks for warning about the potential risks of using globals in this way. I was also surprised that dash is calling the same method multiple times with identical inputs - when I first noted that the calculation method was triggered several times, I assumed that the updates to the inputs (here x, y, and z) were done in sequential and each update triggered the callback. But as my example demonstrates, it seems the call is made with same inputs one time per output.

Solution by @TimoDZ looks very short and simple, and thanks for warning about the potential risks of using globals in this way. I was also surprised that dash is calling the same method multiple times with identical inputs. When I first noted that the calculation method was triggered several times, I assumed that the updates to the inputs (here x, y, and z) were done in sequential and each update triggered the callback. But as my example demonstrates, it seems the call is made with same inputs one time per output. Solution proposed by @Emil is conceptually similar, but it avoids using globals by using Store instead. Thanks a lot for all help, I will try this!

I ran the code by Emil and added a print statement of the cash content, a sleep as proxy for the calculation and a print of time at beginning and end of that method. Unfortunately it appears to not work. As expected the callback is fired three times after drop menu selection. The idea was that the last two calls would raise the PreventUpdate exception, but it looks like the two last calls are made before the first call finishes and actually updates the cache.

If my understanding is correct, then this problem could be more significant in the solution proposed by Emil since in that solution the comparison that is supposed to raise the PreventUpdate exception in the second and third call is made to the output of the first call, which will take some time to compute - and thereby the comparison is not working (it is made to an even earlier call to that method). However, my guess is that also solution by TimoDZ could in principle suffer from a similar issue - if the callbacks are fired almost simulatanously and if the number of inputs is large.

import json
import dash
import dash_core_components as dcc
import dash_html_components as html
import time

from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

Create components.

input_keys = [“x”, “y”, “z”]
inputs = [dcc.Input(id=key, type=“number”, value=i + 1) for i, key in enumerate(input_keys)]
options = [{‘label’: f’Alt {i}’, ‘value’: i} for i in range(2)]
selector = dcc.Dropdown(id=‘dropdown’, value=0, multi=False, clearable=False, options=options)
result = html.Div(id=‘result’)

Create a store for storing previous inputs.

cache = dcc.Store(id=‘previous_inputs’)

Initialize the app.

app = dash.Dash(name)
app.layout = html.Div(inputs + [selector, result, cache])

@app.callback([Output(key, ‘value’) for key in input_keys],
[Input(‘dropdown’, ‘value’)])
def load_hard_coded_values(alt_index):
if alt_index == 0:
return 1, 2, 3
return 3, 2, 1

@app.callback([Output(‘result’, ‘children’), Output(‘previous_inputs’, ‘data’)],
[Input(key, ‘value’) for key in input_keys],
[State(‘previous_inputs’, ‘data’)])
def update_calculation_output(x, y, z, cache):
# Check if current inputs match previous inputs.
print(f’update_calculation_output stated: {time.time()} ‘)
print(f’old cache: {cache}’)
if json.dumps([x, y, z]) == cache:
print(“No calculation”)
raise PreventUpdate
# Otherwise, do calc and update the store.
print(f’Performing lengthy calculation with ({x}, {y}, {z})…’)
time.sleep(1)
print(f’update_calculation_output finished: {time.time()} ')
return x * y * z, json.dumps([x, y, z])

if name == ‘main’:
app.run_server(debug=True, host=“127.0.0.1”, port=8050)

image

Do you think my understanding of the issues with the proposed solutions is correct?

Hello,
I’m new to dash.I’m trying to upload a file and do some analysis using that file in callback functions. In one callback function I’ve uploaded that file and returned the json format of that file to one particular id (i.e intermediate-value). But when I try to access the same json data in another callback,it is throwing an error (TypeError: the JSON object must be str, bytes or bytearray, not NoneType). Please provide me some help. Here is my code.
I’m getting error in my update_graph() function

import dash
import dash_core_components as dcc
import dash_html_components as html
import base64
import json

import io

import plotly.graph_objs as go


import pandas as pd
import numpy as np

import time

from dash.dependencies import Input, Output
import dash_table as dt





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

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

colors = {
    "graphBackground": "#F5F5F5",
    "background": "#ffffff",
    "text": "#000000"
}

app.layout = html.Div([
    dcc.Upload(
        id='upload-data',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Files')
        ]),
        style={
            'width': '100%',
            'height': '60px',
            'lineHeight': '60px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
            'margin': '10px'
        },
        # Allow multiple files to be uploaded
        multiple=True
    ),
    html.Div(id='intermediate-value',style={'display':'none'}),
    
    html.Div(id='output-data-upload'),
    html.Div(id='conf_matrix')
  

])


def parse_data(contents, filename):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV or TXT file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')))
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded))
        elif 'txt' or 'tsv' in filename:
            # Assume that the user uploaded an txt or tsv file
            df = pd.read_csv(
                io.StringIO(decoded.decode('utf-8')), delimiter = r'\s+')
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    return df



@app.callback(Output('intermediate-value','children'),
             
            [
                Input('upload-data', 'contents'),
                Input('upload-data', 'filename')
            ])
def update_table(contents, filename):
    #table = html.Div()
    #print("flag 0")
    if contents:
        print("flag 0")
        contents = contents[0]
        filename = filename[0]
        df = parse_data(contents, filename)
        df=pd.DataFrame(df)
        
        df =df.convert_dtypes()
        return df.to_json(orient='split')
        


@app.callback(Output('output-data-upload','children'),
             
            [
                Input('upload-data', 'contents'),
                Input('upload-data', 'filename')
            ])
def update_table(contents, filename):
    #table = html.Div()
    #print("flag 0")
    if contents:
        print("flag 0")
        contents = contents[0]
        filename = filename[0]
        df = parse_data(contents, filename)
        df=pd.DataFrame(df)
        
        df =df.convert_dtypes()
        

        def chkd(col):
            if col.dtype== 'float64' or col.dtype == 'Int64':
                return 'Continuous'
            else:
                return 'Categorical'
            
        df_tab=pd.DataFrame(df.describe(include='all')).stack().unstack(0)
        
        try:
            df_tab['cnt_null'] = df.apply(lambda col: col.isnull().sum())
            df_tab['null_%'] = df_tab.apply(lambda row: round((row['cnt_null']/(row['cnt_null']+row['count']))*100,2), axis=1)
            df_tab['cnt_zeros'] = df.isin([0]).sum(axis=0)
            df_tab['Data_Type'] = df.dtypes
            df_tab['variance']=df.var(axis=0)
            df_tab['mode'] = df.mode().iloc[0,:]
            df_tab['1%'] = df.quantile(0.01)
            df_tab['5%'] = df.quantile(0.05)
            df_tab['10%'] = df.quantile(0.1)
            df_tab['90%'] = df.quantile(0.9)
            df_tab['95%'] = df.quantile(0.95)
            df_tab['99%'] = df.quantile(0.99)
            df_tab['skew']=df.skew(axis=0)
            df_tab['kurtosis']=df.kurt(axis=0)
            df_tab['Variable_Type'] = df.apply(lambda col: 'Categorical' if col.dtype=='object' else chkd(col))
        
        
        except TypeError:
            pass
        
        df_tab=df_tab.reset_index(level=0)
        
        df_tab.rename(columns={'50%':'median'},inplace=True)
        df_tab.rename(columns={'count':'cnt_non_null'},inplace=True)
        df_tab.rename(columns={'index':'Variable'},inplace=True) 
        
        df_tab=df_tab[['Variable','Variable_Type','Data_Type','cnt_non_null','cnt_null','null_%','cnt_zeros','mean','std','mode','min','1%','5%','10%','25%','median','75%','90%','95%','99%','max','variance','skew','kurtosis']]

        
        #cols=[{'name': i, 'id': i} for i in df_tab.columns]
        df_tab = df_tab.sort_values(by ='Variable_Type',ascending=False)
        print("wait for 3 sec")
         
        time.sleep(3)
        
        df_tab.to_csv('univariate.csv')
        
        new_df = pd.read_csv('univariate.csv')
   
       
        table = html.Div([
            html.H5(filename),
            dt.DataTable(
               data=new_df.to_dict('records'),
               columns=[{'name': i, 'id': i} for i in df_tab.columns]
            ),
            ])        
                
        return table
    

#
@app.callback(Output('conf_matrix', 'children'), [Input('intermediate-value', 'children')])
def update_graph(jsonified_cleaned_data):

    # more generally, this line would be
    # json.loads(jsonified_cleaned_data)
    dff =json.loads(jsonified_cleaned_data)
    return str(dff)
    




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

I’m not sure why you ask this question in this thread, which is about something completely different?

I am also new to dash, but my understanding is that callbacks are fired when page is loading even when inputs to the callbacks have not been updated by user yet. In your case, the method is probably called with None instead of a str the first time. The error message indicates that the jsonified_cleaned_data variable is None insteda of a str. One solution would be to encapsulate the json.loads in a try clause and except TypeError (you may want to add ValueError as well) and return the empty str in that case.

I think that what you are seeing now is a bug in the latest release of Dash. I am running a build of the new wildcard branch,

and everything works as intended. However, if i install Dash from scratch from the official channels, i.e. pip, i see the issue that you are reporting.

I’m extremely Sorry for asking it in a different thread. But I needed help regarding Multiple callbacks. So I asked it over here.

I wanted to load the json data in update_graph() function. I tried the way you told. But its not working.
Please provide me some help.

Indeed, i can see what you mean and i think the callbacks are called in a parallel way that way the two other callbacks are fired and catch the “old cache” value before it gets updated by the first callback output. in this particular case where you have few variables (x,y and z) i think the only way you got now is the global var approach (like the example shown previously) since it is possible to update those values before even starting the heavy computation and you don’t have to wait to the return of the first callback which will be too late to prevent the other callbacks from firing which is the issue using Store. Hopefully it gets fixed soon.

Another way to do it using dcc.Store, is to have two separate callbacks one to verify the Store value and update it or raise the preventUpdate and another for the heavy work function that gets fired by any change in value of the dcc.Store , which i believe is how you do share data between callbacks without the need for global vars ( rather than having a hidden div ), i tried it and it seems working as intended :

import json
import dash
import dash_core_components as dcc
import dash_html_components as html
import time

from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

#Create components.
name = "main"
input_keys = ["x", "y", "z"]
inputs = [dcc.Input(id=key, type="number", value=i + 1) for i, key in enumerate(input_keys)]
options = [{'label': f'Alt {i}', 'value': i} for i in range(2)]
selector = dcc.Dropdown(id='dropdown', value=0, multi=False, clearable=False, options=options)
result = html.Div(id='result')

#Create a store for storing previous inputs.
cache = dcc.Store(id='previous_inputs')

#Initialize the app.
app = dash.Dash(name)
app.layout = html.Div(inputs + [selector, result, cache])

@app.callback([Output(key, 'value') for key in input_keys],
[Input('dropdown', 'value')])
def load_hard_coded_values(alt_index):
    if alt_index == 0:
        return 1, 2, 3
    return 3, 2, 1

#Update Store
@app.callback(Output('previous_inputs', 'data'),
[Input(key, 'value') for key in input_keys],
[State('previous_inputs', 'data')])
def update_Cache(x, y, z, cache):
# Check if current inputs match previous inputs.
    if json.dumps([x, y, z]) == cache:
        print("No calculation")
        raise PreventUpdate
    # Otherwise, update the store.
    return json.dumps([x, y, z])

# Real callback for computation 

@app.callback([Output('result', 'children')],[Input('previous_inputs', 'data')],[State(key, 'value') for key in input_keys])
def executeHeavyWork(cache,x,y,z):
    print(f'Performing lengthy calculation with ({x}, {y}, {z})…')
    time.sleep(1)
    print(f'update_calculation_output finished: {time.time()} ')
    return [x * y * z]
if name == 'main':
    app.run_server(debug=True, host="127.0.0.1", port=8050)

more info here

@gshetty: Do you still get same error message? Can you paste your new code?

@TimoDZ: Using two chained callbacks, where the first one just checks if we shall continue, should work most of the time. I will make a try with this type of solution. Would be really nice if Dash could be developed to feature more controll of when to fire callbacks. Thanks a lot for valuable input!

this indeed sounds like a bug. the forthcoming wildcards pull request on github should fix this and it will be published next week

1 Like

Maybe you can also try memoize:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import os
from flask_caching import Cache

app = dash.Dash(__name__)
timeout = 60
cache = Cache(app.server, config={
    'CACHE_TYPE': 'filesystem',
    'CACHE_DIR': 'cache-directory'
})

app.layout = html.Div(
    children=[dcc.Input(id='x',
                        type="number",
                        value=0),
              dcc.Input(id='y',
                        type="number",
                        value=0),
              dcc.Input(id='z',
                        type="number",
                        value=0),
              dcc.Dropdown(id='dropdown',
                           options=[{'label': f'Alt {i}',
                                     'value': i} for i in range(2)],
                           value=0,
                           multi=False,
                           clearable=False
                           ),
              html.Div(id='result',
                       children='')
              ]
    )

# CALLBACKS
@app.callback(
    [Output(component_id='x',
            component_property='value'),
     Output(component_id='y',
            component_property='value'),
     Output(component_id='z',
            component_property='value')],
    [Input(component_id='dropdown',
           component_property='value')])
def load_hard_coded_values(alt_index):
    # Set x, y, and z to hard coded predefined values
    if alt_index == 0:
        return 1, 2, 3
    else:
        return 3, 2, 1


@app.callback(
    Output(component_id='result',
           component_property='children'),
    [Input(component_id='x',
            component_property='value'),
     Input(component_id='y',
            component_property='value'),
     Input(component_id='z',
            component_property='value')])
@cache.memoize(timeout=timeout)
def update_calculation_output(x, y, z):
    print(f'Performing lengthy calculation with ({x}, {y}, {z})....')
    return x*y*z


if __name__ == '__main__':
    app.run_server(debug=True, host=os.environ['COMPUTERNAME'], port=8050)

Have you tried with the Dash 1.11.0 update?