Updated SVG export function for a better total ratio order
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 243 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 43 KiB |
BIN
2026-05-17_FRA_Run_12.02Km_overlay.png
Normal file
BIN
2026-05-17_FRA_Run_12.02Km_overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
12
2026-05-17_FRA_Run_12.02Km_overlay.svg
Normal file
12
2026-05-17_FRA_Run_12.02Km_overlay.svg
Normal file
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ Interactive Python Dash app to visualize, analyze, and explore your jogging or r
|
|||||||
|
|
||||||
SVG-Export:
|
SVG-Export:
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="2025-09-15_HH_Run_10.90Km_overlay.png" alt="Description" width="400">
|
<img src="2026-05-17_FRA_Run_12.02Km_overlay.png" alt="Description" width="400">
|
||||||
</p>
|
</p>
|
||||||
This SVG can then be overlaid on another image (Strava‑inspired).
|
This SVG can then be overlaid on another image (Strava‑inspired).
|
||||||
|
|
||||||
|
|||||||
@@ -622,136 +622,126 @@ def create_info_banner(df):
|
|||||||
# EXPORT SUMMARY IMAGE (SVG)
|
# EXPORT SUMMARY IMAGE (SVG)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def create_strava_style_svg(df, total_distance_km=None, total_time=None, avg_pace=None,
|
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
|
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
|
lats = df['lat'].values
|
||||||
lons = df['lon'].values
|
lons = df['lon'].values
|
||||||
|
|
||||||
# Bounding Box der Route
|
|
||||||
lat_min, lat_max = lats.min(), lats.max()
|
lat_min, lat_max = lats.min(), lats.max()
|
||||||
lon_min, lon_max = lons.min(), lons.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
|
# Ziel: Route-Bereich soll in etwa 500×500px passen (quadratisch)
|
||||||
lat_range = lat_max - lat_min
|
# Skalierung: Route maximal einpassen, Seitenverhältnis erhalten
|
||||||
lon_range = lon_max - lon_min
|
target_size = 500
|
||||||
|
scale_x = target_size / lon_range
|
||||||
if lat_range == 0 or lon_range == 0:
|
scale_y = target_size / lat_range
|
||||||
raise ValueError("Route hat keine Variation in Koordinaten")
|
|
||||||
|
|
||||||
# Skalierung berechnen
|
|
||||||
scale_x = (route_width - 2 * padding) / lon_range
|
|
||||||
scale_y = (route_height - 2 * padding) / lat_range
|
|
||||||
|
|
||||||
# Einheitliche Skalierung für korrekte Proportionen
|
|
||||||
scale = min(scale_x, scale_y)
|
scale = min(scale_x, scale_y)
|
||||||
|
|
||||||
# Zentrieren
|
# Tatsächliche Routengröße in Pixel
|
||||||
center_x = route_width / 2
|
route_px_w = lon_range * scale
|
||||||
center_y = height / 2
|
route_px_h = lat_range * scale
|
||||||
|
|
||||||
# Route-Pfad erstellen
|
# Padding in Pixel (padding_pct × Routengröße)
|
||||||
|
pad = max(route_px_w, route_px_h) * padding_pct
|
||||||
|
|
||||||
|
# Canvas für Route (tight + padding)
|
||||||
|
canvas_w = route_px_w + 2 * pad
|
||||||
|
canvas_h = route_px_h + 2 * pad
|
||||||
|
|
||||||
|
# Stats-Bereich rechts: feste Breite 220px
|
||||||
|
stats_w = 220
|
||||||
|
total_w = canvas_w + stats_w
|
||||||
|
total_h = canvas_h
|
||||||
|
|
||||||
|
# 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 = []
|
path_data = []
|
||||||
for i, (lat, lon) in enumerate(zip(lats, lons)):
|
for i, (lat, lon) in enumerate(zip(lats, lons)):
|
||||||
# Koordinaten transformieren (Y-Achse umkehren für SVG)
|
x, y = to_svg(lat, lon)
|
||||||
x = center_x + (lon - (lon_min + lon_max) / 2) * scale
|
path_data.append(f"{'M' if i == 0 else 'L'} {x:.2f} {y:.2f}")
|
||||||
y = center_y - (lat - (lat_min + lat_max) / 2) * scale
|
|
||||||
|
|
||||||
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 = SubElement(svg, 'path')
|
||||||
route_path.set('d', ' '.join(path_data))
|
route_path.set('d', ' '.join(path_data))
|
||||||
route_path.set('stroke', '#ff6909') # Deine Routenfarbe
|
route_path.set('stroke', '#ff6909')
|
||||||
route_path.set('stroke-width', '4')
|
route_path.set('stroke-width', '4')
|
||||||
route_path.set('fill', 'none')
|
route_path.set('fill', 'none')
|
||||||
route_path.set('stroke-linecap', 'round')
|
route_path.set('stroke-linecap', 'round')
|
||||||
route_path.set('stroke-linejoin','round')
|
route_path.set('stroke-linejoin','round')
|
||||||
|
|
||||||
# Start-Punkt (grün)
|
# Start-Punkt (grün)
|
||||||
start_x = center_x + (lons[0] - (lon_min + lon_max) / 2) * scale
|
sx, sy = to_svg(lats[0], lons[0])
|
||||||
start_y = center_y - (lats[0] - (lat_min + lat_max) / 2) * scale
|
sc = SubElement(svg, 'circle')
|
||||||
start_circle = SubElement(svg, 'circle')
|
sc.set('cx', f'{sx:.2f}'); sc.set('cy', f'{sy:.2f}')
|
||||||
start_circle.set('cx', str(start_x))
|
sc.set('r', '8'); sc.set('fill', '#4CAF50')
|
||||||
start_circle.set('cy', str(start_y))
|
sc.set('stroke', 'white'); sc.set('stroke-width', '2')
|
||||||
start_circle.set('r', '8')
|
|
||||||
start_circle.set('fill', '#4CAF50') # Grün
|
|
||||||
start_circle.set('stroke', 'white')
|
|
||||||
start_circle.set('stroke-width', '2')
|
|
||||||
|
|
||||||
# End-Punkt (rot)
|
# End-Punkt (rot)
|
||||||
end_x = center_x + (lons[-1] - (lon_min + lon_max) / 2) * scale
|
ex, ey = to_svg(lats[-1], lons[-1])
|
||||||
end_y = center_y - (lats[-1] - (lat_min + lat_max) / 2) * scale
|
ec = SubElement(svg, 'circle')
|
||||||
end_circle = SubElement(svg, 'circle')
|
ec.set('cx', f'{ex:.2f}'); ec.set('cy', f'{ey:.2f}')
|
||||||
end_circle.set('cx', str(end_x))
|
ec.set('r', '8'); ec.set('fill', '#f44336')
|
||||||
end_circle.set('cy', str(end_y))
|
ec.set('stroke', 'white'); ec.set('stroke-width', '2')
|
||||||
end_circle.set('r', '8')
|
|
||||||
end_circle.set('fill', '#f44336') # Rot
|
|
||||||
end_circle.set('stroke', 'white')
|
|
||||||
end_circle.set('stroke-width', '2')
|
|
||||||
|
|
||||||
# Stats-Bereich (rechts 40% der Breite)
|
# Stats-Block: vertikal zentriert im rechten Bereich
|
||||||
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 = [
|
stats = [
|
||||||
("TOTAL DISTANCE", f"{total_distance_km:.1f} km" if total_distance_km else "N/A"),
|
("Distance", f"{total_distance_km:.1f} km" if total_distance_km else "N/A"),
|
||||||
("TOTAL TIME", total_time or "N/A"),
|
("Pace", avg_pace or "N/A"),
|
||||||
("AVERAGE 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):
|
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)
|
lbl = SubElement(svg, 'text')
|
||||||
label_text = SubElement(svg, 'text')
|
lbl.set('x', f'{stats_cx:.1f}')
|
||||||
label_text.set('x', str(stats_x))
|
lbl.set('y', f'{y_pos:.1f}')
|
||||||
label_text.set('y', str(y_pos))
|
lbl.set('font-family', 'Arial, sans-serif')
|
||||||
label_text.set('font-family', 'Arial, sans-serif')
|
lbl.set('font-size', '18px')
|
||||||
label_text.set('font-size', '14')
|
lbl.set('font-weight', 'bold')
|
||||||
label_text.set('font-weight', 'bold')
|
lbl.set('fill', 'white')
|
||||||
label_text.set('fill', '#000000') # TEXTFARBE #333333
|
lbl.set('text-anchor', 'middle')
|
||||||
label_text.text = label
|
lbl.text = label
|
||||||
|
|
||||||
# Wert (größere Schrift, weiß)
|
val = SubElement(svg, 'text')
|
||||||
value_text = SubElement(svg, 'text')
|
val.set('x', f'{stats_cx:.1f}')
|
||||||
value_text.set('x', str(stats_x))
|
val.set('y', f'{y_pos + 40:.1f}')
|
||||||
value_text.set('y', str(y_pos + 25))
|
val.set('font-family', 'Arial, sans-serif')
|
||||||
value_text.set('font-family', 'Arial, sans-serif')
|
val.set('font-size', '34px')
|
||||||
value_text.set('font-size', '24')
|
val.set('font-weight', 'bold')
|
||||||
value_text.set('font-weight', 'bold')
|
val.set('fill', 'white')
|
||||||
value_text.set('fill', 'white') # TEXTFARBE
|
val.set('text-anchor', 'middle')
|
||||||
value_text.text = value
|
val.text = value
|
||||||
|
|
||||||
return svg
|
return svg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def save_svg(svg_element, filename="run_overlay.svg"):
|
def save_svg(svg_element, filename="run_overlay.svg"):
|
||||||
"""SVG als Datei speichern"""
|
"""SVG als Datei speichern"""
|
||||||
rough_string = tostring(svg_element, 'unicode')
|
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)
|
# 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_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
|
total_seconds = df['time_diff_sec'].iloc[-1] if 'time_diff_sec' in df.columns else 0
|
||||||
|
|
||||||
# Pace berechnen
|
# Pace berechnen
|
||||||
@@ -2812,8 +2803,7 @@ def export_summary_image(n_clicks, json_data, selected_file):
|
|||||||
total_distance_km=total_distance_km,
|
total_distance_km=total_distance_km,
|
||||||
total_time=total_time_str,
|
total_time=total_time_str,
|
||||||
avg_pace=avg_pace,
|
avg_pace=avg_pace,
|
||||||
width=800,
|
padding_pct=0.12
|
||||||
height=600
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# SVG speichern
|
# SVG speichern
|
||||||
|
|||||||
Reference in New Issue
Block a user