Black Lives Matter. Please consider donating to Black Girls Code today.
Dash HoloViews is now available! Check out the docs.

Incorporating uploaded data into a callback

First I made a simple app with fixed data:

import base64
import datetime
import io

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table
import plotly.graph_objs as go
import pandas as pd

external_stylesheets = ['css stylesheet test.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
df = pd.read_csv('myfile.csv')

text = 'Col1'
vals = df.groupby([text]).size()
available_indicators = ['Col1', 'Col2', 'Col3', 'Col4', 'Col5', 'Col6', 'Col7']
status_vals = df['Status'].unique()

app.layout = html.Div([
    html.Div([
        html.H1("Welcome to the Dashboard"),
        html.P("Learning Dash is fun!")
             ], ),
    dcc.Graph(id='donut-with-slider'),
    dcc.Dropdown(
        id='Category',
        options=[{'label': i, 'value': i} for i in available_indicators],
        value='Col1'),
    dcc.Dropdown(
        id='Status',
        options=[{'label': i, 'value': i} for i in status_vals],
        value='Filled'),
    dcc.Slider(
        id='year-slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        value=df['Year'].max(),
        marks={str(Year): str(Year) for Year in df['Year'].unique()},
        step=None
            ),
        ])

@app.callback(
    Output('donut-with-slider', 'figure'),
    Input('year-slider', 'value'),
    Input('Category', 'value'),
    Input('Status', 'value'))
def update_figure(selected_year, Category, Status):
    df2 = df[df.Year == selected_year]
    df3 = df2[df2.Status == Status]
    text = Category
    vals = df3.groupby([text]).size()
    fig = go.Figure(data=[go.Pie(labels=vals.index.values, values=vals, hole=0.3)])

    fig.update_layout(transition_duration=500)
    return fig

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

And all is well. What I’m struggling to do is adapt this so that data can be uploaded using dcc.Upload, and then incorporated into the donut-with-slider plot so it remains interactive. Importantly there is also a lot of pandas legwork that has to go into cleaning up the uploaded data. This is what I have so far that is not working:

#ideally I wouldn't need to start this way, with the last 
#saved data, because uploaded files might have new 
#values, but I can't think of how else to do this
df = pd.read_csv('Most_Recent_Version.csv')

text = 'Col1'
vals = df.groupby([text]).size()
available_indicators = ['Col1', 'Col2', 'Col3', 'Col4', 'Col5', 'Col6', 'Col7']
status_vals = df['Status'].unique()
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'
        },
        multiple=True
    ),
    html.Div([
    html.H1("Welcome to the Dashboard"),
    html.P("Learning Dash is so interesting!!")
         ], ),
    dcc.Graph(id='donut-with-slider'),
    dcc.Dropdown(
        id='Category',
        options=[{'label': i, 'value': i} for i in available_indicators],
        value='Receiving GBU/GF'),
    dcc.Dropdown(
        id='Status',
        options=[{'label': i, 'value': i} for i in status_vals],
        value='Filled'),
    dcc.Slider(
        id='year-slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        value=df['Year'].max(),
        marks={str(Year): str(Year) for Year in df['Year'].unique()},
        step=None),
        html.Div(id='output-data-upload'),    
    ])

def parse_contents(contents, filename):
    if contents is not None:
        content_type, content_string = contents.split(',')
        decoded = base64.b64decode(content_string)
        try:
            if 'csv' in filename:
                #if they upload a csv, the file is already clean
                df = pd.read_excel(io.BytesIO(decoded), header=0) 
                
            elif 'xls' in filename:
                df = pd.read_excel(io.BytesIO(decoded), sheet_name=tabs, header=1, usecols='B:AC') 
                #lines and lines of pandas cleanup
                #save new Most_Recent_Version.csv                
                cleaned.to_csv('Most_Recent_Version.csv', index=None)
                return cleaned

        except Exception as e:
            print(e)
            return html.Div([
                'There was an error processing this file.'
        
            ])   
    
    else:
    return dash.no_update

@app.callback(
    Output('donut-with-slider', 'figure'), 
    [Input('upload-data', 'contents'),
    Input('upload-data', 'filename')],
    Input('Category', 'value'),
    Input('Status', 'value'),
    Input('year-slider', 'value'))
def update_figure(contents, filename, selected_year, Category, Status):
    if contents is not None:
        cleaned = parse_contents(contents, filename)
        #Cant find reason this is necessary but without 
        #the dataframe declaration this comes as a list
        df = pd.DataFrame(cleaned)
    else:
        return dash.no_update
    df2 = df[df.Year == selected_year]
    df3 = df2[df2.Status == Status]
    text = Category
    vals = df3.groupby([text]).size()
    fig = go.Figure(data=[go.Pie(labels=vals.index.values, values=vals, hole=0.3)])
    
    fig.update_layout(transition_duration=500)
    return fig



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

In addition to callback where I commented about the list type, the error it’s throwing lies with content_type, content_string = contents.split(',') in parse_data(): AttributeError: ‘list’ object has no attribute ‘split’

Maybe some of this is connected, why is everything constantly getting converted to a list? What am I missing here? I tried having a second callback to update the output but that didn’t seem to help either.