Update Scatter Graph from Upload

I’m trying to create an app here I can upload a csv file and that returns a scatter graph. On this scatter graph there will be a dropdown indicating No_of_Presyn and No_of_Postsyn, accompanied by Radio buttons to determine whether the x axis is linear of log. Finally, there will also be a slider button indicating the index (time) when this data was created.

So far I have:

import dash_html_components as html
import dash_core_components as dcc
import dash

import plotly
#import dash_table_experiments as dte
from dash.dependencies import Input, Output, State

import pandas as pd
import numpy as np

import json
import datetime
import operator
import os

import base64
import io


node_list = ['137de', '305b5', '8f6ee', '5c549', '948a3', 'a21aa', 'b98b4', 'c765c', '41583', 'a0dfa', '3c281', '36b02']

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

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

app.config['suppress_callback_exceptions'] = True

colors = {
    'background' : '#111111',
    'text' : '#7FDBFF'
}



app.layout = html.Div([

    html.H5('Upload Files'),
    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.Br(),

    dcc.RadioItems(
        id = 'xaxis-type',
        options = [{'label' : i, 'value' : i} for i in ['Linear', 'Log']],
        value = 'Linear',
        labelStyle = {'display' : 'inline-block'}),


    dcc.Dropdown(id = 'yaxis-column',
        options = [{'label' : i, 'value' : i} for i in ['No_of_Presyn', 'No_of_Postsyn']],
        value = 'No_of_Presyn',
        placeholder = 'Filter Column'),

    html.Br(),
    #html.Div(dcc.Graph(id = 'sv_scatter')),
    html.Div(id= 'output-data-upload'),

    dcc.Slider(
        id = 'node_slider',
        min = 0,
        max = 10,
        value = 0,
        marks = {str(i):str(i) for i in range(0,11)},
        step = None
        )

    ])


# Functions

# file upload function
def parse_contents(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 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))
    except Exception as e:
        print(e)
        return None

    return df

# callback figure creation
@app.callback(Output('output-data-upload', ''),
              [Input('upload-data', 'contents'),
               #Input('upload-data', 'filename'),
               Input('xaxis-type', 'value'),
               Input('yaxis-column','value'),
               Input('node_slider', 'value')],
               [State('upload-data', 'filename')])

def update_output(list_of_contents, list_of_names, xaxis_type, yaxis_column, node_index):

    if list_of_contents is not None:

        df = parse_contents(list_of_contents, list_of_names)

        children = [
            parse_contents(c, n) for c, n in
            zip(list_of_contents, list_of_names)]

        if children is not None:

            #print(children)

            dff = df[df.node_index == node_index]

            return{
                'data' : [go.Scatter(
                    x = dff.supervoxel_sizes.values.tolist(),
                    y = dff.No_of_Presyn if 'Presyn' in yaxis_column else dff.No_of_Postsyn,
                    mode = 'markers',
                    marker = {
                        'size' : 15,
                        'opacity' : 0.5,
                        'line' : {'width' : 0.5, 'color' : 'white'}
                    }
                )],
                'layout' : go.Layout(
                    xaxis = {
                        'title' : 'SV_sizes',
                        'type' : 'linear' if xaxis_type == 'Linear' else 'log'
                    },
                    yaxis = {
                        'title' : yaxis_column
                    },
                )
            }

        else:
            return [{}]
    else:
        return [{}]



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

This code returns the upload button, dropdown, radio buttons and slider however when I try to upload data, I receive an error:

Traceback (most recent call last):
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/dash/dash.py", line 1152, in dispatch
    response.set_data(self.callback_map[output]['callback'](*args))
  File "/Users/markuswilliampleijzier/anaconda/lib/python3.6/site-packages/dash/dash.py", line 1038, in add_context
    output_value = func(*args, **kwargs)
  File "/Users/markuswilliampleijzier/Desktop/DASH_TIME_MACHINE/test_upload_v3.py", line 122, in update_output
    df = parse_contents(list_of_contents, list_of_names)
  File "/Users/markuswilliampleijzier/Desktop/DASH_TIME_MACHINE/test_upload_v3.py", line 90, in parse_contents
    content_type, content_string = contents.split(',')
AttributeError: 'list' object has no attribute 'split'

Thanks for any help in advance!!!

