diff --git a/src/app.py b/src/app.py index 78246fc..af20c1f 100644 --- a/src/app.py +++ b/src/app.py @@ -1,510 +1,502 @@ -# %% -import os -import base64 -import logging -import requests -import dash, requests -from dash import dcc -from dash import html, dash_table -from dash.dependencies import Output, State, Input -import pandas as pd -import numpy as np -import plotly.express as px -from datetime import datetime, timedelta -import dash_dangerously_set_inner_html -from urllib.parse import parse_qs, urlparse - - -# %% - -log = logging.getLogger(__name__) -for variable in ['CLIENT_ID','CLIENT_SECRET','REDIRECT_URL'] : - if variable not in os.environ.keys() : - log.error(f'Missing required environment variable \'{variable}\', please review the README') - exit(1) - -app = dash.Dash(__name__) -app.title = "Fitbit Wellness Report" -server = app.server - -app.layout = html.Div(children=[ - dcc.ConfirmDialog( - id='errordialog', - message='Invalid Access Token : Unable to fetch data', - ), - html.Div(id="input-area", className="hidden-print", - style={ - 'display': 'flex', - 'align-items': 'center', - 'justify-content': 'center', - 'gap': '20px', - 'margin': 'auto', - 'flex-wrap': 'wrap', - 'margin-top': '30px' - },children=[ - dcc.DatePickerRange( - id='my-date-picker-range', - display_format='MMMM DD, Y', - minimum_nights=40, - max_date_allowed=datetime.today().date() - timedelta(days=1), - min_date_allowed=datetime.today().date() - timedelta(days=1000), - end_date=datetime.today().date() - timedelta(days=1), - start_date=datetime.today().date() - timedelta(days=365) - ), - html.Button(id='submit-button', type='submit', children='Submit', n_clicks=0, className="button-primary"), - html.Button("Login to FitBit", id="login-button"), - ]), - dcc.Location(id="location"), - dcc.Store(id="oauth-token", storage_type='session'), # Store OAuth token in session storage - html.Div(id="instruction-area", className="hidden-print", style={'margin-top':'30px', 'margin-right':'auto', 'margin-left':'auto','text-align':'center'}, children=[ - html.P( "Select a date range to generate a report.", style={'font-size':'17px', 'font-weight': 'bold', 'color':'#54565e'}), - ]), - html.Div(id='loading-div', style={'margin-top': '40px'}, children=[ - dcc.Loading( - id="loading-progress", - type="default", - children=html.Div(id="loading-output-1") - ), - ]), - - html.Div(id='output_div', style={'max-width': '1400px', 'margin': 'auto'}, children=[ - - html.Div(id='report-title-div', - style={ - 'display': 'flex', - 'align-items': 'center', - 'justify-content': 'center', - 'flex-direction': 'column', - 'margin-top': '20px'}, children=[ - html.H2(id="report-title", style={'font-weight': 'bold'}), - html.H4(id="date-range-title", style={'font-weight': 'bold'}), - html.P(id="generated-on-title", style={'font-weight': 'bold', 'font-size': '16'}) - ]), - html.Div(style={"height": '40px'}), - html.H4("Resting Heart Rate 💖", style={'font-weight': 'bold'}), - html.H6("Resting heart rate (RHR) is derived from a person's average sleeping heart rate. Fitbit tracks heart rate with photoplethysmography. This technique uses sensors and green light to detect blood volume when the heart beats. If a Fitbit device isn't worn during sleep, RHR is derived from daytime sedentary heart rate. According to the American Heart Association, a normal RHR is between 60-100 beats per minute (bpm), but this can vary based upon your age or fitness level."), - dcc.Graph( - id='graph_RHR', - figure=px.line(), - config= {'displaylogo': False} - ), - html.Div(id='RHR_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.H4("Steps Count 👣", style={'font-weight': 'bold'}), - html.H6("Fitbit devices use an accelerometer to track steps. Some devices track active minutes, which includes activities over 3 metabolic equivalents (METs), such as brisk walking and cardio workouts."), - dcc.Graph( - id='graph_steps', - figure=px.bar(), - config= {'displaylogo': False} - ), - dcc.Graph( - id='graph_steps_heatmap', - figure=px.bar(), - config= {'displaylogo': False} - ), - html.Div(id='steps_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.H4("Activity 🏃‍♂️", style={'font-weight': 'bold'}), - html.H6("Heart Rate Zones (fat burn, cardio and peak) are based on a percentage of maximum heart rate. Maximum heart rate is calculated as 220 minus age. The Centers for Disease Control recommends that adults do at least 150-300 minutes of moderate-intensity aerobic activity each week or 75-150 minutes of vigorous-intensity aerobic activity each week."), - dcc.Graph( - id='graph_activity_minutes', - figure=px.bar(), - config= {'displaylogo': False} - ), - html.Div(id='fat_burn_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(id='cardio_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(id='peak_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.H4("Weight Log ⏲️", style={'font-weight': 'bold'}), - html.H6("Fitbit connects with the Aria family of smart scales to track weight. Weight may also be self-reported using the Fitbit app. Studies suggest that regular weigh-ins may help people who want to lose weight."), - dcc.Graph( - id='graph_weight', - figure=px.line(), - config= {'displaylogo': False} - ), - html.Div(id='weight_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.H4("SpO2 🩸", style={'font-weight': 'bold'}), - html.H6("A pulse oximeter reading indicates what percentage of your blood is saturated, known as the SpO2 level. A typical, healthy reading is 95–100% . If your SpO2 level is less than 92%, a doctor may recommend you get an ABG. A pulse ox is the most common type of test because it's noninvasive and provides quick readings."), - dcc.Graph( - id='graph_spo2', - figure=px.line(), - config= {'displaylogo': False} - ), - html.Div(id='spo2_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.H4("Sleep 💤", style={'font-weight': 'bold'}), - html.H6("Fitbit estimates sleep stages (awake, REM, light sleep and deep sleep) and sleep duration based on a person's movement and heart-rate patterns. The National Sleep Foundation recommends 7-9 hours of sleep per night for adults"), - dcc.Checklist(options=[{'label': 'Color Code Sleep Stages', 'value': 'Color Code Sleep Stages','disabled':True}], value=['Color Code Sleep Stages'], style={'max-width': '1330px', 'margin': 'auto'}, inline=True, id="sleep-stage-checkbox", className="hidden-print"), - dcc.Graph( - id='graph_sleep', - figure=px.bar(), - config= {'displaylogo': False} - ), - dcc.Graph( - id='graph_sleep_regularity', - figure=px.bar(), - config= {'displaylogo': False} - ), - html.Div(id='sleep_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), - html.Div(style={"height": '40px'}), - html.Div(className="hidden-print", style={'margin': 'auto', 'text-align': 'center'}, children=[ - dash_dangerously_set_inner_html.DangerouslySetInnerHTML( ''' -
- - - -
- ''')]), - html.Div(style={"height": '25px'}), - ]), -]) - -@app.callback(Output('location', 'href'),Input('login-button', 'n_clicks')) -def authorize(n_clicks): - """Authorize the application""" - if n_clicks : - client_id = os.environ['CLIENT_ID'] - redirect_uri = os.environ['REDIRECT_URL'] - scope = 'profile activity cardio_fitness heartrate sleep weight oxygen_saturation respiratory_rate' - auth_url = f'https://www.fitbit.com/oauth2/authorize?scope={scope}&client_id={client_id}&response_type=code&prompt=none&redirect_uri={redirect_uri}' - return auth_url - return dash.no_update - -@app.callback(Output('oauth-token', 'data'),Input('location', 'href')) -def handle_oauth_callback(href): - """Process the OAuth callback""" - if href: - # Parse the query string from the URL to extract the 'code' parameter - parsed_url = urlparse(href) - query_params = parse_qs(parsed_url.query) - oauth_code = query_params.get('code', [None])[0] - if oauth_code : - print(f"OAuth code received") - else : - print("No OAuth code found in URL.") - return dash.no_update - # Exchange code for a token - client_id = os.environ['CLIENT_ID'] - client_isecret = os.environ['CLIENT_SECRET'] - redirect_uri = os.environ['REDIRECT_URL'] - token_url='https://api.fitbit.com/oauth2/token?' - payload = {'code': oauth_code, 'grant_type': 'authorization_code', 'client_id': client_id, 'redirect_uri': redirect_uri} - token_creds = base64.b64encode(f"{client_id}:{client_isecret}".encode("utf-8")).decode("utf-8") - token_headers = {"Authorization": f"Basic {token_creds}"} - token_response = requests.post(token_url, data=payload, headers=token_headers) - token_response_json = token_response.json() - access_token = token_response_json.get('access_token') - if access_token : - print(f"Acceess token received!") - return access_token - else : - print("No access token found in response.") - return dash.no_update - -@app.callback(Output('login-button', 'children'),Output('login-button', 'disabled'),Input('oauth-token', 'data')) -def update_login_button(oauth_token): - if oauth_token: - return html.Span("Logged in"), True - else: - return "Login to FitBit", False - - -def seconds_to_tick_label(seconds): - """Calculate the number of hours, minutes, and remaining seconds""" - hours, remainder = divmod(seconds, 3600) - minutes, seconds = divmod(remainder, 60) - mult, remainder = divmod(hours, 12) - if mult >=2: - hours = hours - (12*mult) - result_datetime = datetime(1, 1, 1, hour=hours, minute=minutes, second=seconds) - if result_datetime.hour >= 12: - result_datetime = result_datetime - timedelta(hours=12) - else: - result_datetime = result_datetime + timedelta(hours=12) - return result_datetime.strftime("%H:%M") - -def format_minutes(minutes): - return "%2dh %02dm" % (divmod(minutes, 60)) - -def calculate_table_data(df, measurement_name): - df = df.sort_values(by='Date', ascending=False) - result_data = { - 'Period' : ['30 days', '3 months', '6 months', '1 year'], - 'Average ' + measurement_name : [], - 'Max ' + measurement_name : [], - 'Min ' + measurement_name : [] - } - last_date = df.head(1)['Date'].values[0] - for period in [30, 90, 180, 365]: - end_date = last_date - start_date = end_date - pd.Timedelta(days=period) - - period_data = df[(df['Date'] >= start_date) & (df['Date'] <= end_date)] - - if len(period_data) >= period: - - max_hr = period_data[measurement_name].max() - if measurement_name == "Steps Count": - min_hr = period_data[period_data[measurement_name] != 0][measurement_name].min() - else: - min_hr = period_data[measurement_name].min() - average_hr = round(period_data[measurement_name].mean(),2) - - if measurement_name == "Total Sleep Minutes": - result_data['Average ' + measurement_name].append(format_minutes(average_hr)) - result_data['Max ' + measurement_name].append(format_minutes(max_hr)) - result_data['Min ' + measurement_name].append(format_minutes(min_hr)) - else: - result_data['Average ' + measurement_name].append(average_hr) - result_data['Max ' + measurement_name].append(max_hr) - result_data['Min ' + measurement_name].append(min_hr) - else: - result_data['Average ' + measurement_name].append(pd.NA) - result_data['Max ' + measurement_name].append(pd.NA) - result_data['Min ' + measurement_name].append(pd.NA) - - return pd.DataFrame(result_data) - -# Sleep stages checkbox functionality -@app.callback(Output('graph_sleep', 'figure', allow_duplicate=True), Input('sleep-stage-checkbox', 'value'), State('graph_sleep', 'figure'), prevent_initial_call=True) -def update_sleep_colors(value, fig): - if len(value) == 1: - fig['data'][0]['marker']['color'] = '#084466' - fig['data'][1]['marker']['color'] = '#1e9ad6' - fig['data'][2]['marker']['color'] = '#4cc5da' - fig['data'][3]['marker']['color'] = '#fd7676' - else: - fig['data'][0]['marker']['color'] = '#084466' - fig['data'][1]['marker']['color'] = '#084466' - fig['data'][2]['marker']['color'] = '#084466' - fig['data'][3]['marker']['color'] = '#084466' - return fig - -# Limits the date range to one year max -@app.callback(Output('my-date-picker-range', 'max_date_allowed'), Output('my-date-picker-range', 'end_date'), - [Input('my-date-picker-range', 'start_date')]) -def set_max_date_allowed(start_date): - start = datetime.strptime(start_date, "%Y-%m-%d") - current_date = datetime.today().date() - timedelta(days=1) - max_end_date = min((start + timedelta(days=365)).date(), current_date) - return max_end_date, max_end_date - -# Disables the button after click and starts calculations -@app.callback(Output('errordialog', 'displayed'), Output('submit-button', 'disabled'), Output('my-date-picker-range', 'disabled'), Input('submit-button', 'n_clicks'),State('oauth-token', 'data'),prevent_initial_call=True) -def disable_button_and_calculate(n_clicks, oauth_token): - headers = { - "Authorization": "Bearer " + oauth_token, - "Accept": "application/json" - } - try: - token_response = requests.get("https://api.fitbit.com/1/user/-/profile.json", headers=headers) - token_response.raise_for_status() - except: - return True, False, False - return False, True, True - -# Fetch data and update graphs on click of submit -@app.callback(Output('report-title', 'children'), Output('date-range-title', 'children'), Output('generated-on-title', 'children'), Output('graph_RHR', 'figure'), Output('RHR_table', 'children'), Output('graph_steps', 'figure'), Output('graph_steps_heatmap', 'figure'), Output('steps_table', 'children'), Output('graph_activity_minutes', 'figure'), Output('fat_burn_table', 'children'), Output('cardio_table', 'children'), Output('peak_table', 'children'), Output('graph_weight', 'figure'), Output('weight_table', 'children'), Output('graph_spo2', 'figure'), Output('spo2_table', 'children'), Output('graph_sleep', 'figure'), Output('graph_sleep_regularity', 'figure'), Output('sleep_table', 'children'), Output('sleep-stage-checkbox', 'options'), Output("loading-output-1", "children"), -Input('submit-button', 'disabled'),State('my-date-picker-range', 'start_date'), State('my-date-picker-range', 'end_date'),State('oauth-token', 'data'), -prevent_initial_call=True) -def update_output(n_clicks, start_date, end_date, oauth_token): - - start_date = datetime.fromisoformat(start_date).strftime("%Y-%m-%d") - end_date = datetime.fromisoformat(end_date).strftime("%Y-%m-%d") - - headers = { - "Authorization": "Bearer " + oauth_token, - "Accept": "application/json" - } - - # Collecting data----------------------------------------------------------------------------------------------------------------------- - - user_profile = requests.get("https://api.fitbit.com/1/user/-/profile.json", headers=headers).json() - response_heartrate = requests.get("https://api.fitbit.com/1/user/-/activities/heart/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() - response_steps = requests.get("https://api.fitbit.com/1/user/-/activities/steps/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() - response_weight = requests.get("https://api.fitbit.com/1/user/-/body/weight/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() - response_spo2 = requests.get("https://api.fitbit.com/1/user/-/spo2/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() - - # Processing data----------------------------------------------------------------------------------------------------------------------- - days_name_list = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday') - report_title = "Wellness Report - " + user_profile["user"]["firstName"] + " " + user_profile["user"]["lastName"] - report_dates_range = datetime.fromisoformat(start_date).strftime("%d %B, %Y") + " – " + datetime.fromisoformat(end_date).strftime("%d %B, %Y") - generated_on_date = "Report Generated : " + datetime.today().date().strftime("%d %B, %Y") - dates_list = [] - dates_str_list = [] - rhr_list = [] - steps_list = [] - weight_list = [] - spo2_list = [] - sleep_record_dict = {} - deep_sleep_list, light_sleep_list, rem_sleep_list, awake_list, total_sleep_list, sleep_start_times_list = [],[],[],[],[],[] - fat_burn_minutes_list, cardio_minutes_list, peak_minutes_list = [], [], [] - - for entry in response_heartrate['activities-heart']: - dates_str_list.append(entry['dateTime']) - dates_list.append(datetime.strptime(entry['dateTime'], '%Y-%m-%d')) - try: - fat_burn_minutes_list.append(entry["value"]["heartRateZones"][1]["minutes"]) - cardio_minutes_list.append(entry["value"]["heartRateZones"][2]["minutes"]) - peak_minutes_list.append(entry["value"]["heartRateZones"][3]["minutes"]) - except KeyError as E: - fat_burn_minutes_list.append(None) - cardio_minutes_list.append(None) - peak_minutes_list.append(None) - if 'restingHeartRate' in entry['value']: - rhr_list.append(entry['value']['restingHeartRate']) - else: - rhr_list.append(None) - - for entry in response_steps['activities-steps']: - if int(entry['value']) == 0: - steps_list.append(None) - else: - steps_list.append(int(entry['value'])) - - for entry in response_weight["body-weight"]: - weight_list.append(float(entry['value'])) - - for entry in response_spo2: - spo2_list += [None]*(dates_str_list.index(entry["dateTime"])-len(spo2_list)) - spo2_list.append(entry["value"]["avg"]) - spo2_list += [None]*(len(dates_str_list)-len(spo2_list)) - - for i in range(0,len(dates_str_list),100): - end_index = i+100 - if i+100 > len(dates_str_list): - end_index = len(dates_str_list) - temp_start_date = dates_str_list[i] - temp_end_date = dates_str_list[end_index-1] - - response_sleep = requests.get("https://api.fitbit.com/1.2/user/-/sleep/date/"+ temp_start_date +"/"+ temp_end_date +".json", headers=headers).json() - - for sleep_record in response_sleep["sleep"][::-1]: - if sleep_record['isMainSleep']: - try: - sleep_start_time = datetime.strptime(sleep_record["startTime"], "%Y-%m-%dT%H:%M:%S.%f") - if sleep_start_time.hour < 12: - sleep_start_time = sleep_start_time + timedelta(hours=12) - else: - sleep_start_time = sleep_start_time + timedelta(hours=-12) - sleep_time_of_day = sleep_start_time.time() - sleep_record_dict[sleep_record['dateOfSleep']] = {'deep': sleep_record['levels']['summary']['deep']['minutes'], - 'light': sleep_record['levels']['summary']['light']['minutes'], - 'rem': sleep_record['levels']['summary']['rem']['minutes'], - 'wake': sleep_record['levels']['summary']['wake']['minutes'], - 'total_sleep': sleep_record["minutesAsleep"], - 'start_time_seconds': (sleep_time_of_day.hour * 3600) + (sleep_time_of_day.minute * 60) + sleep_time_of_day.second - } - except KeyError as E: - pass - - for day in dates_str_list: - if day in sleep_record_dict: - deep_sleep_list.append(sleep_record_dict[day]['deep']) - light_sleep_list.append(sleep_record_dict[day]['light']) - rem_sleep_list.append(sleep_record_dict[day]['rem']) - awake_list.append(sleep_record_dict[day]['wake']) - total_sleep_list.append(sleep_record_dict[day]['total_sleep']) - sleep_start_times_list.append(sleep_record_dict[day]['start_time_seconds']) - else: - deep_sleep_list.append(None) - light_sleep_list.append(None) - rem_sleep_list.append(None) - awake_list.append(None) - total_sleep_list.append(None) - sleep_start_times_list.append(None) - - df_merged = pd.DataFrame({ - "Date": dates_list, - "Resting Heart Rate": rhr_list, - "Steps Count": steps_list, - "Fat Burn Minutes": fat_burn_minutes_list, - "Cardio Minutes": cardio_minutes_list, - "Peak Minutes": peak_minutes_list, - "weight": weight_list, - "SPO2": spo2_list, - "Deep Sleep Minutes": deep_sleep_list, - "Light Sleep Minutes": light_sleep_list, - "REM Sleep Minutes": rem_sleep_list, - "Awake Minutes": awake_list, - "Total Sleep Minutes": total_sleep_list, - "Sleep Start Time Seconds": sleep_start_times_list - }) - - df_merged['Total Sleep Seconds'] = df_merged['Total Sleep Minutes']*60 - df_merged["Sleep End Time Seconds"] = df_merged["Sleep Start Time Seconds"] + df_merged['Total Sleep Seconds'] - df_merged["Total Active Minutes"] = df_merged["Fat Burn Minutes"] + df_merged["Cardio Minutes"] + df_merged["Peak Minutes"] - rhr_avg = {'overall': round(df_merged["Resting Heart Rate"].mean(),1), '30d': round(df_merged["Resting Heart Rate"].tail(30).mean(),1)} - steps_avg = {'overall': int(df_merged["Steps Count"].mean()), '30d': int(df_merged["Steps Count"].tail(31).mean())} - weight_avg = {'overall': round(df_merged["weight"].mean(),1), '30d': round(df_merged["weight"].tail(30).mean(),1)} - spo2_avg = {'overall': round(df_merged["SPO2"].mean(),1), '30d': round(df_merged["SPO2"].tail(30).mean(),1)} - sleep_avg = {'overall': round(df_merged["Total Sleep Minutes"].mean(),1), '30d': round(df_merged["Total Sleep Minutes"].tail(30).mean(),1)} - active_mins_avg = {'overall': round(df_merged["Total Active Minutes"].mean(),2), '30d': round(df_merged["Total Active Minutes"].tail(30).mean(),2)} - weekly_steps_array = np.array([0]*days_name_list.index(datetime.fromisoformat(start_date).strftime('%A')) + df_merged["Steps Count"].to_list() + [0]*(6 - days_name_list.index(datetime.fromisoformat(end_date).strftime('%A')))) - weekly_steps_array = np.transpose(weekly_steps_array.reshape((int(len(weekly_steps_array)/7), 7))) - weekly_steps_array = pd.DataFrame(weekly_steps_array, index=days_name_list) - - # Plotting data----------------------------------------------------------------------------------------------------------------------- - - fig_rhr = px.line(df_merged, x="Date", y="Resting Heart Rate", line_shape="spline", color_discrete_sequence=["#d30f1c"], title=f"Daily Resting Heart Rate

Overall average : {rhr_avg['overall']} bpm | Last 30d average : {rhr_avg['30d']} bpm



") - if df_merged["Resting Heart Rate"].dtype != object: - fig_rhr.add_annotation(x=df_merged.iloc[df_merged["Resting Heart Rate"].idxmax()]["Date"], y=df_merged["Resting Heart Rate"].max(), text=str(df_merged["Resting Heart Rate"].max()), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_rhr.add_annotation(x=df_merged.iloc[df_merged["Resting Heart Rate"].idxmin()]["Date"], y=df_merged["Resting Heart Rate"].min(), text=str(df_merged["Resting Heart Rate"].min()), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_rhr.add_hline(y=df_merged["Resting Heart Rate"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["Resting Heart Rate"].mean(), 1)) + " BPM", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - fig_rhr.add_hrect(y0=62, y1=68, fillcolor="green", opacity=0.15, line_width=0) - rhr_summary_df = calculate_table_data(df_merged, "Resting Heart Rate") - rhr_summary_table = dash_table.DataTable(rhr_summary_df.to_dict('records'), [{"name": i, "id": i} for i in rhr_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#5f040a','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_steps = px.bar(df_merged, x="Date", y="Steps Count", color_discrete_sequence=["#2fb376"], title=f"Daily Steps Count

Overall average : {steps_avg['overall']} steps | Last 30d average : {steps_avg['30d']} steps



") - if df_merged["Steps Count"].dtype != object: - fig_steps.add_annotation(x=df_merged.iloc[df_merged["Steps Count"].idxmax()]["Date"], y=df_merged["Steps Count"].max(), text=str(df_merged["Steps Count"].max())+" steps", showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_steps.add_annotation(x=df_merged.iloc[df_merged["Steps Count"].idxmin()]["Date"], y=df_merged["Steps Count"].min(), text=str(df_merged["Steps Count"].min())+" steps", showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_steps.add_hline(y=df_merged["Steps Count"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["Steps Count"].mean(), 1)) + " Steps", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.8, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - fig_steps_heatmap = px.imshow(weekly_steps_array, color_continuous_scale='YLGn', origin='lower', title="Weekly Steps Heatmap", labels={'x':"Week Number", 'y': "Day of the Week"}, height=350, aspect='equal') - fig_steps_heatmap.update_traces(colorbar_orientation='h', selector=dict(type='heatmap')) - steps_summary_df = calculate_table_data(df_merged, "Steps Count") - steps_summary_table = dash_table.DataTable(steps_summary_df.to_dict('records'), [{"name": i, "id": i} for i in steps_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#072f1c','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_activity_minutes = px.bar(df_merged, x="Date", y=["Fat Burn Minutes", "Cardio Minutes", "Peak Minutes"], title=f"Activity Minutes

Overall total active minutes average : {active_mins_avg['overall']} minutes | Last 30d total active minutes average : {active_mins_avg['30d']} minutes



") - fig_activity_minutes.update_layout(yaxis_title='Active Minutes', legend=dict(orientation="h",yanchor="bottom", y=1.02, xanchor="right", x=1, title_text='')) - fat_burn_summary_df = calculate_table_data(df_merged, "Fat Burn Minutes") - fat_burn_summary_table = dash_table.DataTable(fat_burn_summary_df.to_dict('records'), [{"name": i, "id": i} for i in fat_burn_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#636efa','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - cardio_summary_df = calculate_table_data(df_merged, "Cardio Minutes") - cardio_summary_table = dash_table.DataTable(cardio_summary_df.to_dict('records'), [{"name": i, "id": i} for i in cardio_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#ef553b','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - peak_summary_df = calculate_table_data(df_merged, "Peak Minutes") - peak_summary_table = dash_table.DataTable(peak_summary_df.to_dict('records'), [{"name": i, "id": i} for i in peak_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#00cc96','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_weight = px.line(df_merged, x="Date", y="weight", line_shape="spline", color_discrete_sequence=["#6b3908"], title=f"Weight

Overall average : {weight_avg['overall']} Unit | Last 30d average : {weight_avg['30d']} Unit



") - if df_merged["weight"].dtype != object: - fig_weight.add_annotation(x=df_merged.iloc[df_merged["weight"].idxmax()]["Date"], y=df_merged["weight"].max(), text=str(df_merged["weight"].max()), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_weight.add_annotation(x=df_merged.iloc[df_merged["weight"].idxmin()]["Date"], y=df_merged["weight"].min(), text=str(df_merged["weight"].min()), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_weight.add_hline(y=round(df_merged["weight"].mean(),1), line_dash="dot",annotation_text="Average : " + str(round(df_merged["weight"].mean(), 1)) + " Units", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - weight_summary_df = calculate_table_data(df_merged, "weight") - weight_summary_table = dash_table.DataTable(weight_summary_df.to_dict('records'), [{"name": i, "id": i} for i in weight_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#4c3b7d','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_spo2 = px.scatter(df_merged, x="Date", y="SPO2", color_discrete_sequence=["#983faa"], title=f"SPO2 Percentage

Overall average : {spo2_avg['overall']}% | Last 30d average : {spo2_avg['30d']}%



", range_y=(90,100), labels={'SPO2':"SpO2(%)"}) - if df_merged["SPO2"].dtype != object: - fig_spo2.add_annotation(x=df_merged.iloc[df_merged["SPO2"].idxmax()]["Date"], y=df_merged["SPO2"].max(), text=str(df_merged["SPO2"].max())+"%", showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_spo2.add_annotation(x=df_merged.iloc[df_merged["SPO2"].idxmin()]["Date"], y=df_merged["SPO2"].min(), text=str(df_merged["SPO2"].min())+"%", showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_spo2.add_hline(y=df_merged["SPO2"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["SPO2"].mean(), 1)) + "%", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - fig_spo2.update_traces(marker_size=6) - spo2_summary_df = calculate_table_data(df_merged, "SPO2") - spo2_summary_table = dash_table.DataTable(spo2_summary_df.to_dict('records'), [{"name": i, "id": i} for i in spo2_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#8d3a18','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_sleep_minutes = px.bar(df_merged, x="Date", y=["Deep Sleep Minutes", "Light Sleep Minutes", "REM Sleep Minutes", "Awake Minutes"], title=f"Sleep Stages

Overall average : {format_minutes(int(sleep_avg['overall']))} | Last 30d average : {format_minutes(int(sleep_avg['30d']))}


", color_discrete_map={"Deep Sleep Minutes": '#084466', "Light Sleep Minutes": '#1e9ad6', "REM Sleep Minutes": '#4cc5da', "Awake Minutes": '#fd7676',}, height=500) - fig_sleep_minutes.update_layout(yaxis_title='Sleep Minutes', legend=dict(orientation="h",yanchor="bottom", y=1.02, xanchor="right", x=1, title_text=''), yaxis=dict(tickvals=[1,120,240,360,480,600,720], ticktext=[f"{m // 60}h" for m in [1,120,240,360,480,600,720]], title="Sleep Time (hours)")) - if df_merged["Total Sleep Minutes"].dtype != object: - fig_sleep_minutes.add_annotation(x=df_merged.iloc[df_merged["Total Sleep Minutes"].idxmax()]["Date"], y=df_merged["Total Sleep Minutes"].max(), text=str(format_minutes(df_merged["Total Sleep Minutes"].max())), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_sleep_minutes.add_annotation(x=df_merged.iloc[df_merged["Total Sleep Minutes"].idxmin()]["Date"], y=df_merged["Total Sleep Minutes"].min(), text=str(format_minutes(df_merged["Total Sleep Minutes"].min())), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) - fig_sleep_minutes.add_hline(y=df_merged["Total Sleep Minutes"].mean(), line_dash="dot",annotation_text="Average : " + str(format_minutes(int(df_merged["Total Sleep Minutes"].mean()))), annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - fig_sleep_minutes.update_xaxes(rangeslider_visible=True,range=[dates_str_list[-30], dates_str_list[-1]],rangeslider_range=[dates_str_list[0], dates_str_list[-1]]) - sleep_summary_df = calculate_table_data(df_merged, "Total Sleep Minutes") - sleep_summary_table = dash_table.DataTable(sleep_summary_df.to_dict('records'), [{"name": i, "id": i} for i in sleep_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#636efa','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) - fig_sleep_regularity = px.bar(df_merged, x="Date", y="Total Sleep Seconds", base="Sleep Start Time Seconds", title="Sleep Regularity

The chart time here is always in local time ( Independent of timezone changes )
", labels={"Total Sleep Seconds":"Time of Day ( HH:MM )"}) - fig_sleep_regularity.update_layout(yaxis = dict(tickmode = 'array',tickvals = list(range(0, 120000, 10000)),ticktext = list(map(seconds_to_tick_label, list(range(0, 120000, 10000)))))) - fig_sleep_regularity.add_hline(y=df_merged["Sleep Start Time Seconds"].mean(), line_dash="dot",annotation_text="Sleep Start Time Trend : "+ str(seconds_to_tick_label(int(df_merged["Sleep Start Time Seconds"].mean()))), annotation_position="bottom right", annotation_bgcolor="#0a3024", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - fig_sleep_regularity.add_hline(y=df_merged["Sleep End Time Seconds"].mean(), line_dash="dot",annotation_text="Sleep End Time Trend : " + str(seconds_to_tick_label(int(df_merged["Sleep End Time Seconds"].mean()))), annotation_position="top left", annotation_bgcolor="#5e060d", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) - return report_title, report_dates_range, generated_on_date, fig_rhr, rhr_summary_table, fig_steps, fig_steps_heatmap, steps_summary_table, fig_activity_minutes, fat_burn_summary_table, cardio_summary_table, peak_summary_table, fig_weight, weight_summary_table, fig_spo2, spo2_summary_table, fig_sleep_minutes, fig_sleep_regularity, sleep_summary_table, [{'label': 'Color Code Sleep Stages', 'value': 'Color Code Sleep Stages','disabled': False}], "" - -if __name__ == '__main__': - app.run_server(debug=True) - - - -# %% +import os +import base64 +import logging +import requests +import dash, requests +from dash import dcc +from dash import html, dash_table +from dash.dependencies import Output, State, Input +import pandas as pd +import numpy as np +import plotly.express as px +from datetime import datetime, timedelta +import dash_dangerously_set_inner_html +from urllib.parse import parse_qs, urlparse + +log = logging.getLogger(__name__) +for variable in ['CLIENT_ID','CLIENT_SECRET','REDIRECT_URL'] : + if variable not in os.environ.keys() : + log.error(f'Missing required environment variable \'{variable}\', please review the README') + exit(1) + +app = dash.Dash(__name__) +app.title = "Fitbit Wellness Report" +server = app.server + +app.layout = html.Div(children=[ + dcc.ConfirmDialog( + id='errordialog', + message='Invalid Access Token : Unable to fetch data', + ), + html.Div(id="input-area", className="hidden-print", + style={ + 'display': 'flex', + 'align-items': 'center', + 'justify-content': 'center', + 'gap': '20px', + 'margin': 'auto', + 'flex-wrap': 'wrap', + 'margin-top': '30px' + },children=[ + dcc.DatePickerRange( + id='my-date-picker-range', + display_format='MMMM DD, Y', + minimum_nights=40, + max_date_allowed=datetime.today().date() - timedelta(days=1), + min_date_allowed=datetime.today().date() - timedelta(days=1000), + end_date=datetime.today().date() - timedelta(days=1), + start_date=datetime.today().date() - timedelta(days=365) + ), + html.Button(id='submit-button', type='submit', children='Submit', n_clicks=0, className="button-primary"), + html.Button("Login to FitBit", id="login-button"), + ]), + dcc.Location(id="location"), + dcc.Store(id="oauth-token", storage_type='session'), # Store OAuth token in session storage + html.Div(id="instruction-area", className="hidden-print", style={'margin-top':'30px', 'margin-right':'auto', 'margin-left':'auto','text-align':'center'}, children=[ + html.P( "Select a date range to generate a report.", style={'font-size':'17px', 'font-weight': 'bold', 'color':'#54565e'}), + ]), + html.Div(id='loading-div', style={'margin-top': '40px'}, children=[ + dcc.Loading( + id="loading-progress", + type="default", + children=html.Div(id="loading-output-1") + ), + ]), + + html.Div(id='output_div', style={'max-width': '1400px', 'margin': 'auto'}, children=[ + + html.Div(id='report-title-div', + style={ + 'display': 'flex', + 'align-items': 'center', + 'justify-content': 'center', + 'flex-direction': 'column', + 'margin-top': '20px'}, children=[ + html.H2(id="report-title", style={'font-weight': 'bold'}), + html.H4(id="date-range-title", style={'font-weight': 'bold'}), + html.P(id="generated-on-title", style={'font-weight': 'bold', 'font-size': '16'}) + ]), + html.Div(style={"height": '40px'}), + html.H4("Resting Heart Rate 💖", style={'font-weight': 'bold'}), + html.H6("Resting heart rate (RHR) is derived from a person's average sleeping heart rate. Fitbit tracks heart rate with photoplethysmography. This technique uses sensors and green light to detect blood volume when the heart beats. If a Fitbit device isn't worn during sleep, RHR is derived from daytime sedentary heart rate. According to the American Heart Association, a normal RHR is between 60-100 beats per minute (bpm), but this can vary based upon your age or fitness level."), + dcc.Graph( + id='graph_RHR', + figure=px.line(), + config= {'displaylogo': False} + ), + html.Div(id='RHR_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.H4("Steps Count 👣", style={'font-weight': 'bold'}), + html.H6("Fitbit devices use an accelerometer to track steps. Some devices track active minutes, which includes activities over 3 metabolic equivalents (METs), such as brisk walking and cardio workouts."), + dcc.Graph( + id='graph_steps', + figure=px.bar(), + config= {'displaylogo': False} + ), + dcc.Graph( + id='graph_steps_heatmap', + figure=px.bar(), + config= {'displaylogo': False} + ), + html.Div(id='steps_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.H4("Activity 🏃‍♂️", style={'font-weight': 'bold'}), + html.H6("Heart Rate Zones (fat burn, cardio and peak) are based on a percentage of maximum heart rate. Maximum heart rate is calculated as 220 minus age. The Centers for Disease Control recommends that adults do at least 150-300 minutes of moderate-intensity aerobic activity each week or 75-150 minutes of vigorous-intensity aerobic activity each week."), + dcc.Graph( + id='graph_activity_minutes', + figure=px.bar(), + config= {'displaylogo': False} + ), + html.Div(id='fat_burn_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(id='cardio_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(id='peak_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.H4("Weight Log ⏲️", style={'font-weight': 'bold'}), + html.H6("Fitbit connects with the Aria family of smart scales to track weight. Weight may also be self-reported using the Fitbit app. Studies suggest that regular weigh-ins may help people who want to lose weight."), + dcc.Graph( + id='graph_weight', + figure=px.line(), + config= {'displaylogo': False} + ), + html.Div(id='weight_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.H4("SpO2 🩸", style={'font-weight': 'bold'}), + html.H6("A pulse oximeter reading indicates what percentage of your blood is saturated, known as the SpO2 level. A typical, healthy reading is 95–100% . If your SpO2 level is less than 92%, a doctor may recommend you get an ABG. A pulse ox is the most common type of test because it's noninvasive and provides quick readings."), + dcc.Graph( + id='graph_spo2', + figure=px.line(), + config= {'displaylogo': False} + ), + html.Div(id='spo2_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.H4("Sleep 💤", style={'font-weight': 'bold'}), + html.H6("Fitbit estimates sleep stages (awake, REM, light sleep and deep sleep) and sleep duration based on a person's movement and heart-rate patterns. The National Sleep Foundation recommends 7-9 hours of sleep per night for adults"), + dcc.Checklist(options=[{'label': 'Color Code Sleep Stages', 'value': 'Color Code Sleep Stages','disabled':True}], value=['Color Code Sleep Stages'], style={'max-width': '1330px', 'margin': 'auto'}, inline=True, id="sleep-stage-checkbox", className="hidden-print"), + dcc.Graph( + id='graph_sleep', + figure=px.bar(), + config= {'displaylogo': False} + ), + dcc.Graph( + id='graph_sleep_regularity', + figure=px.bar(), + config= {'displaylogo': False} + ), + html.Div(id='sleep_table', style={'max-width': '1200px', 'margin': 'auto', 'font-weight': 'bold'}, children=[]), + html.Div(style={"height": '40px'}), + html.Div(className="hidden-print", style={'margin': 'auto', 'text-align': 'center'}, children=[ + dash_dangerously_set_inner_html.DangerouslySetInnerHTML( ''' +
+ + + +
+ ''')]), + html.Div(style={"height": '25px'}), + ]), +]) + +@app.callback(Output('location', 'href'),Input('login-button', 'n_clicks')) +def authorize(n_clicks): + """Authorize the application""" + if n_clicks : + client_id = os.environ['CLIENT_ID'] + redirect_uri = os.environ['REDIRECT_URL'] + scope = 'profile activity cardio_fitness heartrate sleep weight oxygen_saturation respiratory_rate' + auth_url = f'https://www.fitbit.com/oauth2/authorize?scope={scope}&client_id={client_id}&response_type=code&prompt=none&redirect_uri={redirect_uri}' + return auth_url + return dash.no_update + +@app.callback(Output('oauth-token', 'data'),Input('location', 'href')) +def handle_oauth_callback(href): + """Process the OAuth callback""" + if href: + # Parse the query string from the URL to extract the 'code' parameter + parsed_url = urlparse(href) + query_params = parse_qs(parsed_url.query) + oauth_code = query_params.get('code', [None])[0] + if oauth_code : + print(f"OAuth code received") + else : + print("No OAuth code found in URL.") + return dash.no_update + # Exchange code for a token + client_id = os.environ['CLIENT_ID'] + client_isecret = os.environ['CLIENT_SECRET'] + redirect_uri = os.environ['REDIRECT_URL'] + token_url='https://api.fitbit.com/oauth2/token?' + payload = {'code': oauth_code, 'grant_type': 'authorization_code', 'client_id': client_id, 'redirect_uri': redirect_uri} + token_creds = base64.b64encode(f"{client_id}:{client_isecret}".encode("utf-8")).decode("utf-8") + token_headers = {"Authorization": f"Basic {token_creds}"} + token_response = requests.post(token_url, data=payload, headers=token_headers) + token_response_json = token_response.json() + access_token = token_response_json.get('access_token') + if access_token : + print(f"Acceess token received!") + return access_token + else : + print("No access token found in response.") + return dash.no_update + +@app.callback(Output('login-button', 'children'),Output('login-button', 'disabled'),Input('oauth-token', 'data')) +def update_login_button(oauth_token): + if oauth_token: + return html.Span("Logged in"), True + else: + return "Login to FitBit", False + + +def seconds_to_tick_label(seconds): + """Calculate the number of hours, minutes, and remaining seconds""" + hours, remainder = divmod(seconds, 3600) + minutes, seconds = divmod(remainder, 60) + mult, remainder = divmod(hours, 12) + if mult >=2: + hours = hours - (12*mult) + result_datetime = datetime(1, 1, 1, hour=hours, minute=minutes, second=seconds) + if result_datetime.hour >= 12: + result_datetime = result_datetime - timedelta(hours=12) + else: + result_datetime = result_datetime + timedelta(hours=12) + return result_datetime.strftime("%H:%M") + +def format_minutes(minutes): + return "%2dh %02dm" % (divmod(minutes, 60)) + +def calculate_table_data(df, measurement_name): + df = df.sort_values(by='Date', ascending=False) + result_data = { + 'Period' : ['30 days', '3 months', '6 months', '1 year'], + 'Average ' + measurement_name : [], + 'Max ' + measurement_name : [], + 'Min ' + measurement_name : [] + } + last_date = df.head(1)['Date'].values[0] + for period in [30, 90, 180, 365]: + end_date = last_date + start_date = end_date - pd.Timedelta(days=period) + + period_data = df[(df['Date'] >= start_date) & (df['Date'] <= end_date)] + + if len(period_data) >= period: + + max_hr = period_data[measurement_name].max() + if measurement_name == "Steps Count": + min_hr = period_data[period_data[measurement_name] != 0][measurement_name].min() + else: + min_hr = period_data[measurement_name].min() + average_hr = round(period_data[measurement_name].mean(),2) + + if measurement_name == "Total Sleep Minutes": + result_data['Average ' + measurement_name].append(format_minutes(average_hr)) + result_data['Max ' + measurement_name].append(format_minutes(max_hr)) + result_data['Min ' + measurement_name].append(format_minutes(min_hr)) + else: + result_data['Average ' + measurement_name].append(average_hr) + result_data['Max ' + measurement_name].append(max_hr) + result_data['Min ' + measurement_name].append(min_hr) + else: + result_data['Average ' + measurement_name].append(pd.NA) + result_data['Max ' + measurement_name].append(pd.NA) + result_data['Min ' + measurement_name].append(pd.NA) + + return pd.DataFrame(result_data) + +# Sleep stages checkbox functionality +@app.callback(Output('graph_sleep', 'figure', allow_duplicate=True), Input('sleep-stage-checkbox', 'value'), State('graph_sleep', 'figure'), prevent_initial_call=True) +def update_sleep_colors(value, fig): + if len(value) == 1: + fig['data'][0]['marker']['color'] = '#084466' + fig['data'][1]['marker']['color'] = '#1e9ad6' + fig['data'][2]['marker']['color'] = '#4cc5da' + fig['data'][3]['marker']['color'] = '#fd7676' + else: + fig['data'][0]['marker']['color'] = '#084466' + fig['data'][1]['marker']['color'] = '#084466' + fig['data'][2]['marker']['color'] = '#084466' + fig['data'][3]['marker']['color'] = '#084466' + return fig + +# Limits the date range to one year max +@app.callback(Output('my-date-picker-range', 'max_date_allowed'), Output('my-date-picker-range', 'end_date'), + [Input('my-date-picker-range', 'start_date')]) +def set_max_date_allowed(start_date): + start = datetime.strptime(start_date, "%Y-%m-%d") + current_date = datetime.today().date() - timedelta(days=1) + max_end_date = min((start + timedelta(days=365)).date(), current_date) + return max_end_date, max_end_date + +# Disables the button after click and starts calculations +@app.callback(Output('errordialog', 'displayed'), Output('submit-button', 'disabled'), Output('my-date-picker-range', 'disabled'), Input('submit-button', 'n_clicks'),State('oauth-token', 'data'),prevent_initial_call=True) +def disable_button_and_calculate(n_clicks, oauth_token): + headers = { + "Authorization": "Bearer " + oauth_token, + "Accept": "application/json" + } + try: + token_response = requests.get("https://api.fitbit.com/1/user/-/profile.json", headers=headers) + token_response.raise_for_status() + except: + return True, False, False + return False, True, True + +# Fetch data and update graphs on click of submit +@app.callback(Output('report-title', 'children'), Output('date-range-title', 'children'), Output('generated-on-title', 'children'), Output('graph_RHR', 'figure'), Output('RHR_table', 'children'), Output('graph_steps', 'figure'), Output('graph_steps_heatmap', 'figure'), Output('steps_table', 'children'), Output('graph_activity_minutes', 'figure'), Output('fat_burn_table', 'children'), Output('cardio_table', 'children'), Output('peak_table', 'children'), Output('graph_weight', 'figure'), Output('weight_table', 'children'), Output('graph_spo2', 'figure'), Output('spo2_table', 'children'), Output('graph_sleep', 'figure'), Output('graph_sleep_regularity', 'figure'), Output('sleep_table', 'children'), Output('sleep-stage-checkbox', 'options'), Output("loading-output-1", "children"), +Input('submit-button', 'disabled'),State('my-date-picker-range', 'start_date'), State('my-date-picker-range', 'end_date'),State('oauth-token', 'data'), +prevent_initial_call=True) +def update_output(n_clicks, start_date, end_date, oauth_token): + + start_date = datetime.fromisoformat(start_date).strftime("%Y-%m-%d") + end_date = datetime.fromisoformat(end_date).strftime("%Y-%m-%d") + + headers = { + "Authorization": "Bearer " + oauth_token, + "Accept": "application/json" + } + + # Collecting data----------------------------------------------------------------------------------------------------------------------- + + user_profile = requests.get("https://api.fitbit.com/1/user/-/profile.json", headers=headers).json() + response_heartrate = requests.get("https://api.fitbit.com/1/user/-/activities/heart/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() + response_steps = requests.get("https://api.fitbit.com/1/user/-/activities/steps/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() + response_weight = requests.get("https://api.fitbit.com/1/user/-/body/weight/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() + response_spo2 = requests.get("https://api.fitbit.com/1/user/-/spo2/date/"+ start_date +"/"+ end_date +".json", headers=headers).json() + + # Processing data----------------------------------------------------------------------------------------------------------------------- + days_name_list = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday') + report_title = "Wellness Report - " + user_profile["user"]["firstName"] + " " + user_profile["user"]["lastName"] + report_dates_range = datetime.fromisoformat(start_date).strftime("%d %B, %Y") + " – " + datetime.fromisoformat(end_date).strftime("%d %B, %Y") + generated_on_date = "Report Generated : " + datetime.today().date().strftime("%d %B, %Y") + dates_list = [] + dates_str_list = [] + rhr_list = [] + steps_list = [] + weight_list = [] + spo2_list = [] + sleep_record_dict = {} + deep_sleep_list, light_sleep_list, rem_sleep_list, awake_list, total_sleep_list, sleep_start_times_list = [],[],[],[],[],[] + fat_burn_minutes_list, cardio_minutes_list, peak_minutes_list = [], [], [] + + for entry in response_heartrate['activities-heart']: + dates_str_list.append(entry['dateTime']) + dates_list.append(datetime.strptime(entry['dateTime'], '%Y-%m-%d')) + try: + fat_burn_minutes_list.append(entry["value"]["heartRateZones"][1]["minutes"]) + cardio_minutes_list.append(entry["value"]["heartRateZones"][2]["minutes"]) + peak_minutes_list.append(entry["value"]["heartRateZones"][3]["minutes"]) + except KeyError as E: + fat_burn_minutes_list.append(None) + cardio_minutes_list.append(None) + peak_minutes_list.append(None) + if 'restingHeartRate' in entry['value']: + rhr_list.append(entry['value']['restingHeartRate']) + else: + rhr_list.append(None) + + for entry in response_steps['activities-steps']: + if int(entry['value']) == 0: + steps_list.append(None) + else: + steps_list.append(int(entry['value'])) + + for entry in response_weight["body-weight"]: + weight_list.append(float(entry['value'])) + + for entry in response_spo2: + spo2_list += [None]*(dates_str_list.index(entry["dateTime"])-len(spo2_list)) + spo2_list.append(entry["value"]["avg"]) + spo2_list += [None]*(len(dates_str_list)-len(spo2_list)) + + for i in range(0,len(dates_str_list),100): + end_index = i+100 + if i+100 > len(dates_str_list): + end_index = len(dates_str_list) + temp_start_date = dates_str_list[i] + temp_end_date = dates_str_list[end_index-1] + + response_sleep = requests.get("https://api.fitbit.com/1.2/user/-/sleep/date/"+ temp_start_date +"/"+ temp_end_date +".json", headers=headers).json() + + for sleep_record in response_sleep["sleep"][::-1]: + if sleep_record['isMainSleep']: + try: + sleep_start_time = datetime.strptime(sleep_record["startTime"], "%Y-%m-%dT%H:%M:%S.%f") + if sleep_start_time.hour < 12: + sleep_start_time = sleep_start_time + timedelta(hours=12) + else: + sleep_start_time = sleep_start_time + timedelta(hours=-12) + sleep_time_of_day = sleep_start_time.time() + sleep_record_dict[sleep_record['dateOfSleep']] = {'deep': sleep_record['levels']['summary']['deep']['minutes'], + 'light': sleep_record['levels']['summary']['light']['minutes'], + 'rem': sleep_record['levels']['summary']['rem']['minutes'], + 'wake': sleep_record['levels']['summary']['wake']['minutes'], + 'total_sleep': sleep_record["minutesAsleep"], + 'start_time_seconds': (sleep_time_of_day.hour * 3600) + (sleep_time_of_day.minute * 60) + sleep_time_of_day.second + } + except KeyError as E: + pass + + for day in dates_str_list: + if day in sleep_record_dict: + deep_sleep_list.append(sleep_record_dict[day]['deep']) + light_sleep_list.append(sleep_record_dict[day]['light']) + rem_sleep_list.append(sleep_record_dict[day]['rem']) + awake_list.append(sleep_record_dict[day]['wake']) + total_sleep_list.append(sleep_record_dict[day]['total_sleep']) + sleep_start_times_list.append(sleep_record_dict[day]['start_time_seconds']) + else: + deep_sleep_list.append(None) + light_sleep_list.append(None) + rem_sleep_list.append(None) + awake_list.append(None) + total_sleep_list.append(None) + sleep_start_times_list.append(None) + + df_merged = pd.DataFrame({ + "Date": dates_list, + "Resting Heart Rate": rhr_list, + "Steps Count": steps_list, + "Fat Burn Minutes": fat_burn_minutes_list, + "Cardio Minutes": cardio_minutes_list, + "Peak Minutes": peak_minutes_list, + "weight": weight_list, + "SPO2": spo2_list, + "Deep Sleep Minutes": deep_sleep_list, + "Light Sleep Minutes": light_sleep_list, + "REM Sleep Minutes": rem_sleep_list, + "Awake Minutes": awake_list, + "Total Sleep Minutes": total_sleep_list, + "Sleep Start Time Seconds": sleep_start_times_list + }) + + df_merged['Total Sleep Seconds'] = df_merged['Total Sleep Minutes']*60 + df_merged["Sleep End Time Seconds"] = df_merged["Sleep Start Time Seconds"] + df_merged['Total Sleep Seconds'] + df_merged["Total Active Minutes"] = df_merged["Fat Burn Minutes"] + df_merged["Cardio Minutes"] + df_merged["Peak Minutes"] + rhr_avg = {'overall': round(df_merged["Resting Heart Rate"].mean(),1), '30d': round(df_merged["Resting Heart Rate"].tail(30).mean(),1)} + steps_avg = {'overall': int(df_merged["Steps Count"].mean()), '30d': int(df_merged["Steps Count"].tail(31).mean())} + weight_avg = {'overall': round(df_merged["weight"].mean(),1), '30d': round(df_merged["weight"].tail(30).mean(),1)} + spo2_avg = {'overall': round(df_merged["SPO2"].mean(),1), '30d': round(df_merged["SPO2"].tail(30).mean(),1)} + sleep_avg = {'overall': round(df_merged["Total Sleep Minutes"].mean(),1), '30d': round(df_merged["Total Sleep Minutes"].tail(30).mean(),1)} + active_mins_avg = {'overall': round(df_merged["Total Active Minutes"].mean(),2), '30d': round(df_merged["Total Active Minutes"].tail(30).mean(),2)} + weekly_steps_array = np.array([0]*days_name_list.index(datetime.fromisoformat(start_date).strftime('%A')) + df_merged["Steps Count"].to_list() + [0]*(6 - days_name_list.index(datetime.fromisoformat(end_date).strftime('%A')))) + weekly_steps_array = np.transpose(weekly_steps_array.reshape((int(len(weekly_steps_array)/7), 7))) + weekly_steps_array = pd.DataFrame(weekly_steps_array, index=days_name_list) + + # Plotting data----------------------------------------------------------------------------------------------------------------------- + + fig_rhr = px.line(df_merged, x="Date", y="Resting Heart Rate", line_shape="spline", color_discrete_sequence=["#d30f1c"], title=f"Daily Resting Heart Rate

Overall average : {rhr_avg['overall']} bpm | Last 30d average : {rhr_avg['30d']} bpm



") + if df_merged["Resting Heart Rate"].dtype != object: + fig_rhr.add_annotation(x=df_merged.iloc[df_merged["Resting Heart Rate"].idxmax()]["Date"], y=df_merged["Resting Heart Rate"].max(), text=str(df_merged["Resting Heart Rate"].max()), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_rhr.add_annotation(x=df_merged.iloc[df_merged["Resting Heart Rate"].idxmin()]["Date"], y=df_merged["Resting Heart Rate"].min(), text=str(df_merged["Resting Heart Rate"].min()), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_rhr.add_hline(y=df_merged["Resting Heart Rate"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["Resting Heart Rate"].mean(), 1)) + " BPM", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + fig_rhr.add_hrect(y0=62, y1=68, fillcolor="green", opacity=0.15, line_width=0) + rhr_summary_df = calculate_table_data(df_merged, "Resting Heart Rate") + rhr_summary_table = dash_table.DataTable(rhr_summary_df.to_dict('records'), [{"name": i, "id": i} for i in rhr_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#5f040a','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_steps = px.bar(df_merged, x="Date", y="Steps Count", color_discrete_sequence=["#2fb376"], title=f"Daily Steps Count

Overall average : {steps_avg['overall']} steps | Last 30d average : {steps_avg['30d']} steps



") + if df_merged["Steps Count"].dtype != object: + fig_steps.add_annotation(x=df_merged.iloc[df_merged["Steps Count"].idxmax()]["Date"], y=df_merged["Steps Count"].max(), text=str(df_merged["Steps Count"].max())+" steps", showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_steps.add_annotation(x=df_merged.iloc[df_merged["Steps Count"].idxmin()]["Date"], y=df_merged["Steps Count"].min(), text=str(df_merged["Steps Count"].min())+" steps", showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_steps.add_hline(y=df_merged["Steps Count"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["Steps Count"].mean(), 1)) + " Steps", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.8, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + fig_steps_heatmap = px.imshow(weekly_steps_array, color_continuous_scale='YLGn', origin='lower', title="Weekly Steps Heatmap", labels={'x':"Week Number", 'y': "Day of the Week"}, height=350, aspect='equal') + fig_steps_heatmap.update_traces(colorbar_orientation='h', selector=dict(type='heatmap')) + steps_summary_df = calculate_table_data(df_merged, "Steps Count") + steps_summary_table = dash_table.DataTable(steps_summary_df.to_dict('records'), [{"name": i, "id": i} for i in steps_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#072f1c','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_activity_minutes = px.bar(df_merged, x="Date", y=["Fat Burn Minutes", "Cardio Minutes", "Peak Minutes"], title=f"Activity Minutes

Overall total active minutes average : {active_mins_avg['overall']} minutes | Last 30d total active minutes average : {active_mins_avg['30d']} minutes



") + fig_activity_minutes.update_layout(yaxis_title='Active Minutes', legend=dict(orientation="h",yanchor="bottom", y=1.02, xanchor="right", x=1, title_text='')) + fat_burn_summary_df = calculate_table_data(df_merged, "Fat Burn Minutes") + fat_burn_summary_table = dash_table.DataTable(fat_burn_summary_df.to_dict('records'), [{"name": i, "id": i} for i in fat_burn_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#636efa','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + cardio_summary_df = calculate_table_data(df_merged, "Cardio Minutes") + cardio_summary_table = dash_table.DataTable(cardio_summary_df.to_dict('records'), [{"name": i, "id": i} for i in cardio_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#ef553b','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + peak_summary_df = calculate_table_data(df_merged, "Peak Minutes") + peak_summary_table = dash_table.DataTable(peak_summary_df.to_dict('records'), [{"name": i, "id": i} for i in peak_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#00cc96','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_weight = px.line(df_merged, x="Date", y="weight", line_shape="spline", color_discrete_sequence=["#6b3908"], title=f"Weight

Overall average : {weight_avg['overall']} Unit | Last 30d average : {weight_avg['30d']} Unit



") + if df_merged["weight"].dtype != object: + fig_weight.add_annotation(x=df_merged.iloc[df_merged["weight"].idxmax()]["Date"], y=df_merged["weight"].max(), text=str(df_merged["weight"].max()), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_weight.add_annotation(x=df_merged.iloc[df_merged["weight"].idxmin()]["Date"], y=df_merged["weight"].min(), text=str(df_merged["weight"].min()), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_weight.add_hline(y=round(df_merged["weight"].mean(),1), line_dash="dot",annotation_text="Average : " + str(round(df_merged["weight"].mean(), 1)) + " Units", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + weight_summary_df = calculate_table_data(df_merged, "weight") + weight_summary_table = dash_table.DataTable(weight_summary_df.to_dict('records'), [{"name": i, "id": i} for i in weight_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#4c3b7d','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_spo2 = px.scatter(df_merged, x="Date", y="SPO2", color_discrete_sequence=["#983faa"], title=f"SPO2 Percentage

Overall average : {spo2_avg['overall']}% | Last 30d average : {spo2_avg['30d']}%



", range_y=(90,100), labels={'SPO2':"SpO2(%)"}) + if df_merged["SPO2"].dtype != object: + fig_spo2.add_annotation(x=df_merged.iloc[df_merged["SPO2"].idxmax()]["Date"], y=df_merged["SPO2"].max(), text=str(df_merged["SPO2"].max())+"%", showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_spo2.add_annotation(x=df_merged.iloc[df_merged["SPO2"].idxmin()]["Date"], y=df_merged["SPO2"].min(), text=str(df_merged["SPO2"].min())+"%", showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_spo2.add_hline(y=df_merged["SPO2"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["SPO2"].mean(), 1)) + "%", annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + fig_spo2.update_traces(marker_size=6) + spo2_summary_df = calculate_table_data(df_merged, "SPO2") + spo2_summary_table = dash_table.DataTable(spo2_summary_df.to_dict('records'), [{"name": i, "id": i} for i in spo2_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#8d3a18','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_sleep_minutes = px.bar(df_merged, x="Date", y=["Deep Sleep Minutes", "Light Sleep Minutes", "REM Sleep Minutes", "Awake Minutes"], title=f"Sleep Stages

Overall average : {format_minutes(int(sleep_avg['overall']))} | Last 30d average : {format_minutes(int(sleep_avg['30d']))}


", color_discrete_map={"Deep Sleep Minutes": '#084466', "Light Sleep Minutes": '#1e9ad6', "REM Sleep Minutes": '#4cc5da', "Awake Minutes": '#fd7676',}, height=500) + fig_sleep_minutes.update_layout(yaxis_title='Sleep Minutes', legend=dict(orientation="h",yanchor="bottom", y=1.02, xanchor="right", x=1, title_text=''), yaxis=dict(tickvals=[1,120,240,360,480,600,720], ticktext=[f"{m // 60}h" for m in [1,120,240,360,480,600,720]], title="Sleep Time (hours)")) + if df_merged["Total Sleep Minutes"].dtype != object: + fig_sleep_minutes.add_annotation(x=df_merged.iloc[df_merged["Total Sleep Minutes"].idxmax()]["Date"], y=df_merged["Total Sleep Minutes"].max(), text=str(format_minutes(df_merged["Total Sleep Minutes"].max())), showarrow=False, arrowhead=0, bgcolor="#5f040a", opacity=0.80, yshift=15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_sleep_minutes.add_annotation(x=df_merged.iloc[df_merged["Total Sleep Minutes"].idxmin()]["Date"], y=df_merged["Total Sleep Minutes"].min(), text=str(format_minutes(df_merged["Total Sleep Minutes"].min())), showarrow=False, arrowhead=0, bgcolor="#0b2d51", opacity=0.80, yshift=-15, borderpad=5, font=dict(family="Helvetica, monospace", size=12, color="#ffffff"), ) + fig_sleep_minutes.add_hline(y=df_merged["Total Sleep Minutes"].mean(), line_dash="dot",annotation_text="Average : " + str(format_minutes(int(df_merged["Total Sleep Minutes"].mean()))), annotation_position="bottom right", annotation_bgcolor="#6b3908", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + fig_sleep_minutes.update_xaxes(rangeslider_visible=True,range=[dates_str_list[-30], dates_str_list[-1]],rangeslider_range=[dates_str_list[0], dates_str_list[-1]]) + sleep_summary_df = calculate_table_data(df_merged, "Total Sleep Minutes") + sleep_summary_table = dash_table.DataTable(sleep_summary_df.to_dict('records'), [{"name": i, "id": i} for i in sleep_summary_df.columns], style_data_conditional=[{'if': {'row_index': 'odd'},'backgroundColor': 'rgb(248, 248, 248)'}], style_header={'backgroundColor': '#636efa','fontWeight': 'bold', 'color': 'white', 'fontSize': '14px'}, style_cell={'textAlign': 'center'}) + fig_sleep_regularity = px.bar(df_merged, x="Date", y="Total Sleep Seconds", base="Sleep Start Time Seconds", title="Sleep Regularity

The chart time here is always in local time ( Independent of timezone changes )
", labels={"Total Sleep Seconds":"Time of Day ( HH:MM )"}) + fig_sleep_regularity.update_layout(yaxis = dict(tickmode = 'array',tickvals = list(range(0, 120000, 10000)),ticktext = list(map(seconds_to_tick_label, list(range(0, 120000, 10000)))))) + fig_sleep_regularity.add_hline(y=df_merged["Sleep Start Time Seconds"].mean(), line_dash="dot",annotation_text="Sleep Start Time Trend : "+ str(seconds_to_tick_label(int(df_merged["Sleep Start Time Seconds"].mean()))), annotation_position="bottom right", annotation_bgcolor="#0a3024", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + fig_sleep_regularity.add_hline(y=df_merged["Sleep End Time Seconds"].mean(), line_dash="dot",annotation_text="Sleep End Time Trend : " + str(seconds_to_tick_label(int(df_merged["Sleep End Time Seconds"].mean()))), annotation_position="top left", annotation_bgcolor="#5e060d", annotation_opacity=0.6, annotation_borderpad=5, annotation_font=dict(family="Helvetica, monospace", size=14, color="#ffffff")) + return report_title, report_dates_range, generated_on_date, fig_rhr, rhr_summary_table, fig_steps, fig_steps_heatmap, steps_summary_table, fig_activity_minutes, fat_burn_summary_table, cardio_summary_table, peak_summary_table, fig_weight, weight_summary_table, fig_spo2, spo2_summary_table, fig_sleep_minutes, fig_sleep_regularity, sleep_summary_table, [{'label': 'Color Code Sleep Stages', 'value': 'Color Code Sleep Stages','disabled': False}], "" + +if __name__ == '__main__': + app.run_server(debug=True) \ No newline at end of file