Search code examples
python-3.xplotlyplotly-dash

Plotly-Dash, Python, loading csv files and plotting results using plotly-Dash board for comparison


this is more likely the first time to use Dash board together with plotly and dont have clear understanding how the code works and where the issues come from. It will be appreciated if you give some suggestion. I am trying to import two csv files by using uploading buttons and dropdownbar should show all variable name and once i select multiple variable names from dopdown bar then they should be plotted in the x-y plot.

The code works to import two different csv files and showing variables in dropdownbar, but plotting doesnt work. I think the code is not able to transfer value correct to plotly functions. Can you review the code and where the source of problems are ? Thanks.

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

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
# Initialize the app
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

# Define the layout
app.layout = html.Div([
    html.H1('CSV File Comparison Dashboard'),
    html.Div([
        html.H2('File A'),
        dcc.Upload(
            id='upload-data-A',
            children=html.Div([
                'Drag and Drop or ',
                html.A('Select Files')
            ]),
            style={
                'width': '50%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center',
                'margin': '10px'
            },
            multiple=False
        ),
        html.Div(id='file-name_A'),
        dcc.Dropdown(
            id='variable-dropdown-A',
            options=[],
            value=[],
            multi=True
        ),
        dcc.Graph(
            id='graph-A'
        )
    ], style={'width': '49%', 'display': 'inline-block', 'vertical-align': 'top'}),
    html.Div([
        html.H2('File B'),
        dcc.Upload(
            id='upload-data-B',
            children=html.Div([
                'Drag and Drop or ',
                html.A('Select Files')
            ]),
            style={
                'width': '50%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center',
                'margin': '10px'
            },
            multiple=False
        ),
        html.Div(id='file-name_B'),
        dcc.Dropdown(
            id='variable-dropdown-B',
            options=[],
            value=[],
            multi=True
        ),
        dcc.Graph(
            id='graph-B'
        )
    ], style={'width': '49%', 'display': 'inline-block', 'vertical-align': 'top'})
])

# Define the callback for updating the variable dropdown menus
@app.callback([Output('variable-dropdown-A', 'options'), Output('variable-dropdown-A', 'value')],
              [Input('upload-data-A', 'contents')],
              prevent_initial_call=True)

def update_dropdowns_A(list_of_contents_A):
    # Initialize empty options for each dropdown
    options_A = []

    # Check if a csv file has been uploaded for file A
    if list_of_contents_A:
        # Read the csv file
        content_type, content_string = list_of_contents_A.split(',')
        decoded = base64.b64decode(content_string)
        df_A = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
        

        # Add the column names to the options for file A
        options_A = [{"label": col, "value": col} for col in df_A.columns]
        return options_A, df_A.columns[0]
    else:
        # Return the options for both dropdowns
        return [], None

@app.callback([Output('variable-dropdown-B', 'options'), Output('variable-dropdown-B', 'value')],
              [Input('upload-data-B', 'contents')],
              prevent_initial_call=True)

def update_dropdowns_B(list_of_contents_B):
    # Initialize empty options for each dropdown

    options_B = []

    # Check if a csv file has been uploaded for file B
    if list_of_contents_B:
        # Read the csv file
        content_type, content_string = list_of_contents_B.split(',')
        decoded = base64.b64decode(content_string)
        df_B = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
        

        # Add the column names to the options for file A
        options_B = [{"label": col, "value": col} for col in df_B.columns]
        return options_B, df_B.columns[0]
    else:
        # Return the options for both dropdowns
        return [], None




# Define the callback for updating the graphs
@app.callback([Output('graph-A', 'figure'), Output('graph-B', 'figure')],
              [Input('variable-dropdown-A', 'value'), Input('variable-dropdown-B', 'value')],
              [State('upload-data-A', 'contents'), State('upload-data-B', 'contents')],
              prevent_initial_call=True)