M

Welcome @markuspleijzier!

You’ve set multiple=True in your Upload component, so list_of_contents is going to be a list, even if it only has one file.

The second time you call parse_content it handles this correctly:
parse_contents(c, n) for c, n in zip(list_of_contents, list_of_names)

but the first time:
df = parse_contents(list_of_contents, list_of_names)
it’s trying to do the whole list at once.

Hi @alexcjohnson,

Thanks for your help! This solved the AttributeError. However I now get an

UnboundLocalError: local variable 'df is referenced before assignment

How can I use the output of the upload in order to create the scatter graph?

I’ve read other forums/ the dash user guide stating how global variables are not welcome in dash, but it isn’t clear to me how I can circumvent this unbound local error.

In previous versions I tried having two separate callback versions however dash informed me that it would be better to have these callbacks and functions in one group.

If that first form (df = parse_contents...) is really what you want - ie you only want to accept a single file and turn that into a DataFrame - then just set multiple=False and get rid of all the children stuff. You’ll still probably need a fallback like if not df: raise PreventUpdate for cases like uploading an unsupported file or changing the other inputs before uploading anything.

It also looks like the output you’re building from your callback is a dcc.Graph figure - so make sure it’s connected to Output('some-graph', 'figure')

I’ve removed the children stuff and changed the multiple variable, however I still get the UnboundLocalError.

Here is my updated code:


import dash_html_components as html
import dash_core_components as dcc
import dash

import plotly
#import dash_table_experiments as dte
from dash.dependencies import Input, Output, State

import pandas as pd
import numpy as np

import json
import datetime
import operator
import os

import base64
import io


node_list = ['137de', '305b5', '8f6ee', '5c549', '948a3', 'a21aa', 'b98b4', 'c765c', '41583', 'a0dfa', '3c281', '36b02']

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

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

app.config['suppress_callback_exceptions'] = True

colors = {
    'background' : '#111111',
    'text' : '#7FDBFF'
}



app.layout = html.Div([

    html.H5('Upload Files'),
    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 = False),

    html.Br(),

    dcc.RadioItems(
        id = 'xaxis-type',
        options = [{'label' : i, 'value' : i} for i in ['Linear', 'Log']],
        value = 'Linear',
        labelStyle = {'display' : 'inline-block'}),


    dcc.Dropdown(id = 'yaxis-column',
        options = [{'label' : i, 'value' : i} for i in ['No_of_Presyn', 'No_of_Postsyn']],
        value = 'No_of_Presyn',
        placeholder = 'Filter Column'),

    html.Br(),
    #html.Div(dcc.Graph(id = 'sv_scatter')),
    html.Div(id= 'output-data-upload', children = [dcc.Graph(id = 'figure')]),

    dcc.Slider(
        id = 'node_slider',
        min = 0,
        max = 10,
        value = 0,
        marks = {str(i):str(i) for i in range(0,11)},
        step = None
        )

    ])


# Functions

# file upload function
def parse_contents(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 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))
    except Exception as e:
        print(e)
        return None

    return df

# callback figure creation
@app.callback(Output('output-data-upload', 'figure'),
              [Input('upload-data', 'contents'),
               Input('xaxis-type', 'value'),
               Input('yaxis-column','value'),
               Input('node_slider', 'value')],
               [State('upload-data', 'filename')])


def update_output(contents, filename, xaxis_type, yaxis_column, node_index):

    if contents is not None:

        df = parse_contents(contents, filename)


        dff = df[df.node_index == node_index]

        return{
            'data' : [go.Scatter(
                x = dff.supervoxel_sizes.values.tolist(),
                y = dff.No_of_Presyn if 'Presyn' in yaxis_column else dff.No_of_Postsyn,
                mode = 'markers',
                marker = {
                    'size' : 15,
                    'opacity' : 0.5,
                    'line' : {'width' : 0.5, 'color' : 'white'}
                }
            )],
            'layout' : go.Layout(
                xaxis = {
                    'title' : 'SV_sizes',
                    'type' : 'linear' if xaxis_type == 'Linear' else 'log'
                },
                yaxis = {
                    'title' : yaxis_column
                },                
            )
        }

    else:
        return [{}]



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

