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

Combine callbacks with serve_layout()

Hello,
I’m currently building a dash app on AWS Elastic Beanstalk and I’m hoping to refresh the data by making a call to S3 every time the user reloads the page. This is fairly straight forward by placing my entire layout along with the call to S3 in a function called serve_layout() then defining app.layout = serve_layout.

However, things are a little more complicated because I also have a callback linked to a dropdown which filters the data, so I can’t place my callback function within serve_layout(). I would prefer not to make a call to S3 every callback as this is expensive.

There must be an easy way to do this: reload data on page refresh AND filter data with dropdown (without reloading on every callback)

I have searched through this blog, which seems like my best bet: https://dash.plot.ly/sharing-data-between-callbacks,
but I wanted to check if there was a quick fix before I implement one of the solutions there.

Here’s my code:

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
import boto3
from datetime import datetime, timedelta


# Create Dash App
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
application = app.server

# Get data and metadata
full = pd.read_csv('s3://commcare/enriched_data.csv', engine='python')
full['timestamp'] = pd.to_datetime(full['timestamp']).dt.tz_localize(None)
s3 = boto3.client('s3', region_name='eu-west-1')
objects = s3.list_objects(Bucket='commcare', Prefix='enriched_data.csv')
last_update = str(objects['Contents'][0]['LastModified'].replace(tzinfo=None)) + ' UTC'

# Get districts and provinces
districts = [{'label':i,'value':i} for i in set(full['district'])]
provinces = [{'label':i,'value':i} for i in set(full['province'])]
    
# Define App layout
app.layout = html.Div(children=[
html.H2(children='amfAR | The Foundation for AIDs Research'),

html.H4(children='CommCare Survey Dashboard'),
    
html.Div(children='Filter by district(s):'),

dcc.Dropdown(
    id='district-dropdown',
    options=districts,
    multi=True,
    placeholder='All districts',
    style={'width': '750px'}
),

html.Div([
    html.Div(dcc.Graph(id='fig1'), 
             className="six columns"),

    html.Div(dcc.Graph(id='fig2'),
             className="six columns"),
], className="row"),

html.Div(children='Last updated: ' + last_update)

])

#app.layout = serve_layout

@app.callback([
dash.dependencies.Output('fig1', 'figure'),
dash.dependencies.Output('fig2', 'figure')],
[dash.dependencies.Input('district-dropdown', 'value')])
def update_graph(value):
    if value==None or len(value)==0:
        df = full
        print('all districts')
    else:
        print('filtering by ' + str(value) + ':' + str(type(value)))
        df = full[full['district'].isin(value)]
        
    enough_staff = pd.get_dummies(df['enough_staff'].dropna()).sum()[['always','sometimes','never','dont_know']].fillna(0)
    fig1 = go.Figure(data=[go.Pie(labels=enough_staff.index, values=enough_staff.values, hole=.5, sort=False)])
    fig1.update_traces(hoverinfo='label+percent+value',marker=dict(colors=['lightgreen','lightblue','lightpink','lightgrey']))
    fig1.update_layout(title_text="Are there enough staff in the clinic")
    
    tmp = df[df['wait_time_hrs']>0]
    today = pd.to_datetime(max(df['timestamp']).date())
    avg_wt = tmp['wait_time_hrs'].mean()
    ref = tmp[tmp['timestamp']<today]['wait_time_hrs'].mean()
    wait_time = df[df['wait_time_hrs']>0][['timestamp','wait_time_hrs']].dropna().set_index('timestamp')
    wait_time = wait_time.resample('D').mean().dropna()
    colors = ['lightpink' if x>avg_wt else 'lightgreen' for x in wait_time['wait_time_hrs']]

    fig2 = make_subplots(
    rows=2, cols=1,
    specs=[[{"type": "indicator"}],
           [{"type": "bar"}]])
    
    fig2.add_trace(go.Indicator(
        mode = "number+delta",
        value = avg_wt,
        delta = {'reference': ref, 'decreasing':{'color':'lightgreen'},'increasing':{'color':'lightpink'}},
        title = {'text': "Average Wait Time (hours)"}), row=1, col=1)
    
    fig2.add_trace(go.Bar(x=wait_time.index, y=wait_time['wait_time_hrs'], marker_color=colors),
                    row=2, col=1)
    
    fig2.update_layout(
        yaxis_title="Wait Time (hours)",
        plot_bgcolor = 'rgba(0,0,0,0)')
    
    return fig1, fig2

if __name__ == '__main__':
    application.run(debug=True, host='0.0.0.0', port=8050) # use this for local testing
    #application.run(debug=True, port=8080) # use this for EB deployment


One option would be to implement the filtering on the client side, using either the DataTable component, or using a custom clientside callback.

If the data isn’t that large you could also just resign yourself to sending the data back up to the server to perform the filtering each time.