diff --git a/2025-09-15_HH_Run_10.90Km_overlay.png b/2025-09-15_HH_Run_10.90Km_overlay.png
deleted file mode 100644
index 912b062..0000000
Binary files a/2025-09-15_HH_Run_10.90Km_overlay.png and /dev/null differ
diff --git a/2025-09-15_HH_Run_10.90Km_overlay.svg b/2025-09-15_HH_Run_10.90Km_overlay.svg
deleted file mode 100644
index a1213b8..0000000
--- a/2025-09-15_HH_Run_10.90Km_overlay.svg
+++ /dev/null
@@ -1,112 +0,0 @@
-
-
diff --git a/2026-05-17_FRA_Run_12.02Km_overlay.png b/2026-05-17_FRA_Run_12.02Km_overlay.png
new file mode 100644
index 0000000..9d40bf1
Binary files /dev/null and b/2026-05-17_FRA_Run_12.02Km_overlay.png differ
diff --git a/2026-05-17_FRA_Run_12.02Km_overlay.svg b/2026-05-17_FRA_Run_12.02Km_overlay.svg
new file mode 100644
index 0000000..f73d178
--- /dev/null
+++ b/2026-05-17_FRA_Run_12.02Km_overlay.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Distance
+ 12.0 km
+ Pace
+ 6:11 /km
+ Time
+ 01:14:23
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ab929c1..dd4e85e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Interactive Python Dash app to visualize, analyze, and explore your jogging or r
SVG-Export:
-
+
This SVG can then be overlaid on another image (Strava‑inspired).
diff --git a/jogging_dashboard_browser_app.py b/jogging_dashboard_browser_app.py
index 177e577..5ca167f 100644
--- a/jogging_dashboard_browser_app.py
+++ b/jogging_dashboard_browser_app.py
@@ -622,136 +622,126 @@ def create_info_banner(df):
# EXPORT SUMMARY IMAGE (SVG)
# -----------------------------------------------------------------------------
def create_strava_style_svg(df, total_distance_km=None, total_time=None, avg_pace=None,
- width=800, height=600, padding=50):
+ padding_pct=0.12):
"""
Erstellt ein STRAVA-style SVG mit transparentem Hintergrund
"""
- # SVG Root Element
- svg = Element('svg')
- svg.set('width', str(width))
- svg.set('height', str(height))
- svg.set('xmlns', 'http://www.w3.org/2000/svg')
- svg.set('style', 'background: transparent;')
-
- # Route-Bereich (links 60% der Breite)
- route_width = width * 0.6
- route_height = height - 2 * padding
-
- # Koordinaten normalisieren für den Route-Bereich
lats = df['lat'].values
lons = df['lon'].values
- # Bounding Box der Route
lat_min, lat_max = lats.min(), lats.max()
lon_min, lon_max = lons.min(), lons.max()
+ lat_range = lat_max - lat_min if lat_max != lat_min else 1e-6
+ lon_range = lon_max - lon_min if lon_max != lon_min else 1e-6
- # Aspect Ratio beibehalten
- lat_range = lat_max - lat_min
- lon_range = lon_max - lon_min
+ # Ziel: Route-Bereich soll in etwa 500×500px passen (quadratisch)
+ # Skalierung: Route maximal einpassen, Seitenverhältnis erhalten
+ target_size = 500
+ scale_x = target_size / lon_range
+ scale_y = target_size / lat_range
+ scale = min(scale_x, scale_y)
- if lat_range == 0 or lon_range == 0:
- raise ValueError("Route hat keine Variation in Koordinaten")
+ # Tatsächliche Routengröße in Pixel
+ route_px_w = lon_range * scale
+ route_px_h = lat_range * scale
- # Skalierung berechnen
- scale_x = (route_width - 2 * padding) / lon_range
- scale_y = (route_height - 2 * padding) / lat_range
+ # Padding in Pixel (padding_pct × Routengröße)
+ pad = max(route_px_w, route_px_h) * padding_pct
- # Einheitliche Skalierung für korrekte Proportionen
- scale = min(scale_x, scale_y)
+ # Canvas für Route (tight + padding)
+ canvas_w = route_px_w + 2 * pad
+ canvas_h = route_px_h + 2 * pad
- # Zentrieren
- center_x = route_width / 2
- center_y = height / 2
+ # Stats-Bereich rechts: feste Breite 220px
+ stats_w = 220
+ total_w = canvas_w + stats_w
+ total_h = canvas_h
- # Route-Pfad erstellen
+ # Offset: Route zentriert im Canvas
+ offset_x = pad
+ offset_y = pad
+
+ def to_svg(lat, lon):
+ x = offset_x + (lon - lon_min) * scale
+ y = offset_y + (lat_max - lat) * scale
+ return x, y
+
+ # SVG Root
+ svg = Element('svg')
+ svg.set('width', f'{total_w:.0f}')
+ svg.set('height', f'{total_h:.0f}')
+ svg.set('xmlns', 'http://www.w3.org/2000/svg')
+ svg.set('style', 'background: transparent;')
+
+ # Route-Pfad
path_data = []
for i, (lat, lon) in enumerate(zip(lats, lons)):
- # Koordinaten transformieren (Y-Achse umkehren für SVG)
- x = center_x + (lon - (lon_min + lon_max) / 2) * scale
- y = center_y - (lat - (lat_min + lat_max) / 2) * scale
+ x, y = to_svg(lat, lon)
+ path_data.append(f"{'M' if i == 0 else 'L'} {x:.2f} {y:.2f}")
- if i == 0:
- path_data.append(f"M {x:.2f} {y:.2f}")
- else:
- path_data.append(f"L {x:.2f} {y:.2f}")
-
- # Route-Pfad zum SVG hinzufügen
route_path = SubElement(svg, 'path')
- route_path.set('d', ' '.join(path_data))
- route_path.set('stroke', '#ff6909') # Deine Routenfarbe
- route_path.set('stroke-width', '4')
- route_path.set('fill', 'none')
+ route_path.set('d', ' '.join(path_data))
+ route_path.set('stroke', '#ff6909')
+ route_path.set('stroke-width', '4')
+ route_path.set('fill', 'none')
route_path.set('stroke-linecap', 'round')
- route_path.set('stroke-linejoin', 'round')
+ route_path.set('stroke-linejoin','round')
# Start-Punkt (grün)
- start_x = center_x + (lons[0] - (lon_min + lon_max) / 2) * scale
- start_y = center_y - (lats[0] - (lat_min + lat_max) / 2) * scale
- start_circle = SubElement(svg, 'circle')
- start_circle.set('cx', str(start_x))
- start_circle.set('cy', str(start_y))
- start_circle.set('r', '8')
- start_circle.set('fill', '#4CAF50') # Grün
- start_circle.set('stroke', 'white')
- start_circle.set('stroke-width', '2')
+ sx, sy = to_svg(lats[0], lons[0])
+ sc = SubElement(svg, 'circle')
+ sc.set('cx', f'{sx:.2f}'); sc.set('cy', f'{sy:.2f}')
+ sc.set('r', '8'); sc.set('fill', '#4CAF50')
+ sc.set('stroke', 'white'); sc.set('stroke-width', '2')
# End-Punkt (rot)
- end_x = center_x + (lons[-1] - (lon_min + lon_max) / 2) * scale
- end_y = center_y - (lats[-1] - (lat_min + lat_max) / 2) * scale
- end_circle = SubElement(svg, 'circle')
- end_circle.set('cx', str(end_x))
- end_circle.set('cy', str(end_y))
- end_circle.set('r', '8')
- end_circle.set('fill', '#f44336') # Rot
- end_circle.set('stroke', 'white')
- end_circle.set('stroke-width', '2')
+ ex, ey = to_svg(lats[-1], lons[-1])
+ ec = SubElement(svg, 'circle')
+ ec.set('cx', f'{ex:.2f}'); ec.set('cy', f'{ey:.2f}')
+ ec.set('r', '8'); ec.set('fill', '#f44336')
+ ec.set('stroke', 'white'); ec.set('stroke-width', '2')
- # Stats-Bereich (rechts 40% der Breite)
- stats_x = route_width + padding
- stats_y_start = padding + 50
-
- ## Hintergrund für Stats (optional, semi-transparent - SCHWARZE BOX)
- #stats_bg = SubElement(svg, 'rect')
- #stats_bg.set('x', str(stats_x - 20))
- #stats_bg.set('y', str(stats_y_start - 30))
- #stats_bg.set('width', str(width * 0.35))
- #stats_bg.set('height', str(250))
- #stats_bg.set('fill', 'rgba(0,0,0,0.7)')
- #stats_bg.set('rx', '10')
-
- # Stats-Text hinzufügen
+ # Stats-Block: vertikal zentriert im rechten Bereich
stats = [
- ("TOTAL DISTANCE", f"{total_distance_km:.1f} km" if total_distance_km else "N/A"),
- ("TOTAL TIME", total_time or "N/A"),
- ("AVERAGE PACE", avg_pace or "N/A")
+ ("Distance", f"{total_distance_km:.1f} km" if total_distance_km else "N/A"),
+ ("Pace", avg_pace or "N/A"),
+ ("Time", total_time or "N/A"),
]
+ n_stats = len(stats)
+ entry_h = 90
+ block_h = n_stats * entry_h
+ stats_start_y = (total_h - block_h) / 2
+ stats_cx = canvas_w + stats_w / 2 # Mitte des Stats-Bereichs
+
for i, (label, value) in enumerate(stats):
- y_pos = stats_y_start + i * 70
+ y_pos = stats_start_y + i * entry_h
- # Label (kleinere Schrift, grau)
- label_text = SubElement(svg, 'text')
- label_text.set('x', str(stats_x))
- label_text.set('y', str(y_pos))
- label_text.set('font-family', 'Arial, sans-serif')
- label_text.set('font-size', '14')
- label_text.set('font-weight', 'bold')
- label_text.set('fill', '#000000') # TEXTFARBE #333333
- label_text.text = label
+ lbl = SubElement(svg, 'text')
+ lbl.set('x', f'{stats_cx:.1f}')
+ lbl.set('y', f'{y_pos:.1f}')
+ lbl.set('font-family', 'Arial, sans-serif')
+ lbl.set('font-size', '18px')
+ lbl.set('font-weight', 'bold')
+ lbl.set('fill', 'white')
+ lbl.set('text-anchor', 'middle')
+ lbl.text = label
- # Wert (größere Schrift, weiß)
- value_text = SubElement(svg, 'text')
- value_text.set('x', str(stats_x))
- value_text.set('y', str(y_pos + 25))
- value_text.set('font-family', 'Arial, sans-serif')
- value_text.set('font-size', '24')
- value_text.set('font-weight', 'bold')
- value_text.set('fill', 'white') # TEXTFARBE
- value_text.text = value
+ val = SubElement(svg, 'text')
+ val.set('x', f'{stats_cx:.1f}')
+ val.set('y', f'{y_pos + 40:.1f}')
+ val.set('font-family', 'Arial, sans-serif')
+ val.set('font-size', '34px')
+ val.set('font-weight', 'bold')
+ val.set('fill', 'white')
+ val.set('text-anchor', 'middle')
+ val.text = value
return svg
+
+
def save_svg(svg_element, filename="run_overlay.svg"):
"""SVG als Datei speichern"""
rough_string = tostring(svg_element, 'unicode')
@@ -2794,7 +2784,8 @@ def export_summary_image(n_clicks, json_data, selected_file):
# Statistiken berechnen (gleich wie im Info-Banner)
total_distance_km = df['cum_dist_km'].iloc[-1] if 'cum_dist_km' in df.columns else 0
- total_time_str = df['duration_hms'].iloc[-1] if 'duration_hms' in df.columns else "00:00:00"
+ total_time_raw = df['duration_hms'].iloc[-1] if 'duration_hms' in df.columns else "00:00:00"
+ total_time_str = str(total_time_raw).split(' ')[-1] if 'days' in str(total_time_raw) else str(total_time_raw)
total_seconds = df['time_diff_sec'].iloc[-1] if 'time_diff_sec' in df.columns else 0
# Pace berechnen
@@ -2812,8 +2803,7 @@ def export_summary_image(n_clicks, json_data, selected_file):
total_distance_km=total_distance_km,
total_time=total_time_str,
avg_pace=avg_pace,
- width=800,
- height=600
+ padding_pct=0.12
)
# SVG speichern