Looks to me as though you need a fallback for if neither csv nor xls is in the filename (else: return None) and then after df = parse_contents you’ll want to handle the case of failed parsing (if not df: return {}).

Now to the question of why parsing is failing when you presumably uploaded a valid file: make sure the order of arguments to your callback matches the inputs, then state, in the @app.callback decorator. Looks like you’ve got the filename in the last spot in the decorator, but the second spot in the argument list.

I’ve changed the order of arguments in the callback function.

The code doesn’t return any more errors now (THANK YOU SO MUCH), but the script also does not display a graph after I upload a file. It does not return any fallback string either - its just blank.

Here is my code:

import dash_html_components as html
import dash_core_components as dcc
import dash

import plotly
import plotly.graph_objs as go
#import dash_table_experiments as dte
from dash.dependencies import Input, Output, State

import pandas as pd
import numpy as np

import json
import datetime
import operator
import os

import base64
import io


node_list = ['137de', '305b5', '8f6ee', '5c549', '948a3', 'a21aa', 'b98b4', 'c765c', '41583', 'a0dfa', '3c281', '36b02']

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

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

app.config['suppress_callback_exceptions'] = True

colors = {
    'background' : '#111111',
    'text' : '#7FDBFF'
}



app.layout = html.Div([

    html.H5('Upload Files'),
    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 = False),

    html.Br(),

    dcc.RadioItems(
        id = 'xaxis-type',
        options = [{'label' : i, 'value' : i} for i in ['Linear', 'Log']],
        value = 'Linear',
        labelStyle = {'display' : 'inline-block'}),


    dcc.Dropdown(id = 'yaxis-column',
        options = [{'label' : i, 'value' : i} for i in ['No_of_Presyn', 'No_of_Postsyn']],
        value = 'No_of_Presyn',),
        #placeholder = 'Filter Column'),

    html.Br(),
    #html.Div(dcc.Graph(id = 'sv_scatter')),
    html.Div(id= 'output-data-upload'),#, children = [dcc.Graph(id = 'figure')]),

    dcc.Slider(
        id = 'node_slider',
        min = 0,
        max = 10,
        value = 0,
        marks = {str(i):str(i) for i in range(0,11)},
        step = None
        )

    ])


# Functions

# file upload function
def parse_contents(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 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))
    except Exception as e:
        print(e)
        return None

    return df

# callback figure creation
@app.callback(Output('output-data-upload', 'figure'),
              [Input('upload-data', 'contents'),
               Input('xaxis-type', 'value'),
               Input('yaxis-column','value'),
               Input('node_slider', 'value')],
               [State('upload-data', 'filename')])


def update_output(contents, xaxis_type, yaxis_column, node_index, filename):


    if contents is not None:

        df = parse_contents(contents, filename)

        if df is None:

            return(print('blah'))

        else:


            dff = df[df.node_index == node_index].copy()

            return{
                'data' : [go.Scatter(
                    x = dff.supervoxel_sizes.values.tolist(),
                    y = dff.No_of_Presyn if 'Presyn' in yaxis_column else dff.No_of_Postsyn,
                    mode = 'markers',
                    marker = {
                        'size' : 15,
                        'opacity' : 0.5,
                        'line' : {'width' : 0.5, 'color' : 'white'}
                    }
                )],
                'layout' : go.Layout(
                    xaxis = {
                        'title' : 'SV_sizes',
                        'type' : 'linear' if xaxis_type == 'Linear' else 'log'
                    },
                    yaxis = {
                        'title' : yaxis_column
                    },                 
                )
            }
    else:
        return(print('double blah'))



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

Oh interesting - I thought the dev tools would have reported the error to you here but apparently not. You’ll see it though if you remove the line:

app.config['suppress_callback_exceptions'] = True

which as of Dash 1.0 can be a kwarg:app = dash.Dash(suppress_callback_exceptions=True)
but that’s just an aside - you don’t need that setting because your whole layout is present from the beginning, you’re just changing props. You only need that if you’re adding components that are themselves connected to callbacks.

Remove that and you should see:

dash.exceptions.NonExistentPropException: 
Attempting to assign a callback with
the property "figure" but the component
"output-data-upload" doesn't have "figure" as a property.

Solution: turn output-data-upload into a dcc.Graph, or put back the dcc.Graph(id = 'figure') and use Output('figure', 'figure')

