Created a new Heart-Rate-Pixel Map Plot, updated the Plot arangement, added elevation info in the hover-info on the main map course.
This commit is contained in:
@@ -765,16 +765,18 @@ def create_map_plot(df):
|
||||
height=800
|
||||
)
|
||||
|
||||
# Info: Frankfurt liegt ca. 112 m ü.NN, Hamburg ca. 6 m ü.NN.
|
||||
fig.update_traces(
|
||||
hovertemplate=(
|
||||
#"Time: %{customdata[0]}<br>" +
|
||||
"Distance (Km): %{customdata[0]:.2f}<br>" +
|
||||
"Speed (Km/h): %{customdata[1]:.2f}<br>" +
|
||||
"Heart Rate (bpm): %{customdata[2]}<br>" +
|
||||
"Elapsed Time: %{customdata[3]}<extra></extra>"
|
||||
"Time: %{customdata[5]}<br>" +
|
||||
"Distance: %{customdata[0]:.2f} km<br>" +
|
||||
"Elevation: %{customdata[1]:.0f} m ü.NN (%{customdata[2]:+.0f} m zum Start)<br>" + #„m ü.NN" bedeutet Meter über Normal-Null
|
||||
"Speed: %{customdata[3]:.1f} km/h<br>" +
|
||||
"Heart Rate: %{customdata[4]:.0f} bpm<extra></extra>"
|
||||
),
|
||||
#customdata=df[['time', 'cum_dist_km', 'duration_hms']]
|
||||
customdata=df[['cum_dist_km', 'speed_kmh', 'heart_rate', 'duration_hms']]
|
||||
#customdata=df[['cum_dist_km', 'speed_kmh', 'heart_rate', 'duration_hms']]
|
||||
customdata=df[['cum_dist_km', 'elev', 'rel_elev', 'speed_kmh', 'heart_rate', 'duration_hms']]
|
||||
)
|
||||
# Define map style and the line ontop
|
||||
fig.update_layout(map_style="open-street-map") #My-Fav: open-street-map, satellite-streets, dark, white-bg
|
||||
@@ -826,226 +828,6 @@ def create_map_plot(df):
|
||||
|
||||
|
||||
|
||||
# #####################
|
||||
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
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
# Separate Behandlung für positive und negative Bereiche
|
||||
y_array = np.array(y_smooth)
|
||||
x_array = np.array(x_smooth)
|
||||
|
||||
# Positive Bereiche (Anstiege) - Gradient von transparent unten zu grün oben
|
||||
positive_mask = y_array >= 0
|
||||
if np.any(positive_mask):
|
||||
# Nulllinie für positive Bereiche
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.zeros_like(y_array),
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Positive Bereiche mit Gradient nach oben
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.where(y_array >= 0, y_array, 0), # Nur positive Werte
|
||||
fill='tonexty', # Fill zur vorherigen Trace (Nulllinie)
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
fillgradient=dict(
|
||||
type="vertical",
|
||||
colorscale=[
|
||||
(0.0, "rgba(17, 17, 17, 0.0)"), # Transparent unten (bei y=0)
|
||||
(1.0, "rgba(43, 82, 144, 0.8)") # Blau oben (bei maximaler Höhe) Grün: 73, 189, 115
|
||||
]
|
||||
),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Negative Bereiche (Abstiege) - Gradient von grün unten zu transparent oben
|
||||
negative_mask = y_array < 0
|
||||
if np.any(negative_mask):
|
||||
# Nulllinie für negative Bereiche
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.where(y_array < 0, y_array, 0), # Nur negative Werte
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Negative Bereiche mit Gradient nach unten
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.zeros_like(y_array),
|
||||
fill='tonexty', # Fill zur vorherigen Trace (negative Werte)
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
fillgradient=dict(
|
||||
type="vertical",
|
||||
colorscale=[
|
||||
(0.0, "rgba(43, 82, 144, 0.8)"), # Blau unten (bei minimaler Tiefe)
|
||||
(1.0, "rgba(17, 17, 17, 0.0)") # Transparent oben (bei y=0)
|
||||
]
|
||||
),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Hauptlinie (geglättet) - über allem
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_smooth,
|
||||
y=y_smooth,
|
||||
mode='lines',
|
||||
line=dict(color='#2b5290', width=2), # Weiße Linie für bessere Sichtbarkeit
|
||||
name='Elevation',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Add horizontal reference line at y=0
|
||||
fig.add_shape(
|
||||
type='line',
|
||||
x0=df['time_loc'].iloc[0],
|
||||
x1=df['time_loc'].iloc[-1],
|
||||
y0=0,
|
||||
y1=0,
|
||||
line=dict(color='gray', width=1, dash='dash'),
|
||||
name='Durchschnittstempo'
|
||||
)
|
||||
|
||||
# Layout im Dark Theme
|
||||
fig.update_layout(
|
||||
title=dict(text='Höhenprofil (relativ zum Ausgangswert: 0m)', font=dict(size=16, color='white')),
|
||||
xaxis_title='Zeit',
|
||||
yaxis_title='Höhe relativ zum Start (m)',
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='#1e1e1e',
|
||||
plot_bgcolor='#111111',
|
||||
font=dict(color='white'),
|
||||
margin=dict(l=40, r=40, t=50, b=40),
|
||||
height=400,
|
||||
uirevision='constant', # Avoiding not needed Re-renderings
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
# Alte Version - normaler fill between:
|
||||
# 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
|
||||
#
|
||||
# fig = go.Figure()
|
||||
#
|
||||
# # Fläche unter der Kurve (mit geglätteten Daten)
|
||||
# fig.add_trace(go.Scatter(
|
||||
# x=x_smooth, y=y_smooth,
|
||||
# mode='lines',
|
||||
# line=dict(color='#1CAF50'), # Fill between color!
|
||||
# fill='tozeroy',
|
||||
# #fillcolor='rgba(226, 241, 248)',
|
||||
# hoverinfo='skip',
|
||||
# showlegend=False
|
||||
# ))
|
||||
#
|
||||
# # Hauptlinie (geglättet)
|
||||
# fig.add_trace(go.Scatter(
|
||||
# x=x_smooth, y=y_smooth,
|
||||
# mode='lines',
|
||||
# line=dict(color='#084C20', width=2), # Line color!
|
||||
# name='Elevation',
|
||||
# showlegend=False
|
||||
# ))
|
||||
#
|
||||
# # SUPDER IDEE, ABER GEHT NICHT WEGE NEUEN smoothed POINTS! GEHT NUR BEI X
|
||||
# #fig.update_traces(
|
||||
# # hovertemplate=(
|
||||
# # #"Time: %{customdata[0]}<br>" +
|
||||
# # "Distance (km): %{customdata[0]:.2f}<br>" +
|
||||
# # "Elevation: %{customdata[1]}<extra></extra>" +
|
||||
# # "Elapsed Time: %{customdata[2]}<extra></extra>"
|
||||
# # ),
|
||||
# # customdata=df[['cum_dist_km','elev', 'time']]
|
||||
# #
|
||||
#
|
||||
# # Layout im Dark Theme
|
||||
# fig.update_layout(
|
||||
# title=dict(text='Höhenprofil relativ zum Startwert', font=dict(size=16, color='white')),
|
||||
# xaxis_title='Zeit',
|
||||
# yaxis_title='Höhe relativ zum Start (m)',
|
||||
# template='plotly_dark',
|
||||
# paper_bgcolor='#1e1e1e',
|
||||
# plot_bgcolor='#111111',
|
||||
# font=dict(color='white'),
|
||||
# margin=dict(l=40, r=40, t=50, b=40),
|
||||
# height=400
|
||||
# )
|
||||
#
|
||||
# return fig
|
||||
# #####################
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1664,7 +1446,403 @@ def create_pixel_pace_map(df, img_width=900, img_height=900,
|
||||
return fig
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PLOT 4: HEART-RATE-MAP
|
||||
# -----------------------------------------------------------------------------
|
||||
def create_pixel_hr_map(df, img_width=900, img_height=900,
|
||||
line_width=3, bg_color='#0d0d0d'):
|
||||
"""
|
||||
Pixel-Map der Heart Rate je Streckenabschnitt.
|
||||
Farbe: dunkelrot (min BPM, niedrige Belastung) → weiß (max BPM, Vollgas).
|
||||
Fehlende HR-Daten (GPX-Dateien) werden als dunkle Linie gezeichnet.
|
||||
"""
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.cm as cm
|
||||
from matplotlib.colors import LinearSegmentedColormap, Normalize
|
||||
import plotly.graph_objects as go
|
||||
|
||||
DPI = 100
|
||||
|
||||
# Leerer Plot wenn keine GPS-Daten
|
||||
if df.empty or 'lat' not in df.columns or 'lon' not in df.columns:
|
||||
fig = go.Figure()
|
||||
fig.update_layout(paper_bgcolor='#1e1e1e', height=img_height,
|
||||
title=dict(text='Keine GPS-Daten', font=dict(color='white')))
|
||||
return fig
|
||||
|
||||
lats = df['lat'].dropna().values
|
||||
lons = df['lon'].dropna().values
|
||||
|
||||
# Heart Rate laden – prüfe beide möglichen Spaltennamen
|
||||
hr_values = None
|
||||
for col in ['heart_rate', 'hr_smooth']:
|
||||
if col in df.columns and df[col].notna().sum() > 10:
|
||||
hr_values = df[col].ffill().bfill().values
|
||||
break
|
||||
|
||||
has_hr = hr_values is not None
|
||||
|
||||
n = min(len(lats), len(lons), len(hr_values) if has_hr else len(lats))
|
||||
lats = lats[:n]
|
||||
lons = lons[:n]
|
||||
if has_hr:
|
||||
hr_values = hr_values[:n]
|
||||
|
||||
# Pixel-Koordinaten
|
||||
xs, ys, _ = _gps_to_pixel(lats, lons, img_width, img_height,
|
||||
pad_top=40, pad_bottom=60, pad_left=10, pad_right=10)
|
||||
xs = xs.astype(int)
|
||||
ys = ys.astype(int)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Colormap: dunkelrot (niedrige BPM) → rot → orange → gelb → weiß (max BPM)
|
||||
# -------------------------------------------------------------------------
|
||||
cmap_colors = [
|
||||
(0.00, '#3d0000'), # sehr dunkelrot (minimale HR)
|
||||
(0.25, '#8b0000'), # dunkelrot
|
||||
(0.50, '#cc2200'), # rot
|
||||
(0.70, '#ff6600'), # orange
|
||||
(0.85, '#ffcc00'), # gelb
|
||||
(1.00, '#ffffff'), # weiß (maximale HR = Vollgas)
|
||||
]
|
||||
cmap_hr = LinearSegmentedColormap.from_list('heartrate', cmap_colors, N=256)
|
||||
|
||||
# HR-Grenzen: Perzentile statt absolutes Min/Max → Ausreißer ignorieren
|
||||
if has_hr:
|
||||
valid_hr = hr_values[~np.isnan(hr_values)]
|
||||
valid_hr = valid_hr[(valid_hr > 40) & (valid_hr < 220)] # Plausibilitätsfilter
|
||||
if len(valid_hr) > 0:
|
||||
hr_min = np.percentile(valid_hr, 2)
|
||||
hr_max = np.percentile(valid_hr, 98)
|
||||
else:
|
||||
hr_min, hr_max = 100, 180
|
||||
has_hr = False
|
||||
else:
|
||||
hr_min, hr_max = 100, 180
|
||||
|
||||
norm_hr = Normalize(vmin=hr_min, vmax=hr_max)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Canvas: exakt img_width × img_height Pixel
|
||||
# -------------------------------------------------------------------------
|
||||
fig_mpl = plt.figure(figsize=(img_width / DPI, img_height / DPI), dpi=DPI)
|
||||
fig_mpl.patch.set_facecolor(bg_color)
|
||||
ax = fig_mpl.add_axes([0, 0, 1, 1])
|
||||
ax.set_facecolor(bg_color)
|
||||
ax.set_xlim(0, img_width)
|
||||
ax.set_ylim(img_height, 0)
|
||||
ax.axis('off')
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Linien zeichnen
|
||||
# -------------------------------------------------------------------------
|
||||
if has_hr:
|
||||
for i in range(n - 1):
|
||||
hr = hr_values[i]
|
||||
if np.isnan(hr) or hr < 40 or hr > 220:
|
||||
# Ungültige HR → sehr dunkel
|
||||
ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
|
||||
color='#1a1a1a', linewidth=line_width * 0.6,
|
||||
solid_capstyle='round')
|
||||
continue
|
||||
|
||||
color = cmap_hr(norm_hr(hr))
|
||||
ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
|
||||
color=color, linewidth=line_width,
|
||||
solid_capstyle='round', solid_joinstyle='round')
|
||||
else:
|
||||
# Keine HR-Daten → Route grau zeichnen mit Hinweis
|
||||
for i in range(n - 1):
|
||||
ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
|
||||
color='#444444', linewidth=line_width,
|
||||
solid_capstyle='round', solid_joinstyle='round')
|
||||
fig_mpl.text(0.5, 0.50, 'Keine Heart-Rate-Daten\n(nur in .fit Dateien verfügbar)',
|
||||
color='#888888', fontsize=11, ha='center', va='center',
|
||||
transform=fig_mpl.transFigure)
|
||||
|
||||
# Start / Ziel
|
||||
ax.plot(xs[0], ys[0], 'o', color='#b9fc62', markersize=10, zorder=5)
|
||||
ax.plot(xs[-1], ys[-1], 's', color='#fca062', markersize=9, zorder=5)
|
||||
|
||||
# Titel
|
||||
if has_hr:
|
||||
avg_hr = float(np.nanmean(valid_hr))
|
||||
title_str = (f'HR-Map · Ø {avg_hr:.0f} bpm | '
|
||||
f'min {hr_min:.0f} max {hr_max:.0f} bpm')
|
||||
else:
|
||||
title_str = 'HR-Map · Keine Heart-Rate-Daten'
|
||||
|
||||
fig_mpl.text(0.5, 0.97, title_str,
|
||||
color='white', fontsize=10, ha='center', va='top',
|
||||
transform=fig_mpl.transFigure)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Colorbar horizontal unten
|
||||
# dunkelrot links (min BPM) → weiß rechts (max BPM)
|
||||
# -------------------------------------------------------------------------
|
||||
if has_hr:
|
||||
cbar_ax = fig_mpl.add_axes([0.15, 0.04, 0.70, 0.020])
|
||||
sm = cm.ScalarMappable(cmap=cmap_hr, norm=norm_hr)
|
||||
sm.set_array([])
|
||||
cbar = fig_mpl.colorbar(sm, cax=cbar_ax, orientation='horizontal')
|
||||
cbar.set_label('Heart Rate (bpm)', color='white', fontsize=8)
|
||||
cbar.ax.xaxis.set_tick_params(color='white', labelcolor='white', labelsize=7)
|
||||
cbar.set_ticks([hr_min, (hr_min + hr_max) / 2, hr_max])
|
||||
cbar.set_ticklabels([
|
||||
f'{hr_min:.0f} bpm',
|
||||
f'{(hr_min + hr_max) / 2:.0f} bpm',
|
||||
f'{hr_max:.0f} bpm'
|
||||
])
|
||||
|
||||
img_b64 = _fig_to_base64(fig_mpl)
|
||||
|
||||
# Plotly-Figure
|
||||
fig = go.Figure()
|
||||
fig.add_layout_image(dict(
|
||||
source=img_b64, xref='paper', yref='paper',
|
||||
x=0, y=1, sizex=1, sizey=1,
|
||||
xanchor='left', yanchor='top', layer='below'
|
||||
))
|
||||
fig.update_layout(
|
||||
title=dict(
|
||||
text='Pixel-HR-Map (dunkelrot = niedrige BPM · weiß = maximale BPM)',
|
||||
font=dict(size=13, color='white')
|
||||
),
|
||||
paper_bgcolor=bg_color,
|
||||
plot_bgcolor=bg_color,
|
||||
font=dict(color='white'),
|
||||
margin=dict(l=0, r=0, t=30, b=0),
|
||||
height=img_height,
|
||||
xaxis=dict(visible=False, range=[0, 1]),
|
||||
yaxis=dict(visible=False, range=[0, 1], scaleanchor='x'),
|
||||
uirevision='hr_map',
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# #####################
|
||||
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
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
# Separate Behandlung für positive und negative Bereiche
|
||||
y_array = np.array(y_smooth)
|
||||
x_array = np.array(x_smooth)
|
||||
|
||||
# Positive Bereiche (Anstiege) - Gradient von transparent unten zu grün oben
|
||||
positive_mask = y_array >= 0
|
||||
if np.any(positive_mask):
|
||||
# Nulllinie für positive Bereiche
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.zeros_like(y_array),
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Positive Bereiche mit Gradient nach oben
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.where(y_array >= 0, y_array, 0), # Nur positive Werte
|
||||
fill='tonexty', # Fill zur vorherigen Trace (Nulllinie)
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
fillgradient=dict(
|
||||
type="vertical",
|
||||
colorscale=[
|
||||
(0.0, "rgba(17, 17, 17, 0.0)"), # Transparent unten (bei y=0)
|
||||
(1.0, "rgba(43, 82, 144, 0.8)") # Blau oben (bei maximaler Höhe) Grün: 73, 189, 115
|
||||
]
|
||||
),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Negative Bereiche (Abstiege) - Gradient von grün unten zu transparent oben
|
||||
negative_mask = y_array < 0
|
||||
if np.any(negative_mask):
|
||||
# Nulllinie für negative Bereiche
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.where(y_array < 0, y_array, 0), # Nur negative Werte
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Negative Bereiche mit Gradient nach unten
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_array,
|
||||
y=np.zeros_like(y_array),
|
||||
fill='tonexty', # Fill zur vorherigen Trace (negative Werte)
|
||||
mode='lines',
|
||||
line=dict(width=0),
|
||||
fillgradient=dict(
|
||||
type="vertical",
|
||||
colorscale=[
|
||||
(0.0, "rgba(43, 82, 144, 0.8)"), # Blau unten (bei minimaler Tiefe)
|
||||
(1.0, "rgba(17, 17, 17, 0.0)") # Transparent oben (bei y=0)
|
||||
]
|
||||
),
|
||||
hoverinfo='skip',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Hauptlinie (geglättet) - über allem
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x_smooth,
|
||||
y=y_smooth,
|
||||
mode='lines',
|
||||
line=dict(color='#2b5290', width=2), # Weiße Linie für bessere Sichtbarkeit
|
||||
name='Elevation',
|
||||
showlegend=False
|
||||
))
|
||||
|
||||
# Add horizontal reference line at y=0
|
||||
fig.add_shape(
|
||||
type='line',
|
||||
x0=df['time_loc'].iloc[0],
|
||||
x1=df['time_loc'].iloc[-1],
|
||||
y0=0,
|
||||
y1=0,
|
||||
line=dict(color='gray', width=1, dash='dash'),
|
||||
name='Durchschnittstempo'
|
||||
)
|
||||
|
||||
# Layout im Dark Theme
|
||||
fig.update_layout(
|
||||
title=dict(text='Höhenprofil (relativ zum Ausgangswert: 0m)', font=dict(size=16, color='white')),
|
||||
xaxis_title='Zeit',
|
||||
yaxis_title='Höhe relativ zum Start (m)',
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='#1e1e1e',
|
||||
plot_bgcolor='#111111',
|
||||
font=dict(color='white'),
|
||||
margin=dict(l=40, r=40, t=50, b=40),
|
||||
height=400,
|
||||
uirevision='constant', # Avoiding not needed Re-renderings
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
# Alte Version - normaler fill between:
|
||||
# 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
|
||||
#
|
||||
# fig = go.Figure()
|
||||
#
|
||||
# # Fläche unter der Kurve (mit geglätteten Daten)
|
||||
# fig.add_trace(go.Scatter(
|
||||
# x=x_smooth, y=y_smooth,
|
||||
# mode='lines',
|
||||
# line=dict(color='#1CAF50'), # Fill between color!
|
||||
# fill='tozeroy',
|
||||
# #fillcolor='rgba(226, 241, 248)',
|
||||
# hoverinfo='skip',
|
||||
# showlegend=False
|
||||
# ))
|
||||
#
|
||||
# # Hauptlinie (geglättet)
|
||||
# fig.add_trace(go.Scatter(
|
||||
# x=x_smooth, y=y_smooth,
|
||||
# mode='lines',
|
||||
# line=dict(color='#084C20', width=2), # Line color!
|
||||
# name='Elevation',
|
||||
# showlegend=False
|
||||
# ))
|
||||
#
|
||||
# # SUPDER IDEE, ABER GEHT NICHT WEGE NEUEN smoothed POINTS! GEHT NUR BEI X
|
||||
# #fig.update_traces(
|
||||
# # hovertemplate=(
|
||||
# # #"Time: %{customdata[0]}<br>" +
|
||||
# # "Distance (km): %{customdata[0]:.2f}<br>" +
|
||||
# # "Elevation: %{customdata[1]}<extra></extra>" +
|
||||
# # "Elapsed Time: %{customdata[2]}<extra></extra>"
|
||||
# # ),
|
||||
# # customdata=df[['cum_dist_km','elev', 'time']]
|
||||
# #
|
||||
#
|
||||
# # Layout im Dark Theme
|
||||
# fig.update_layout(
|
||||
# title=dict(text='Höhenprofil relativ zum Startwert', font=dict(size=16, color='white')),
|
||||
# xaxis_title='Zeit',
|
||||
# yaxis_title='Höhe relativ zum Start (m)',
|
||||
# template='plotly_dark',
|
||||
# paper_bgcolor='#1e1e1e',
|
||||
# plot_bgcolor='#111111',
|
||||
# font=dict(color='white'),
|
||||
# margin=dict(l=40, r=40, t=50, b=40),
|
||||
# height=400
|
||||
# )
|
||||
#
|
||||
# return fig
|
||||
# #####################
|
||||
|
||||
|
||||
|
||||
@@ -2102,7 +2280,7 @@ app.layout = html.Div([
|
||||
}),
|
||||
html.P(
|
||||
"Plot 1) Heatmap: Einzellauf oder alle Läufe der Region umschalten.\n"
|
||||
"Plot 2) & 3) Elevation- & Pace-Map: des aktuell gewählten Laufs. ",
|
||||
"Plot 2), 3) & 4) Elevation-, Pace- & HR-Map: des aktuell gewählten Laufs.",
|
||||
style={'color': '#666', 'margin': '2px 0 8px 0', 'fontSize': '12px'}
|
||||
),
|
||||
], style={'padding': '0 20px'}),
|
||||
@@ -2162,10 +2340,17 @@ app.layout = html.Div([
|
||||
dcc.Graph(id='fig-pixel-pace'),
|
||||
], style={'flex': '1', 'minWidth': '300px'}),
|
||||
|
||||
# --- HR-Map ---
|
||||
html.Div([
|
||||
dcc.Graph(id='fig-pixel-hr'),
|
||||
], style={'flex': '1', 'minWidth': '300px'}),
|
||||
|
||||
], style={
|
||||
'display': 'flex', 'flexWrap': 'wrap',
|
||||
'gap': '5px', 'padding': '0 20px',
|
||||
'backgroundColor': '#111111'
|
||||
'display': 'grid',
|
||||
'gridTemplateColumns': 'repeat(2, 1fr)', # immer 2 Spalten
|
||||
'gap': '8px',
|
||||
'padding': '0 20px',
|
||||
'backgroundColor': '#111111'
|
||||
}),
|
||||
|
||||
html.Hr(style={'borderColor': '#333', 'margin': '10px 20px'}),
|
||||
@@ -2204,6 +2389,7 @@ def load_data(selected_file): # Dateipfad der ausgewählten Da
|
||||
#Output('fig-pixel-heatmap', 'figure'),
|
||||
Output('fig-pixel-elevation', 'figure'), # ← aus pixel_map_extension.py
|
||||
Output('fig-pixel-pace', 'figure'), # ← aus pixel_map_extension.py
|
||||
Output('fig-pixel-hr', 'figure'), # ← NEU
|
||||
Input('stored-df', 'data'),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
@@ -2222,9 +2408,11 @@ def update_all_plots(json_data):
|
||||
# Pixel-Maps (Elevation + Pace bleiben hier; Heatmap hat eigenen Callback)
|
||||
fig_pixel_elevation = create_pixel_elevation_map(df, img_width=800, img_height=500)
|
||||
fig_pixel_pace = create_pixel_pace_map(df, img_width=800, img_height=500)
|
||||
fig_pixel_hr = create_pixel_hr_map(df, img_width=800, img_height=500) # ← NEU
|
||||
|
||||
return (info, fig_map, fig_elev, fig_dev, fig_speed, fig_hr, fig_pace,
|
||||
fig_pixel_elevation, fig_pixel_pace)
|
||||
fig_pixel_elevation, fig_pixel_pace, fig_pixel_hr)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user