From 80c82097c3ee8cceff595bd4819bf684a4322142 Mon Sep 17 00:00:00 2001 From: Marcel Weschke Date: Thu, 30 Apr 2026 13:13:13 +0200 Subject: [PATCH] Updated the banner, added: Elevation and Calories --- jogging_dashboard_browser_app.py | 94 ++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/jogging_dashboard_browser_app.py b/jogging_dashboard_browser_app.py index 6c5713a..9749436 100644 --- a/jogging_dashboard_browser_app.py +++ b/jogging_dashboard_browser_app.py @@ -352,7 +352,11 @@ def process_gpx(file_path): df['time_loc'] = df['time'].dt.tz_localize(None) df['time_diff'] = df['time'] - df['time'].iloc[0] df['time_diff_sec'] = df['time_diff'].dt.total_seconds() - df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0]) + #df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0]) + secs = df['time_diff'].dt.total_seconds().astype(int) + df['duration_hms'] = (secs // 3600).astype(int).astype(str).str.zfill(2) + ':' + \ + ((secs % 3600) // 60).astype(int).astype(str).str.zfill(2) + ':' + \ + (secs % 60).astype(int).astype(str).str.zfill(2) # Kumulative Distanz (gleiche Logik wie in deiner FIT-Funktion) distances = [0] @@ -445,6 +449,70 @@ def safe_add_column_to_dataframe(df, column_name, values): +# ============================================================================= +# NEU: Elevation Gain & Calories – Hilfsfunktionen +# ============================================================================= + +# ── Biometrics (kalibriert gegen Strava) ───────────────────────────────────── +_WEIGHT_KG = 75.0 +_HEIGHT_CM = 178.0 +_AGE_YEARS = 35 +_IS_MALE = True +_HR_REST = 64.5 # Ruhepuls in bpm – kalibriert gegen Strava 550 kcal +# ───────────────────────────────────────────────────────────────────────────── + +def calculate_elevation_gain(df): + if 'elev' not in df.columns or df['elev'].isna().all(): + return 0 + + elev_smooth = df['elev'].rolling( + window=30, win_type='gaussian', center=True, min_periods=1 + ).mean(std=5) + + delta = elev_smooth.diff().fillna(0) + # Schwellwert relativ zur Gesamtamplitude statt absolut fest + amplitude = df['elev'].max() - df['elev'].min() + threshold = amplitude * 0.00095 # ~0.4% der Gesamtamplitude + gain = delta[delta > threshold].sum() + + print(f"DEBUG elev: min={df['elev'].min():.1f}, max={df['elev'].max():.1f}, delta>0.03={delta[delta>0.03].sum():.1f}") + return int(round(gain)) + + +def calculate_calories_burned(df): + # Geschlechts- und altersbasierte HR-Zonen nach Karvonen + # Kein fixer HR_REST — nutze den tatsächlichen Minimum-HR aus dem Lauf + # als Annäherung an den Ruhepuls + hr_max = 220 - _AGE_YEARS + + use_hr = ('heart_rate' in df.columns and + df['heart_rate'].notna().sum() > len(df) * 0.5) + + if not use_hr: + return 0 + + hr = df['heart_rate'].ffill().fillna(df['heart_rate'].bfill()).values + ts = df['time_diff_sec'].values + + # Ruhepuls aus dem Lauf selbst schätzen: 5. Perzentile der HR-Werte + hr_rest = float(df['heart_rate'].quantile(0.05)) + hr_rest = max(45.0, min(hr_rest, 80.0)) # Plausibilitätsgrenze + + vo2max = 15.0 * (hr_max / hr_rest) + + cumulative = 0.0 + for i in range(1, len(ts)): + dt = ts[i] - ts[i - 1] + if dt <= 0 or dt > 10: + continue + frac = max(0.0, (hr[i] - hr_rest) / (hr_max - hr_rest)) + met = max(1.0, min((frac * vo2max) / 3.5, 18.0)) + kcal_per_s = met * _WEIGHT_KG * 3.5 / 12000.0 + cumulative += kcal_per_s * dt + + print(f"DEBUG calories: rows={len(df)}, hr_valid={df['heart_rate'].notna().sum()}, duration={df['time_diff_sec'].iloc[-1]:.0f}s, hr_mean={df['heart_rate'].mean():.1f}") + return int(round(cumulative)) + # ============================================================================= # INFO BANNER @@ -468,22 +536,38 @@ def create_info_banner(df): else: formatted_pace = "N/A" + # Elevation Gain + elevation_gain_m = calculate_elevation_gain(df) + + # Calories + total_calories = calculate_calories_burned(df) + # Build the info banner layout info_banner = html.Div([ html.Div([ html.H4("Total Distance", style={'margin-bottom': '5px'}), html.H2(f"{total_distance_km:.2f} km") - ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}), + ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}), html.Div([ html.H4("Total Time", style={'margin-bottom': '5px'}), html.H2(formatted_total_time) - ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}), + ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}), html.Div([ html.H4("Average Pace", style={'margin-bottom': '5px'}), html.H2(formatted_pace) - ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}), + ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}), + + html.Div([ + html.H4("Elevation", style={'margin-bottom': '5px'}), + html.H2(f"{elevation_gain_m} m") + ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}), + + html.Div([ + html.H4("Calories", style={'margin-bottom': '5px'}), + html.H2(f"{total_calories} kcal") + ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}), ], style={ 'display': 'flex', 'justifyContent': 'space-around', @@ -1395,7 +1479,9 @@ app.layout = html.Div([ Input('file-dropdown', 'value') ) def load_data(selected_file): # Dateipfad der ausgewählten Datei + print(f"DEBUG load_data: {selected_file}") df = process_selected_file(selected_file) # Verarbeitet diese Datei + print(f"DEBUG load_data: rows={len(df)}") return df.to_json(date_format='iso', orient='split') # Callback 2: Update All (static) Plots