Updated the elevation related part and the elevation line plot. Also correction at the pace_bars_plot right tickz entries positioning bug.

This commit is contained in:
2026-05-07 15:19:37 +02:00
parent 9b329232c6
commit bbf99a59f2
2 changed files with 72 additions and 105 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -239,12 +239,15 @@ def process_fit(file_path):
df['time_diff_sec'] = df['time_diff'].dt.total_seconds()
df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])
# Cumulative distance (km)
distances = [0]
for i in range(1, len(df)):
d = haversine(df.loc[i-1, 'lon'], df.loc[i-1, 'lat'], df.loc[i, 'lon'], df.loc[i, 'lat'])
distances.append(distances[-1] + d)
df['cum_dist_km'] = distances
# Cumulative distance (km) (vektorisiert)
lat_r = np.radians(df['lat'].values)
lon_r = np.radians(df['lon'].values)
dlat = np.diff(lat_r, prepend=lat_r[0])
dlon = np.diff(lon_r, prepend=lon_r[0])
a = np.sin(dlat/2)**2 + np.cos(lat_r) * np.cos(np.roll(lat_r,1)) * np.sin(dlon/2)**2
a[0] = 0
step_dist = 2 * 6371 * np.arcsin(np.sqrt(np.clip(a, 0, 1)))
df['cum_dist_km'] = np.cumsum(step_dist)
# Elevation handling
if 'elev' in df.columns:
@@ -275,37 +278,27 @@ def process_fit(file_path):
df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)
# Heart rate handling (NEU!)
# ##############
# UPDATE: Da NaN-Problem mit heart_rate, manuell nochmal neu einlesen und überschreiben:
# save heart rate data into variable
# Heart rate handling
# Zweiter Durchlauf nötig: nach dropna() hat df weniger Zeilen als records,
# safe_add_column_to_dataframe behandelt den Längenunterschied korrekt.
heart_rate = []
for record in fit_file.get_messages("record"):
# Records can contain multiple pieces of data (ex: timestamp, latitude, longitude, etc)
for data in record:
# Print the name and value of the data (and the units if it has any)
if data.name == 'heart_rate':
heart_rate.append(data.value)
# Hier variable neu überschrieben:
df = safe_add_column_to_dataframe(df, 'heart_rate', heart_rate)
# ##############
# ######################################
# IF issues with heart_rate values, usw these DEBUG prints:
#print(heart_rate)
if 'heart_rate' in df.columns:
df['heart_rate'] = pd.to_numeric(df['heart_rate'], errors='coerce')
df['hr_smooth'] = df['heart_rate'].rolling(window=5, center=True).mean()
# print(f"Heart rate range: {df['heart_rate'].min():.0f} - {df['heart_rate'].max():.0f} bpm") # Uncomment if needed - DEBUG purpose!
#df['hr_smooth'] = df['heart_rate'].rolling(window=5, center=True).mean()
df['hr_smooth'] = df['heart_rate'].rolling(window=2, center=True, min_periods=1).mean()
else:
print("Keine Heart Rate Daten gefunden!")
df['heart_rate'] = np.nan
df['hr_smooth'] = np.nan
df['hr_smooth'] = np.nan
print(f"Verarbeitete FIT-Datei: {len(df)} Datenpunkte")
print(f"Distanz: {df['cum_dist_km'].iloc[-1]:.2f} km")
print(f"Dauer: {df['duration_hms'].iloc[-1]}")
# ######################################
return df
@@ -366,12 +359,15 @@ def process_gpx(file_path):
((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]
for i in range(1, len(df)):
d = haversine(df.loc[i-1, 'lon'], df.loc[i-1, 'lat'], df.loc[i, 'lon'], df.loc[i, 'lat'])
distances.append(distances[-1] + d)
df['cum_dist_km'] = distances
# Cumulative distance (km) (vektorisiert)
lat_r = np.radians(df['lat'].values)
lon_r = np.radians(df['lon'].values)
dlat = np.diff(lat_r, prepend=lat_r[0])
dlon = np.diff(lon_r, prepend=lon_r[0])
a = np.sin(dlat/2)**2 + np.cos(lat_r) * np.cos(np.roll(lat_r,1)) * np.sin(dlon/2)**2
a[0] = 0
step_dist = 2 * 6371 * np.arcsin(np.sqrt(np.clip(a, 0, 1)))
df['cum_dist_km'] = np.cumsum(step_dist)
# Elevation (gleiche Logik wie in deiner FIT-Funktion)
df['elev'] = df['elev'].bfill()
@@ -508,15 +504,12 @@ def calculate_calories_burned(df):
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
dt = np.diff(ts, prepend=ts[0])
mask = (dt > 0) & (dt <= 10)
frac = np.clip((hr - hr_rest) / (hr_max - hr_rest), 0, None)
met = np.clip((frac * vo2max) / 3.5, 1.0, 18.0)
kcal_per_s = met * _WEIGHT_KG * 3.5 / 12000.0
cumulative = float(np.sum(kcal_per_s[mask] * dt[mask]))
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))
@@ -1070,8 +1063,7 @@ def create_pixel_heatmap(dataframes,
if len(lats_r) < 2:
continue
xs, ys = to_px(lats_r, lons_r)
for x, y in zip(xs, ys):
count_grid[y, x] += 1
np.add.at(count_grid, (ys, xs), 1)
max_count = max(count_grid.max(), 1)
log_max = np.log1p(max_count)
@@ -1107,21 +1099,30 @@ def create_pixel_heatmap(dataframes,
# - Einzellauf-Modus: nur highlight_df, aber count_grid kam von allen
draw_frames = dataframes if (mode == 'city' or highlight_df is None) else [highlight_df]
for df in draw_frames: # ← draw_frames statt dataframes
from matplotlib.collections import LineCollection
# (LineCollection - alle Segmente in einem Aufruf):
for df in draw_frames:
lats_r = df['lat'].dropna().values
lons_r = df['lon'].dropna().values
if len(lats_r) < 2:
continue
xs, ys = to_px(lats_r, lons_r)
for i in range(len(xs) - 1):
mid_x = np.clip((xs[i] + xs[i+1]) // 2, 0, img_width - 1)
mid_y = np.clip((ys[i] + ys[i+1]) // 2, 0, img_height - 1)
norm_val = np.log1p(count_grid[mid_y, mid_x]) / log_max if log_max > 0 else 0
ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
color=cmap(norm_val), linewidth=line_width,
solid_capstyle='round', solid_joinstyle='round')
# Segmente als Array: shape (N-1, 2, 2)
points = np.array([xs, ys]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
# Farbe pro Segment aus count_grid
mid_xs = np.clip((xs[:-1] + xs[1:]) // 2, 0, img_width - 1)
mid_ys = np.clip((ys[:-1] + ys[1:]) // 2, 0, img_height - 1)
counts = count_grid[mid_ys, mid_xs]
norm_vals = np.log1p(counts) / log_max if log_max > 0 else np.zeros_like(counts)
colors = cmap(norm_vals)
lc = LineCollection(segments, colors=colors, linewidths=line_width,
capstyle='round', joinstyle='round')
ax.add_collection(lc)
# Colorbar als Inset-Axes (verändert NICHT die Figure-Größe)
#cbar_ax = fig_mpl.add_axes([0.88, 0.10, 0.025, 0.70]) # # verticale Position: rechts
@@ -1633,32 +1634,17 @@ def create_pixel_hr_map(df, img_width=900, img_height=900,
# ELEVATION-PLOT:
# -----------------------------------------------------------------------------
def create_elevation_plot(df, smooth_points=500):
# Originale Daten
x = df['time']
y = df['rel_elev']
# Einfache Glättung: nur Y-Werte glätten, X-Werte beibehalten
if len(y) >= 4: # Genug Punkte für cubic interpolation
y_numeric = y.to_numpy()
# Nur gültige Y-Punkte für Interpolation
mask = ~np.isnan(y_numeric)
if np.sum(mask) >= 4: # Genug gültige Punkte
# Index-basierte Interpolation für Y-Werte
valid_indices = np.where(mask)[0]
valid_y = y_numeric[mask]
# Interpolation über die Indizes
f = interp1d(valid_indices, valid_y, kind='cubic', bounds_error=False, fill_value='extrapolate')
# Neue Y-Werte für alle ursprünglichen X-Positionen
all_indices = np.arange(len(y))
y_smooth = f(all_indices)
# Originale X-Werte beibehalten
x_smooth = x
else:
# Fallback: originale Daten
x_smooth, y_smooth = x, y
else:
# Zu wenige Punkte: originale Daten verwenden
x_smooth, y_smooth = x, y
y_smooth = (
pd.Series(y.values)
.interpolate(method='linear', limit=10, limit_direction='both') # kurze Lücken schließen
.rolling(window=15, center=True, min_periods=1)
.mean()
.values
)
x_smooth = x
fig = go.Figure()
@@ -1957,12 +1943,12 @@ def create_heart_rate_plot(df):
fig = go.Figure()
# Heart Rate Linie (geglättet)
mask = df['heart_rate'].isna()
fig.add_trace(go.Scatter(
x=df['time'][~mask],
y=df['hr_smooth'][~mask],
y=df['heart_rate'][~mask],
mode='lines',
#name='Geglättete Herzfrequenz',
line=dict(color='#ff2c48', width=2),
line=dict(color='#ff2c48', width=1.5), # etwas dünner für gezackte Linie
showlegend=False,
hovertemplate=(
"Zeit: %{x}<br>" +
@@ -2232,7 +2218,7 @@ def create_pace_bars_plot(df, formatted_pace=None):
# -------------------------------------------------------------------------
fig.add_trace(go.Bar(
x=pace_vals,
y=y_labels,
y=list(range(len(y_labels))), # ← 0,1,2,3... statt Strings y=y_labels,
orientation='h',
marker=dict(
color=bar_colors,
@@ -2296,7 +2282,7 @@ def create_pace_bars_plot(df, formatted_pace=None):
if right_text:
fig.add_annotation(
x=x_max,
y=km_lbl,
y=i, # ← numerischer Index statt km_lbl String
text=right_text,
showarrow=False,
xanchor='right',
@@ -2329,13 +2315,16 @@ def create_pace_bars_plot(df, formatted_pace=None):
yaxis=dict(
title='',
autorange='reversed', # km 1 oben, letzter km unten (wie Strava)
tickmode='array',
tickvals=list(range(len(y_labels))), # ← 0,1,2...
ticktext=y_labels, # ← "1","2",...,"0.4"
tickfont=dict(size=11, color='white'),
showgrid=False,
zeroline=False,
),
template='plotly_dark',
height=plot_height,
margin=dict(l=50, r=80, t=45, b=45),
margin=dict(l=40, r=40, t=45, b=45),
plot_bgcolor='#111111',
paper_bgcolor='#1e1e1e',
font=dict(color='white'),
@@ -2567,7 +2556,7 @@ def update_all_plots(json_data):
Output('heatmap-city-info', 'children'),
Input('file-dropdown', 'value'),
Input('heatmap-mode-toggle', 'value'),
prevent_initial_call=True
#prevent_initial_call=True # Sonst beim Start der App kein Renderprozess
)
def update_pixel_heatmap(selected_file, toggle_value):
"""
@@ -2578,11 +2567,11 @@ def update_pixel_heatmap(selected_file, toggle_value):
"""
import os
if not selected_file or selected_file in ['NO_FILES', 'NO_FOLDER', 'ERROR']:
fig = go.Figure()
fig.update_layout(paper_bgcolor='#1e1e1e', font=dict(color='white'),
title=dict(text='Keine Datei ausgewählt'))
return fig, ''
if not selected_file or selected_file in ['NO_FILES', 'NO_FOLDER', 'ERROR', None]:
empty = go.Figure()
empty.update_layout(paper_bgcolor='#1e1e1e', font=dict(color='white'),
title=dict(text='Datei wählen...', font=dict(color='white')))
return empty, ''
city_code = extract_city_code(selected_file)
city_info_text = f'Erkannte Region: {city_code}' if city_code else 'Kein Stadtcode erkannt'
@@ -2631,28 +2620,6 @@ def update_pixel_heatmap(selected_file, toggle_value):
return fig, city_info_text
# Lade alle Läufe dieser Stadt
all_options = list_files()
city_dfs, city_paths = load_runs_for_city(city_code, all_options)
if not city_dfs:
# Fallback: nur aktuelle Datei
try:
df_single = process_selected_file(selected_file)
city_dfs = [df_single]
except Exception:
city_dfs = []
fig = create_pixel_heatmap(
dataframes=city_dfs,
mode='city',
city_code=city_code,
n_city_runs=len(city_dfs),
)
city_info_text = f'Region {city_code} · {len(city_dfs)} Läufe geladen'
return fig, city_info_text
# Callback 3: Export SVG