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 @@ - - - - - - - - Distance - 11.3 km - Time - 01:01:58 - Pace - 5:30 /km - 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:

- Description + Description

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