diff --git a/DashboardApp_WebVersion.png b/DashboardApp_WebVersion.png index 43c52af..809941c 100644 Binary files a/DashboardApp_WebVersion.png and b/DashboardApp_WebVersion.png differ diff --git a/jogging_dashboard_browser_app.py b/jogging_dashboard_browser_app.py index e7a0180..9584792 100644 --- a/jogging_dashboard_browser_app.py +++ b/jogging_dashboard_browser_app.py @@ -781,7 +781,7 @@ def create_map_plot(df): hovertemplate=( "Time: %{customdata[5]}
" + "Distance: %{customdata[0]:.2f} km
" + - "Elevation: %{customdata[1]:.0f} m ü.NN (%{customdata[2]:+.0f} m zum Start)
" + #„m ü.NN" bedeutet Meter über Normal-Null + "Elevation: %{customdata[1]:.0f} m ASL (Δ %{customdata[2]:+.0f} m from start)
" + #"ASL" = above sea level; diff. vs start value" "Speed: %{customdata[3]:.1f} km/h
" + "Heart Rate: %{customdata[4]:.0f} bpm" ), @@ -1160,7 +1160,7 @@ def create_pixel_heatmap(dataframes, img_b64 = _fig_to_base64(fig_mpl) plotly_title = ( - f'Pixel-Heatmap · {city_code} · {n_city_runs} Läufe (Region)' + f'Pixel-Heatmap · {city_code} · {n_city_runs} runs (region)' if mode == 'city' and city_code else 'Pixel-Heatmap · Einzellauf' ) fig = go.Figure() @@ -1438,7 +1438,7 @@ def create_pixel_pace_map(df, img_width=900, img_height=900, sec_v = int((valid_mean - min_v) * 60) fig_mpl.text(0.5, 0.97, f'Pace-Map · Ø {min_v}:{sec_v:02d} min/km | ' - f'schnell: {p5:.1f} langsam: {p95:.1f} min/km', + f'Slow: {p95:.1f} Fast: {p5:.1f} min/km', color='white', fontsize=10, ha='center', va='top', transform=fig_mpl.transFigure) @@ -1735,7 +1735,8 @@ def create_elevation_plot(df, smooth_points=500): y=y_smooth, mode='lines', line=dict(color='#2b5290', width=2), # Weiße Linie für bessere Sichtbarkeit - name='Elevation', + hovertemplate='Time: %{x|%H:%M:%S}
Δ Elevation = %{y:.1f} m', + #name='Elevation', showlegend=False )) @@ -1752,9 +1753,9 @@ def create_elevation_plot(df, smooth_points=500): # 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)', + title=dict(text='Elevation Profile (relative to start: 0 m)', font=dict(size=16, color='white')), + xaxis_title='Time', + yaxis_title='Relative Elevation (m)', template='plotly_dark', paper_bgcolor='#1e1e1e', plot_bgcolor='#111111', @@ -1852,51 +1853,6 @@ def create_elevation_plot(df, smooth_points=500): -# ----------------------------------------------------------------------------- -# DEVIATION-PLOT: Distanz-Zeit-Diagramm -# ----------------------------------------------------------------------------- -def create_deviation_plot(df): - # Compute mean velocity in km/s - vel_kmps_mean = df['cum_dist_km'].iloc[-1] / df['time_diff_sec'].iloc[-1] - # Expected cumulative distance assuming constant mean velocity - df['cum_dist_km_qmean'] = df['time_diff_sec'] * vel_kmps_mean - # Deviation from mean velocity distance - df['del_dist_km_qmean'] = df['cum_dist_km'] - df['cum_dist_km_qmean'] - # Plot the deviation - fig = px.line( - df, - x='time_loc', - y='del_dist_km_qmean', - labels={ - 'time_loc': 'Zeit', - 'del_dist_km_qmean': 'Δ Strecke (km)' - }, - template='plotly_dark', - ) - fig.update_layout( - title=dict(text='Abweichung von integriertem Durchschnittstempo', font=dict(size=16)), - yaxis_title='Abweichung (km)', - xaxis_title='Zeit', - height=400, - paper_bgcolor='#1e1e1e', - plot_bgcolor='#111111', - font=dict(color='white', size=14), - margin=dict(l=40, r=40, t=50, b=40), - uirevision='constant', # Avoiding not needed Re-renderings - ) - # 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' - ) - return fig - - # ----------------------------------------------------------------------------- # SPEED-PLOT: # ----------------------------------------------------------------------------- @@ -1909,12 +1865,13 @@ def create_speed_plot(df): y=df['speed_kmh_smooth'][~mask], mode='lines', name='Geglättete Geschwindigkeit', - line=dict(color='royalblue') + line=dict(color='royalblue'), + hovertemplate='Time: %{x|%H:%M:%S}
Speed = %{y:.1f} km/h', )) fig.update_layout( - title=dict(text=f'Tempo über die Zeit (geglättet) - Ø {mean_speed_kmh:.2f} km/h', font=dict(size=16)), - xaxis=dict(title='Zeit', tickformat='%H:%M', type='date'), - yaxis=dict(title='Geschwindigkeit (km/h)', rangemode='tozero'), + title=dict(text=f'Speed over Time (smoothed) - Ø {mean_speed_kmh:.2f} km/h', font=dict(size=16)), + xaxis=dict(title='Time', tickformat='%H:%M', type='date'), + yaxis=dict(title='Speed (km/h)', rangemode='tozero'), template='plotly_dark', paper_bgcolor='#1e1e1e', plot_bgcolor='#111111', @@ -1936,6 +1893,51 @@ def create_speed_plot(df): +# ----------------------------------------------------------------------------- +# DEVIATION-PLOT: Distanz-Zeit-Diagramm +# ----------------------------------------------------------------------------- +def create_deviation_plot(df): + # Compute mean velocity in km/s + vel_kmps_mean = df['cum_dist_km'].iloc[-1] / df['time_diff_sec'].iloc[-1] + # Expected cumulative distance assuming constant mean velocity + df['cum_dist_km_qmean'] = df['time_diff_sec'] * vel_kmps_mean + # Deviation from mean velocity distance + df['del_dist_km_qmean'] = df['cum_dist_km'] - df['cum_dist_km_qmean'] + # Plot the deviation + fig = go.Figure() + fig.add_trace(go.Scatter( + x=df['time_loc'], + y=df['del_dist_km_qmean'], + mode='lines', + name='Δ Strecke (km)', + line=dict(color='royalblue'), + hovertemplate='Time: %{x|%H:%M:%S}
Δ Strecke = %{y:.2f} km', + )) + fig.update_layout( + title=dict(text='Distance deviation from integrated average pace', font=dict(size=16)), + yaxis_title='Δ Distance (km)', + xaxis_title='Time', + template='plotly_dark', + height=400, + paper_bgcolor='#1e1e1e', + plot_bgcolor='#111111', + font=dict(color='white', size=14), + margin=dict(l=40, r=40, t=50, b=40), + uirevision='constant', # Avoiding not needed Re-renderings + ) + # 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' + ) + return fig + + # ----------------------------------------------------------------------------- # HEART-RATE-PLOT: @@ -1963,11 +1965,11 @@ def create_heart_rate_plot(df): x=df['time'][~mask], y=df['heart_rate'][~mask], mode='lines', - line=dict(color='#ff2c48', width=1.5), # etwas dünner für gezackte Linie + line=dict(color='#ff1d34', width=1.5), # etwas dünner für gezackte Linie showlegend=False, hovertemplate=( - "Zeit: %{x}
" + - "Herzfrequenz: %{y:.0f} bpm
" + + "Time: %{x}
" + + "Heart Rate: %{y:.0f} bpm
" + "" ) )) @@ -2061,19 +2063,19 @@ def create_heart_rate_plot(df): ) # Layout - title_text = f'Herzfrequenz über die Zeit (geglättete)' + title_text = f'Heart Rate over Time:' if mean_hr > 0: - title_text += f' - Ø {mean_hr:.0f} bpm (Range: {min_hr:.0f}-{max_hr:.0f})' + title_text += f' Ø {mean_hr:.0f} bpm (Range: {min_hr:.0f}-{max_hr:.0f})' fig.update_layout( title=dict(text=title_text, font=dict(size=16, color='white')), xaxis=dict( - title='Zeit', + title='Time', tickformat='%H:%M', type='date' ), yaxis=dict( - title='Herzfrequenz (bpm)', + title='Heart Rate (bpm)', range=[70, 200] # instead of: rangemode='tozero' ), template='plotly_dark', @@ -2313,7 +2315,7 @@ def create_pace_bars_plot(df, formatted_pace=None): fig.update_layout( title=dict( - text=f'Tempo je Kilometer · Ø {formatted_pace}', + text=f'Pace per km · Ø {formatted_pace}', font=dict(size=15, color='white') ), xaxis=dict( @@ -2328,7 +2330,7 @@ def create_pace_bars_plot(df, formatted_pace=None): color='white', ), yaxis=dict( - title='', + title='Distance (km)', autorange='reversed', # km 1 oben, letzter km unten (wie Strava) tickmode='array', tickvals=list(range(len(y_labels))), # ← 0,1,2... @@ -2369,7 +2371,7 @@ app.layout = html.Div([ html.Div([ # Linke Seite: Datei-Dropdown html.Div([ - html.Label("Datei wählen:", style={'color': '#aaaaaa', 'marginBottom': '5px'}), + html.Label("Select File:", style={'color': '#aaaaaa', 'marginBottom': '5px'}), dcc.Dropdown( id='file-dropdown', options=list_files(), @@ -2429,8 +2431,8 @@ app.layout = html.Div([ 'color': '#aaaaaa', 'margin': '10px 0 0 0', 'fontSize': '16px' }), html.P( - "Plot 1) Heatmap: Einzellauf oder alle Läufe der Region umschalten.\n" - "Plot 2), 3) & 4) Elevation-, Pace- & HR-Map: des aktuell gewählten Laufs.", + "Plot 1) Heatmap: Toggle between single run or all runs in the region.\n" + "Plot 2), 3) & 4) Elevation, Pace & HR maps: of the currently selected run.", style={'color': '#aaaaaa', 'margin': '2px 0 8px 0', 'fontSize': '12px'} ), ], style={'padding': '0 20px'}), @@ -2451,7 +2453,7 @@ app.layout = html.Div([ # dcc.Checklist als Toggle-Switch (ein Checkbox = ON/OFF) dcc.Checklist( id='heatmap-mode-toggle', - options=[{'label': ' Alle Läufe (Region)', 'value': 'city'}], + options=[{'label': ' All runs (regional)', 'value': 'city'}], #value=[], # Standard: leer = Einzellauf value=['city'], # Standard: Region aktiv - Jezt ist immer der Harken gesetzt! inputStyle={ @@ -2507,8 +2509,8 @@ app.layout = html.Div([ # ENDE !!!!!!!!!!!!!!!! dcc.Graph(id='fig-elevation'), - dcc.Graph(id='fig_deviation'), dcc.Graph(id='fig_speed'), + dcc.Graph(id='fig_deviation'), dcc.Graph(id='fig_hr'), dcc.Graph(id='fig_pace_bars') ]) @@ -2531,8 +2533,8 @@ def load_data(selected_file): # Dateipfad der ausgewählten Da Output('info-banner', 'children'), Output('fig-map', 'figure', allow_duplicate=True), Output('fig-elevation', 'figure'), - Output('fig_deviation', 'figure'), Output('fig_speed', 'figure'), + Output('fig_deviation', 'figure'), Output('fig_hr', 'figure'), Output('fig_pace_bars', 'figure'), # NEU: drei Pixel-Maps @@ -2550,8 +2552,8 @@ def update_all_plots(json_data): info = create_info_banner(df) fig_map = create_map_plot(df) fig_elev = create_elevation_plot(df) - fig_dev = create_deviation_plot(df) fig_speed = create_speed_plot(df) + fig_dev = create_deviation_plot(df) fig_hr = create_heart_rate_plot(df) fig_pace = create_pace_bars_plot(df) @@ -2560,7 +2562,7 @@ def update_all_plots(json_data): 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, + return (info, fig_map, fig_elev, fig_speed, fig_dev, fig_hr, fig_pace, fig_pixel_elevation, fig_pixel_pace, fig_pixel_hr) @@ -2621,7 +2623,7 @@ def update_pixel_heatmap(selected_file, toggle_value): n_city_runs=len(city_dfs), img_width=800, img_height=500, # ← NEU ) - city_info_text = f'Region {city_code} · Einzellauf · Count aus {len(city_dfs)} Läufen' + city_info_text = f'Region {city_code} · single run · Count of {len(city_dfs)} runs' else: # --- Region-Modus: alle Läufe anzeigen --- fig = create_pixel_heatmap( @@ -2631,7 +2633,7 @@ def update_pixel_heatmap(selected_file, toggle_value): n_city_runs=len(city_dfs), img_width=800, img_height=500, # ← NEU ) - city_info_text = f'Region {city_code} · {len(city_dfs)} Läufe geladen' + city_info_text = f'Region {city_code} · {len(city_dfs)} runs loaded' return fig, city_info_text