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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user