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:
2026-05-06 12:59:13 +02:00
parent fc78b0c5b0
commit f5a49a218d
3 changed files with 444 additions and 237 deletions

View File

@@ -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)