1

I'm creating a fitness chart using Plotly Dash that allows a user to enter a weight, which saves the data to an excel file, and then the user can refresh the screen to update the graph. I've been able to do them seperately by only having one function under the app.callback section. How can I have both functions? I can make the graph OR I can collect the input and refresh, but not both. Here's a sample of the data I'm using.

enter image description here

And here's the MVP code I'm trying to use.

import openpyxl
import dash
from dash import html, dcc, Input, Output, State
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Input(id='weight', placeholder='Enter Your Weight', type='text'),
    html.Button(id='submit-button', type='submit', children='Submit'),
    html.A(html.Button('Refresh'), href='/'),
    dcc.Graph(
        id='chart')

])


@app.callback(Output('chart', 'figure'),
              [Input('submit-button', 'n_clicks')],
              [State('weight', 'value')],
              )
def display_time_series(n_clicks, input_value):
    xlsx = pd.read_excel('Weight Tracker.xlsx')
    df = xlsx
    fig = px.line(df, x="DATE", y="ACTUAL WEIGHT")

    fig.add_trace(
        go.Scatter(x=df['DATE'], y=df['HIGH ESTIMATE'], name="HIGH ESTIMATE", line=dict(color="green", dash="dash")),
        secondary_y=False,
    )

    fig.add_trace(
        go.Scatter(x=df['DATE'], y=df['LOW ESTIMATE'], name="LOW ESTIMATE", line=dict(color="red", dash="dash")),
        secondary_y=False,
    )

    if n_clicks is not None:

        wb = openpyxl.load_workbook('Weight Tracker.xlsx')

        sheet = wb.active

        # Finds the last open row in Column B or the 'Actual Weight' Column
        last_empty_entry = max((b.row for b in sheet['B'] if b.value is not None)) + 1

        c1 = sheet.cell(row=last_empty_entry, column=2)
        c1.value = int(input_value)

        wb.save("Weight Tracker.xlsx")
        print("Excel has been saved.")

        return fig


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

Here's the error I'm getting and the graph doesn't display and the input button doesn't do anything.

Cannot read properties of null (reading 'data')
 at f.value (http://127.0.0.1:8050/_dash-component-suites/dash/dcc/async-graph.js:1:4493)

    at f.value (http://127.0.0.1:8050/_dash-component-suites/dash/dcc/async-graph.js:1:9778)

    at callComponentWillReceiveProps (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:13111:16)

    at updateClassInstance (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:13313:9)

    at updateClassComponent (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:17242:22)

    at beginWork (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:18755:18)

    at HTMLUnknownElement.callCallback (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:182:16)

    at Object.invokeGuardedCallbackDev (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:231:18)

    at invokeGuardedCallback (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:286:33)

    at beginWork$1 (http://127.0.0.1:8050/_dash-component-suites/dash/deps/[email protected]_3_1m1648990364.14.0.js:23338:9)
1
  • i can't really help with the error but some advice i would give is to not read in an excel document everytime. optimally you would just have a global dataframe of some sorts that handles the data and you just have an extra function if the user wants to export their data. at the very least i would use .csv files since they are way faster to read and write :) Commented Nov 8, 2022 at 19:32

1 Answer 1

1

The main issue you're having is the callback is being called at initial start of program, so to fix this pass in prevent_initial_callbacks=True into dash app instance.

Then you need 2 separate inputs for each button and don't use an anchor for Refresh button it won't work.

import dash
from dash import html, dcc, Input, Output, State
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import datetime as dt


app = dash.Dash(__name__, prevent_initial_callbacks=True)

app.layout = html.Div([
    dcc.Input(id='weight', placeholder='Enter Your Weight', type='text'),
    html.Button(id='submit-button', type='submit', children='Submit'),
    html.Button('Refresh', id='refresh'),
    dcc.Graph(id='chart'),
    html.P(children='dummy', id='dummy', hidden=True)

])


@app.callback(Output('chart', 'figure'),
              [Input('refresh', 'n_clicks')],
              prevent_initial_callback=True,
              )
def display_time_series(n_clicks):
    if n_clicks is not None:
        xlsx = pd.read_excel('Weight Tracker.xlsx')
        df = xlsx
        fig = px.line(df, x="DATE", y="ACTUAL WEIGHT")

        fig.add_trace(
            go.Scatter(x=df['DATE'], y=df['HIGH ESTIMATE'], name="HIGH ESTIMATE", line=dict(color="green", dash="dash")),
            secondary_y=False,
        )

        fig.add_trace(
            go.Scatter(x=df['DATE'], y=df['LOW ESTIMATE'], name="LOW ESTIMATE", line=dict(color="red", dash="dash")),
            secondary_y=False,
        )
        return fig


@app.callback(Output('dummy', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('weight', 'value')],
              prevent_initial_callback=True
              )
def save_new_entry(n_clicks, input_value):
    if n_clicks is not None:

        wb = openpyxl.load_workbook('Weight Tracker.xlsx')

        sheet = wb.active

        # Finds the last open row in Column B or the 'Actual Weight' Column
        last_empty_entry = max((b.row for b in sheet['B'] if b.value is not None)) + 1

        c0 = sheet.cell(row=last_empty_entry, column=1)
        c0.value = dt.datetime.now()

        c1 = sheet.cell(row=last_empty_entry, column=2)
        c1.value = int(input_value)

        wb.save("Weight Tracker.xlsx")
        print("Excel has been saved.")



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

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much. I didn't know you could have multiple callbacks like that and I haven't ever seen "prevent_initial_callbacks=True" used in projects before. Again, thank you much!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.