Callback
  • All of the callbacks in a Dash app are executed with the initial value of their inputs when the app is first loaded. This is known as the "initial call" of the callback
  • A callback is executed when all of the callback's inputs have reached their final values
  • Each callback function can have multiple outputs
  • Callback must have Output(s) and Input(s)
  • Callback accepts the properties of an objects, instead of the object itself
  • Multiple Inputs Callback
    # assets/style.css
    body {
        font-family: "Lato", sans-serif;
        margin: 0;
        background-color: #F7F7F7;
    }
    
    .header {
        background-color: #222222;
        height: 288px;
        padding: 16px 0 0 0;
    }
    
    .header-emoji {
        font-size: 48px;
        margin: 0 auto;
        text-align: center;
    }
    
    .header-title {
        color: #FFFFFF;
        font-size: 48px;
        font-weight: bold;
        text-align: center;
        margin: 0 auto;
    }
    
    .header-description {
        color: #CFCFCF;
        margin: 4px auto;
        text-align: center;
        max-width: 384px;
    }
    
    .wrapper {
        margin-right: auto;
        margin-left: auto;
        max-width: 1024px;
        padding-right: 10px;
        padding-left: 10px;
        margin-top: 32px;
    }
    
    .card {
        margin-bottom: 24px;
        box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
    }
    
    .menu {
        height: 112px;
        width: 912px;
        display: flex;
        justify-content: space-evenly;
        padding-top: 24px;
        margin: -80px auto 0 auto;
        background-color: #FFFFFF;
        box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
    }
    
    .Select-control {
        width: 256px;
        height: 48px;
    }
    
    .Select--single > .Select-control .Select-value, .Select-placeholder {
        line-height: 48px;
    }
    
    .Select--multi .Select-value-label {
        line-height: 32px;
    }
    
    .menu-title {
        margin-bottom: 6px;
        font-weight: bold;
        color: #079A82;
    }
    		
    import dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output
    import pandas as pd
    import numpy as np
    
    data = pd.read_csv("avocado.csv")
    #data = data.query("type == 'conventional' and region == 'Albany'")
    data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
    data.sort_values("Date", inplace=True)
    
    app = dash.Dash(__name__)
    
    app.layout = html.Div(
            children = [html.Div(
                children = [html.H1(children="Avocado Analytics",
                className="header-title",
        )]
                ),
    
                html.Div(
                children=[
                    html.Div(children="Region", className="menu-title"),
                    dcc.Dropdown(
                        id="region-filter",
                        options=[
                            {"label": region, "value": region}
                            for region in np.sort(data.region.unique())
                        ],
                        value="Albany",
                        clearable=False,
                        className="dropdown",
                    ),
                ]
            ),
            html.Div(
                children=[
                    html.Div(children="Type", className="menu-title"),
                    dcc.Dropdown(
                        id="type-filter",
                        options=[
                            {"label": avocado_type, "value": avocado_type}
                            for avocado_type in data.type.unique()
                        ],
                        value="organic",
                        clearable=False,
                        searchable=False,
                        className="dropdown",
                    ),
                ],
            ),
    
                html.Div(
            children = dcc.Graph(
                id="volume-chart", config={"displayModeBar": False},
            ),
            className="card"
            )
        ]
    )
    
    @app.callback(
        Output("volume-chart", "figure"),
        [
            Input("region-filter", "value"),
            Input("type-filter", "value"),
        ],
    )
    def update_plot(region, type):
        mask = (
            (data.region == region)
            & (data.type == type))
        filtered_data = data.loc[mask, :]
    
        volume_chart_figure = {
            "data": [
                {
                    "x": filtered_data["Date"],
                    "y": filtered_data["Total Volume"],
                    "type": "lines",
                },
            ],
            "layout": {
                "title": {
                    "text": "Avocados Sold",
                    "x": 0.05,
                    "xanchor": "left"
                },
                "xaxis": {"fixedrange": True},
                "yaxis": {"fixedrange": True},
                "colorway": ["#E12D39"],
            },
        }
    
        return volume_chart_figure
    
    if __name__ == "__main__":
        app.run_server(debug=True)
    		
    Multiple Outputs Callback
    import dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output
    
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    app.layout = html.Div([
        html.Label('Label 1', id='id_1'),
        html.Label('Label 2', id='id_2'),
        dcc.Input(id='input'),
    ])
    
    @app.callback(
        [Output(component_id='id_1', component_property='children'),
            Output(component_id='id_2', component_property='children')],
        Input(component_id='input', component_property='value')
    )
    def update_outputs(input_value):
        if input_value:
            return 'Label 1: '+input_value, 'Label 2: '+input_value
        else:
            return 'Label 1:', 'Label 2'
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		
    Chained Callbacks
    import dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output
    
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    app.layout = html.Div([
        html.Label('Label 1', id='id_1'),
        html.Label('Label 2', id='id_2'),
        dcc.Input(id='input'),
    ])
    
    @app.callback(
        Output(component_id='id_1', component_property='children'),
        Input(component_id='input', component_property='value')
    )
    def update_input_label1(input_value):
        if input_value:
            return 'Label 1: '+input_value
    
    @app.callback(
        Output(component_id='id_2', component_property='children'),
        Input(component_id='id_1', component_property='children')
    )
    def update_label1_label2(input_value):
        if input_value:
            return 'Label 2: '+input_value
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		
    State
  • Input, components response right away
  • State, do not response until the user finish entering all of information in the form
  • import dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output, State
    
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    app.layout = html.Div([
        dcc.Input(id='input-state', type='text', value='input'),
        html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
        html.Div(id='output-state')
    ])
    
    
    @app.callback(Output('output-state', 'children'),
                  Input('submit-button-state', 'n_clicks'),
                  State('input-state', 'value')
                  )
    def update_output(n_clicks, input):
        return u'''
            The Button has been pressed {} times,
            Input is "{}",
        '''.format(n_clicks, input)
    
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		
    PreventUpdate
  • Do not update the callback output
  • import dash
    import dash_html_components as html
    from dash.dependencies import Input, Output
    from dash.exceptions import PreventUpdate
    
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    app.layout = html.Div([
        html.Button('Click here to see the content', id='show-secret'),
        html.Div(id='body-div')
    ])
    
    # suppress exception when a component does not exist in the initial state
    app.config['suppress_callback_exceptions'] = True
    
    @app.callback(
        Output(component_id='body-div', component_property='children'),
        Input(component_id='show-secret', component_property='n_clicks')
    )
    def update_output(n_clicks):
        if n_clicks is None:
            raise PreventUpdate # do not update the callback output
        else:
            return html.Label(id='label')
    
    @app.callback(
        Output(component_id='label', component_property='children'),
        Input(component_id='show-secret', component_property='n_clicks')
    )
    def update_label(n_clicks):
        return 'Clicks: '+str(n_clicks)
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		
    dash.callback_context
  • A global variable, available only inside a callback
  • Use to Determine which Input has fired
  • import json
    
    import dash
    import dash_html_components as html
    import dash_core_components as dcc
    from dash.dependencies import Input, Output
    
    app = dash.Dash(__name__)
    
    app.layout = html.Div([
        dcc.Input(id='input1'),
        dcc.Input(id='input2'),
        html.Div(id='container')
    ])
    
    
    @app.callback(Output('container', 'children'),
                  [Input('input1', 'value'),
                  Input('input2', 'value')]
                  )
    def display(click1, click2):
        ctx = dash.callback_context
    
        if not ctx.triggered:
            button_id = 'No clicks yet'
        else:
            button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
        ctx_msg = json.dumps({
            'states': ctx.states,
            'triggered': ctx.triggered,
            'inputs': ctx.inputs
        }, indent=2)
            
        return html.Pre(ctx_msg)
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		
    Sharing Data Between Callbacks

  • Use global variable
  • import os
    import dash
    import pandas as pd
    
    from dash.dependencies import Output, Input
    from dash.exceptions import PreventUpdate
    
    import dash_html_components as html
    import dash_core_components as dcc
    import dash_table
    import plotly.graph_objects as go
    from dash.exceptions import PreventUpdate
    
    app = dash.Dash(__name__)
    
    df = None
    
    def get_files():
        container = []
        for root, dirs, files in os.walk("./"):
            for file in files:
                if file.endswith(".csv"):
                    container.append(file)
    
        return [{'label': file, 'value': file} for file in container]
    
    def get_fields(data):
        fields = []
        for item in data.columns:
            t = data[item].dtype
            if t == 'int' or t =='float':
                fields.append(item)
        return [{'label': field, 'value': field} for field in fields]
    
    app.layout = html.Div([
        #dcc.Store(id='output'),
        dcc.Dropdown(id='field1', options=get_files()),
        html.Div(id='content'),
    ])
    
    @app.callback(Output('content', 'children'),
                  Input('field1', 'value'))
    def update_fields(value):
        if not value:
            raise PreventUpdate
    
        print(value)
    
        global df
        df = pd.read_csv(value)
        print(df)
    
        graph = dcc.Graph(id='graph')
    
        fields = get_fields(df)
    
        dropdown = dcc.Dropdown(id='field2', options=get_fields(df), multi=True)
    
        return [dropdown, graph]
    
    @app.callback(Output('graph', 'figure'),
                  Input('field2', 'value'))
    def update_fields2(value):
        if not value:
            raise PreventUpdate
        if len(value) < 2:
            raise PreventUpdate
    
        fig = go.Figure()
        for i in range(1, len(value)):
            fig.add_trace(go.Scatter(x=df[value[0]], y=df[value[i]], mode='markers', name=value[i]))
        return fig
    
    if __name__ == '__main__':
        app.run_server(debug=True, threaded=True, port=10450)
    		
  • Three places to save data
  • Saving the data as part of Dash's front-end store
  • import dash
    import dash_core_components as dcc
    import dash_html_components as html
    import numpy as np
    import pandas as pd
    from dash.dependencies import Input, Output
    from dash.exceptions import PreventUpdate
    import plotly.express as px
    
    external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    app.layout = html.Div([
        html.Div(id='display-1'),
        html.Div(id='display-2'),
        dcc.Dropdown(id='dropdown',
            options=[
                {'label': 'A', 'value': 'A'},
                {'label': 'B', 'value': 'B'}
                ]),
    
        # Hidden div inside the app that stores the intermediate value
        html.Div(id='intermediate-value', style={'display': 'none'})
    ])
    
    @app.callback(Output('intermediate-value', 'children'), Input('dropdown', 'value'))
    def clean_data(value):
        if not value:
            raise PreventUpdate
        cleaned_df = pd.read_csv('solar.csv')
    
        # more generally, this line would be
        # json.dumps(cleaned_df)
        print('Output intermediate value ...')
        return cleaned_df.to_json(date_format='iso', orient='split')
    
    @app.callback(Output('display-1', 'children'),
                  Input('intermediate-value', 'children'))
    def update_output_1(value):
        dff = pd.read_json(value, orient='split')
        return str(dff)
    
    @app.callback(Output('display-2', 'children'),
                  Input('intermediate-value', 'children'))
    def update_output_1(value):
        dff = pd.read_json(value, orient='split')
        return str(dff)
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		

  • Save the data on the filesystem cache with a session ID and then reference the data using that session ID
  • Because data is saved on the server instead of transported over the network, this method is generally faster than the "hidden div" method
  • Note that filesystem cache doesn't work on systems with ephemeral filesystems like Heroku
  • import dash
    from dash.dependencies import Input, Output
    import dash_core_components as dcc
    import dash_html_components as html
    import datetime
    from flask_caching import Cache
    import os
    import pandas as pd
    import time
    import uuid
    
    external_stylesheets = [
        # Dash CSS
        'https://codepen.io/chriddyp/pen/bWLwgP.css',
        # Loading screen CSS
        'https://codepen.io/chriddyp/pen/brPBPO.css']
    
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    cache = Cache(app.server, config={
        'CACHE_TYPE': 'redis',
        # Note that filesystem cache doesn't work on systems with ephemeral
        # filesystems like Heroku.
        'CACHE_TYPE': 'filesystem',
        'CACHE_DIR': 'cache-directory',
    
        # should be equal to maximum number of users on the app at a single time
        # higher numbers will store more data in the filesystem / redis cache
        'CACHE_THRESHOLD': 200
    })
    
    
    def get_dataframe(session_id):
        @cache.memoize()
        def query_and_serialize_data(session_id):
            # expensive or user/session-unique data processing step goes here
    
            # simulate a user/session-unique data processing step by generating
            # data that is dependent on time
            now = datetime.datetime.now()
    
            # simulate an expensive data processing task by sleeping
            time.sleep(5)
    
            df = pd.DataFrame({
                'time': [
                    str(now - datetime.timedelta(seconds=15)),
                    str(now - datetime.timedelta(seconds=10)),
                    str(now - datetime.timedelta(seconds=5)),
                    str(now)
                ],
                'values': ['a', 'b', 'a', 'c']
            })
            return df.to_json()
    
        return pd.read_json(query_and_serialize_data(session_id))
    
    
    def serve_layout():
        session_id = str(uuid.uuid4())
    
        return html.Div([
            html.Div(session_id, id='session-id', style={}),
            html.Button('Get data', id='get-data-button'),
            html.Div(id='output-1'),
            html.Div(id='output-2')
        ])
    
    
    app.layout = serve_layout
    
    
    @app.callback(Output('output-1', 'children'),
                  Input('get-data-button', 'n_clicks'),
                  Input('session-id', 'children'))
    def display_value_1(value, session_id):
        df = get_dataframe(session_id)
        return html.Div([
            'Output 1 - Button has been clicked {} times'.format(value),
            html.Pre(df.to_csv())
        ])
    
    
    @app.callback(Output('output-2', 'children'),
                  Input('get-data-button', 'n_clicks'),
                  Input('session-id', 'children'))
    def display_value_2(value, session_id):
        df = get_dataframe(session_id)
        return html.Div([
            'Output 2 - Button has been clicked {} times'.format(value),
            html.Pre(df.to_csv())
        ])
    
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    		

  • Use Store
  • import os
    import dash
    import pandas as pd
    
    from dash.dependencies import Output, Input, State
    from dash.exceptions import PreventUpdate
    
    import dash_html_components as html
    import dash_core_components as dcc
    import dash_table
    import plotly.graph_objects as go
    from dash.exceptions import PreventUpdate
    
    app = dash.Dash(__name__)
    
    def get_files():
        container = []
        for root, dirs, files in os.walk("./"):
            for file in files:
                if file.endswith(".csv"):
                    container.append(file)
    
        return [{'label': file, 'value': file} for file in container]
    
    def get_fields(data):
        fields = []
        for item in data.columns:
            t = data[item].dtype
            if t == 'int' or t =='float':
                fields.append(item)
        return [{'label': field, 'value': field} for field in fields]
    
    app.layout = html.Div([
        dcc.Store(id='memory'),
        dcc.Dropdown(id='field1', options=get_files()),
        html.Div(id='content'),
    ])
    
    @app.callback(Output('memory', 'data'),
                  Input('field1', 'value'))
    def update_store(value):
        if not value:
            raise PreventUpdate
    
        df = pd.read_csv(value)
        return df.to_dict('records')
    
    @app.callback(Output('content', 'children'),
                  Input('memory', 'modified_timestamp'),
                  State('memory', 'data'))
    def update_fields(ts, data):
        if ts is None:
            raise PreventUpdate
    
        df = pd.DataFrame.from_dict(data)
        graph = dcc.Graph(id='graph')
    
        fields = get_fields(df)
    
        dropdown = dcc.Dropdown(id='field2', options=get_fields(df), multi=True)
    
        return dropdown, graph
    
    @app.callback(Output('graph', 'figure'),
                  Input('field2', 'value'),
                  State('memory', 'data'))
    def update_fields2(value, data):
        if not value:
            raise PreventUpdate
        if len(value) < 2:
            raise PreventUpdate
    
        df = pd.DataFrame.from_dict(data)
    
        fig = go.Figure()
        for i in range(1, len(value)):
            fig.add_trace(go.Scatter(x=df[value[0]], y=df[value[i]], mode='markers', name=value[i]))
        return fig
    
    if __name__ == '__main__':
        app.run_server(debug=True, threaded=True, port=10450)
    		
    Reference
  • Develop Data Visualization Interfaces in Python With Dash