def update_graphs(variables_A, variables_B, contents_A, contents_B):
    
    if not variables_A or not variables_B:
        return {}, {}
    # # Initialize empty dataframes for each file
    df_A = pd.DataFrame()
    df_B = pd.DataFrame()

    # # Check if a csv file has been uploaded for file A
    # if contents_A is not None:
    #     # Read the csv file
    #     content_type, content_string = contents_A.split(',')
    #     decoded = base64.b64decode(content_string)
    #     df_A = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
    
    # # Check if a csv file has been uploaded for file B
    # if contents_B is not None:
    #     # Read the csv file
    #     content_type, content_string = contents_B.split(',')
    #     decoded = base64.b64decode(content_string)
    #     df_B = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
    
    # Initialize empty figures for each graph
    fig_A = go.Figure()
    fig_B = go.Figure()

    # Check if any variables have been selected for file A

    for var in variables_A:
        fig_A.add_trace(go.Scatter(
            x=df_A['time'],
            y=df_A[var],
            mode='lines',
            name=var
        ))

    fig_A.update_layout(title='Time Series Plot',
                      xaxis_title='Time',
                      yaxis_title='Value')

    # Check if any variables have been selected for file B

    for var in variables_B:
        fig_B.add_trace(go.Scatter(
            x=df_B['time'],
            y=df_B[var],
            mode='lines',
            name=var
        ))

    fig_B.update_layout(title='Time Series Plot',
                      xaxis_title='Time',
                      yaxis_title='Value')

    # Return the figures for both graphs
    return fig_A, fig_B

# Update file name
@app.callback(Output('file-name_A', 'children'),
              [Input('upload-data-A', 'filename')])
def update_file_name(filename):
    if filename is not None:
        return html.P(f'File loaded: {filename}')
    else:
        return html.P('No file loaded')
    
@app.callback(Output('file-name_B', 'children'),
              [Input('upload-data-B', 'filename')])
def update_file_name(filename):
    if filename is not None:
        return html.P(f'File loaded: {filename}')
    else:
        return html.P('No file loaded')


# Run the app
if __name__ == '__main__':
    app.run_server(debug=True, port = 8000)

Explanation on the issue and furtuer comments on what to do in case if i need to extend the number of plots using subplot functions.


