Updated the banner, added: Elevation and Calories
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user