diff --git a/src/app.py b/src/app.py index f0c08b7..e79208c 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,7 @@ # %% -import dash, requests, math +import dash, requests from dash import dcc -from dash import html +from dash import html, dash_table from dash.dependencies import Output, State, Input import pandas as pd import numpy as np @@ -16,7 +16,8 @@ server = app.server app.layout = html.Div(children=[ - html.Div(style={ + html.Div(className="hidden-print", + style={ 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', @@ -29,7 +30,7 @@ app.layout = html.Div(children=[ id='my-date-picker-range', minimum_nights=40, max_date_allowed=datetime.today().date() - timedelta(days=1), - min_date_allowed=datetime.today().date() - timedelta(days=720), + 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) ), @@ -45,7 +46,7 @@ app.layout = html.Div(children=[ ), ]), - html.Div(id='output_div', children=[ + html.Div(id='output_div', style={'max-width': '1400px', 'margin': 'auto'}, children=[ html.Div(id='report-title-div', style={ @@ -58,51 +59,104 @@ app.layout = html.Div(children=[ 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'}), ]), ]) +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) + + # Add the average to the result DataFrame + 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) + # 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") - max_end_date = start + timedelta(days=365) + 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 @@ -111,11 +165,10 @@ def disable_button_and_calculate(n_clicks): return True, 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('graph_steps', 'figure'), Output('graph_steps_heatmap', 'figure'), Output('graph_activity_minutes', 'figure'), Output('graph_weight', 'figure'), Output('graph_spo2', 'figure'), Output("loading-output-1", "children"), +@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("loading-output-1", "children"), Input('submit-button', 'disabled'), State('input-on-submit', 'value'), State('my-date-picker-range', 'start_date'), State('my-date-picker-range', 'end_date'), -prevent_initial_call=True -) +prevent_initial_call=True) def update_output(n_clicks, value, start_date, end_date): start_date = datetime.fromisoformat(start_date).strftime("%Y-%m-%d") @@ -136,7 +189,7 @@ def update_output(n_clicks, value, start_date, end_date): # 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_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 = [] @@ -188,12 +241,14 @@ def update_output(n_clicks, value, start_date, end_date): non_zero_steps_df = df_merged[df_merged["Steps Count"] != 0] 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': round(non_zero_steps_df["Steps Count"].mean(),0), '30d': round(non_zero_steps_df["Steps Count"].tail(30).mean(),0)} + steps_avg = {'overall': int(df_merged["Steps Count"].mean()), '30d': int(df_merged.sort_values(by='Date', ascending=False)["Steps Count"].head(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)} 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



") @@ -201,22 +256,38 @@ def update_output(n_clicks, value, start_date, end_date): 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)), 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) - fig_steps = px.bar(df_merged, x="Date", y="Steps Count", title=f"Daily Steps Count

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



") + 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



") 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=non_zero_steps_df.iloc[non_zero_steps_df["Steps Count"].idxmin()]["Date"], y=non_zero_steps_df["Steps Count"].min(), text=str(non_zero_steps_df["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=non_zero_steps_df["Steps Count"].mean(), line_dash="dot",annotation_text="Average : " + str(round(df_merged["Steps Count"].mean(), 1)), 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')) - fig_activity_minutes = px.bar(df_merged, x="Date", y=["Fat Burn Minutes", "Cardio Minutes", "Peak Minutes"], title=f"Activity Minutes

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



") + 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



") 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)), 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 = px.bar(df_merged, x="Date", y="SPO2", title="SPO2 Percentage", range_y=(80,100)) + 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)) + 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'}) - return report_title, report_dates_range, generated_on_date, fig_rhr, fig_steps, fig_steps_heatmap, fig_activity_minutes, fig_weight, fig_spo2, "" + 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, "" if __name__ == '__main__': app.run_server(debug=True) diff --git a/src/assets/custom_styling.css b/src/assets/custom_styling.css index e2eb625..afe097b 100644 --- a/src/assets/custom_styling.css +++ b/src/assets/custom_styling.css @@ -131,7 +131,7 @@ body { font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ line-height: 1.6; font-weight: 400; - font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: Helvetica, Arial, sans-serif; color: rgb(50, 50, 50); } @@ -140,7 +140,10 @@ body { h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: 0; - font-weight: 300; } + font-weight: 400; + max-width: 1330px; + margin: auto; +} h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} @@ -216,8 +219,8 @@ input[type="submit"].button-primary, input[type="reset"].button-primary, input[type="button"].button-primary { color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } + background-color: #108de4; + border-color: #028bb4; } .button.button-primary:hover, button.button-primary:hover, input[type="submit"].button-primary:hover, @@ -419,4 +422,10 @@ there. @media (min-width: 1000px) {} /* Larger than Desktop HD */ -@media (min-width: 1200px) {} \ No newline at end of file +@media (min-width: 1200px) {} + +@media print { + .hidden-print { + display: none !important; + } +} \ No newline at end of file