Solution

  • There are a lot of things that we could discuss about your code, but just focusing on your issue I have made these modifications.

    import dash
    import dash_core_components as dcc
    import dash_html_components as html
    import pandas as pd
    import plotly.graph_objs as go
    from dash.dependencies import Input, Output, State
    import base64
    import io
    import plotly.express as px
    import dash_table
    
    external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
    # Initialize the app
    app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
    
    # Define the layout
    app.layout = html.Div(
        [
            dcc.Store(id="a"),
            dcc.Store(id="b"),
            html.H1("CSV File Comparison Dashboard"),
            html.Div(
                [
                    html.H2("File A"),
                    dcc.Upload(
                        id="upload-data-A",
                        children=html.Div(["Drag and Drop or ", html.A("Select Files")]),
                        style={
                            "width": "50%",
                            "height": "60px",
                            "lineHeight": "60px",
                            "borderWidth": "1px",
                            "borderStyle": "dashed",
                            "borderRadius": "5px",
                            "textAlign": "center",
                            "margin": "10px",
                        },
                        multiple=False,
                    ),
                    html.Div(id="file-name_A"),
                    dcc.Dropdown(
                        id="variable-dropdown-A", options=[], value=[], multi=True
                    ),
                    dcc.Graph(id="graph-A"),
                ],
                style={"width": "49%", "display": "inline-block", "vertical-align": "top"},
            ),
            html.Div(
                [
                    html.H2("File B"),
                    dcc.Upload(
                        id="upload-data-B",
                        children=html.Div(["Drag and Drop or ", html.A("Select Files")]),
                        style={
                            "width": "50%",
                            "height": "60px",
                            "lineHeight": "60px",
                            "borderWidth": "1px",
                            "borderStyle": "dashed",
                            "borderRadius": "5px",
                            "textAlign": "center",
                            "margin": "10px",
                        },
                        multiple=False,
                    ),
                    html.Div(id="file-name_B"),
                    dcc.Dropdown(
                        id="variable-dropdown-B", options=[], value=[], multi=True
                    ),
                    dcc.Graph(id="graph-B"),
                ],
                style={"width": "49%", "display": "inline-block", "vertical-align": "top"},
            ),
        ]
    )
    
    
    # Define the callback for updating the variable dropdown menus
    @app.callback(
        [
            Output("variable-dropdown-A", "options"),
            Output("variable-dropdown-A", "value"),
            Output("a", "data"),
        ],
        [Input("upload-data-A", "contents")],
        prevent_initial_call=True,
    )
    def update_dropdowns_A(list_of_contents_A):
        # Check if a csv file has been uploaded for file A
        if list_of_contents_A:
            # Read the csv file
            content_type, content_string = list_of_contents_A.split(",")
            decoded = base64.b64decode(content_string)
            df_A = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
            print(df_A.head())
    
            # Add the column names to the options for file A
            options_A = [{"label": col, "value": col} for col in df_A.columns]
            return options_A, df_A.columns, df_A.to_json(orient="records")
        else:
            # Return the options for both dropdowns
            return [], None
    
    
    @app.callback(
        [
            Output("variable-dropdown-B", "options"),
            Output("variable-dropdown-B", "value"),
            Output("b", "data"),
        ],
        [Input("upload-data-B", "contents")],
        prevent_initial_call=True,
    )
    def update_dropdowns_B(list_of_contents_B):
        # Check if a csv file has been uploaded for file B
        if list_of_contents_B:
            # Read the csv file
            content_type, content_string = list_of_contents_B.split(",")
            decoded = base64.b64decode(content_string)
            df_B = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
            print(df_B.head())
    
            # Add the column names to the options for file A
            options_B = [{"label": col, "value": col} for col in df_B.columns]
            return options_B, df_B.columns, df_B.to_json(orient="records")
        else:
            # Return the options for both dropdowns
            return [], None
    
    
    # Define the callback for updating the graphs
    @app.callback(
        [Output("graph-A", "figure"), Output("graph-B", "figure")],
        [Input("variable-dropdown-A", "value"), Input("variable-dropdown-B", "value")],
        [State("a", "data"), State("b", "data")],
        prevent_initial_call=True,
    )
    def update_graphs(variables_A, variables_B, contents_A, contents_B):
        if not variables_A or not variables_B:
            return {}, {}
        df_A = pd.read_json(contents_A, orient="records")
        df_B = pd.read_json(contents_B, orient="records")
        # Initialize empty figures for each graph
        fig_A = go.Figure()
        fig_B = go.Figure()
        # Check if any variables have been selected for file A
        for var in variables_A:
            fig_A.add_trace(go.Scatter(x=df_A["time"], y=df_A[var], mode="lines", name=var))
        fig_A.update_layout(
            title="Time Series Plot", xaxis_title="Time", yaxis_title="Value"
        )
        # Check if any variables have been selected for file B
        for var in variables_B:
            fig_B.add_trace(go.Scatter(x=df_B["time"], y=df_B[var], mode="lines", name=var))
        fig_B.update_layout(
            title="Time Series Plot", xaxis_title="Time", yaxis_title="Value"
        )
    
        # Return the figures for both graphs
        return fig_A, fig_B
    
    
    # Update file name
    @app.callback(Output("file-name_A", "children"), [Input("upload-data-A", "filename")])
    def update_file_name(filename):
        if filename is not None:
            return html.P(f"File loaded: {filename}")
        else:
            return html.P("No file loaded")
    
    
    @app.callback(Output("file-name_B", "children"), [Input("upload-data-B", "filename")])
    def update_file_name(filename):
        if filename is not None:
            return html.P(f"File loaded: {filename}")
        else:
            return html.P("No file loaded")
    
    
    # Run the app
    if __name__ == "__main__":
        app.run_server(debug=True, port=8000)
    

    The main issue that you have is that Dash does not store data to local memory when you process the csv files. That means that when your update_dropdowns_x completes the respective df_A and df_B are deleted. When your update_graphs is triggered there is no data for it to work with. I have added two dcc.Store objects to persist the data from the csv between callbacks. You will notice that I did not pass the dataframe because html requires the data to be in text/json. That is why the df_A.to_json(orient="records") was used as the output. The code was then processed with black to correct minor formatting errors. I also set the dropdowns to automatically include all channels, but you can add the [0] back to the return to have the state that your original app was at. Here is a screenshot. since you didn't post an example of the csv files I just made something up.

    enter image description here