Hopefully that’s the last piece of the puzzle… either way, we’re getting there!

(and I’ve made an issue to improve error reporting in this case https://github.com/plotly/dash/issues/794)

I’ve unhashed the children = dcc.Graph(id = 'figure')) and changed the callback to Output('figure', 'figure') but still no scatter plot is produced :frowning: .

I’ve added a print(df.head()) one line after the df = parse_contents(contents, filename) and this prints the head of the DataFrame. Doing this for the dff DataFrame also works and prints the correct subsetted DF.

In other words the return{ ... } statement with the data and layout of the output graph is not being read for some reason.

Changing this return statement so that it returns a html.Div([dcc.Graph()]) also does not produce a plot.

Hmph, you’ve got me stumped - did you also remove the suppress_callback_exceptions=True line? I can’t see anything else obviously amiss and it bothers me that it’s still failing silently, even with debug=True. I’d be happy to try this out locally if you’re able to send me a data file - I don’t think we can do that on this site but you can send it to alex@plot.ly

Hi @alexcjohnson,

I was playing around with code last night and I got the figure to plot! Not exactly sure what I changed.

Here is the final code anyways.

Thanks for your help!

import dash_html_components as html
import dash_core_components as dcc
import dash

import plotly
import plotly.graph_objs as go
#import dash_table_experiments as dte
from dash.dependencies import Input, Output, State

import pandas as pd
import numpy as np

import json
import datetime
import operator
import os

import base64
import io


node_list = ['137de', '305b5', '8f6ee', '5c549', '948a3', 'a21aa', 'b98b4', 'c765c', '41583', 'a0dfa', '3c281', '36b02']

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

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

#app.config['suppress_callback_exceptions'] = True

colors = {
    'background' : '#111111',
    'text' : '#7FDBFF'
}



app.layout = html.Div([

    html.H5('Upload Files'),
    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 = False),

    html.Br(),

    dcc.RadioItems(
        id = 'xaxis-type',
        options = [{'label' : i, 'value' : i} for i in ['Linear', 'Log']],
        value = 'Linear',
        labelStyle = {'display' : 'inline-block'}),


    dcc.Dropdown(id = 'yaxis-column',
        options = [{'label' : i, 'value' : i} for i in ['No_of_Presyn', 'No_of_Postsyn']],
        value = 'No_of_Presyn',),
        #placeholder = 'Filter Column'),

    html.Br(),
    #html.Div(dcc.Graph(id = 'sv_scatter')),
    #html.Div(id = 'output-data-upload'), # children = dcc.Graph(id = 'figure')),
    #dcc.Graph(id='figure', figure = {}),
    dcc.Graph(id = 'output-data-upload'),

    dcc.Slider(
        id = 'node_slider',
        min = 0,
        max = 10,
        value = 0,
        marks = {str(i):str(i) for i in range(0,11)},
        step = None
        )

    ])


# Functions

# file upload function
def parse_contents(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 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))
    except Exception as e:
        print(e)
        return None

    return df

# callback figure creation
@app.callback(Output('output-data-upload', 'figure'),
              [Input('upload-data', 'contents'),
               Input('xaxis-type', 'value'),
               Input('yaxis-column','value'),
               Input('node_slider', 'value')],
               [State('upload-data', 'filename')])


def update_output(contents, xaxis_type, yaxis_column, node_index, filename):

    #df = parse_contents(contents, filename)

    if contents is not None:

        df = parse_contents(contents, filename)

        print(df.head())


        dff = df[df.node_index == node_index]

        return{
            'data' : [go.Scatter(
                x = dff.supervoxel_sizes.values.tolist(),
                y = dff.No_of_Presyn if 'Presyn' in yaxis_column else dff.No_of_Postsyn,
                mode = 'markers',
                marker = {
                    'size' : 15,
                    'opacity' : 0.5,
                    'line' : {'width' : 0.5, 'color' : 'white'}
                }
            )],
            'layout' : go.Layout(
                xaxis = {
                    'title' : 'SV_sizes',
                    'type' : 'linear' if xaxis_type == 'Linear' else 'log'
                },
                yaxis = {
                    'title' : yaxis_column
                },                 
            )
        }
    else:
        return{}



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

Best,

M

1 Like