LRWise/Data_Visualization_TdF_Winner
0
1import marimo2 3__generated_with = "0.11.26"4app = marimo.App(width="medium")5 6 7@app.cell8def _():9 import marimo as mo10 import pandas as pd11 import svg12 import plotly.express as px13 import plotly.graph_objects as go14 import math15 return go, math, mo, pd, px, svg16 17 18@app.cell19def _(pd):20 winners = pd.read_csv("Data/tdf_winners.csv")21 df_stage = pd.read_csv("Data/stage_data.csv")22 df_stage_winner = pd.read_csv("Data/tdf_stages.csv")23 # This is needed to find geolocations.24 # unique_locations25 #client_ors = ors.Client(key='5b3ce3597851110001cf624802e069d6633748a5ae4e9842334f1dc2')26 #df_final = get_coordinates_for_locations(client_ors, unique_locations)27 #import time28 29 #def get_coordinates_for_locations(client, unique_locations, sleep = 0.5):30 # results = []31 32 # for location in unique_locations:33 # response = client.pelias_search(text=location)34 35 # Get the first result if available36 # features = response.get("features", [])37 # if features:38 # coords = features[0]["geometry"]["coordinates"] # [lon, lat]39 # results.append({40 # "location": location,41 # "longitude": coords[0],42 # "latitude": coords[1],43 # "coordinates": coords44 # })45 # else:46 # results.append({47 # "location": location,48 # "longitude": None,49 # "latitude": None,50 # "coordinates": None51 # })52 # time.sleep(sleep)53 54 # return pd.DataFrame(results)55 56 df_stage_final = pd.read_excel("Data/stage_information.xlsx")57 return df_stage, df_stage_final, df_stage_winner, winners58 59 60@app.cell61def _(pd, winners):62 nationality_info = {63 "\xa0Luxembourg": (["#EA141D", "#FFFFFF", "#51ADDA"], "up_down"),64 "\xa0France": (["#002395", "#FFFFFF", "#ED2939"], "left_right"),65 "\xa0Belgium": (["#000000", "#FAE042", "#ED2939"], "left_right"),66 "\xa0Italy":(["#009246", "#ffffff", "#ce2b37"], "left_right"),67 "\xa0\xa0Switzerland":(["#ff0000", "#ffffff", "#ff0000"], "center"),68 "\xa0Spain": (["#C60B1E", "#FFC400", "#C60B1E"], "up_down"),69 "\xa0Netherlands": (["#AE1C28", "#FFFFFF", "#21468B"], "up_down"),70 "\xa0United States": (["#3C3B6E", "#FFFFFF", "#B22234"], "left_right"),71 "\xa0Ireland": (["#169B62", "#FFFFFF", "#FF883E"], "left_right"),72 "\xa0Denmark": (["#C60C30", "#FFFFFF", "#C60C30"], "up_down"),73 "\xa0Germany": (["#000000", "#DD0000", "#FFCE00"], "up_down"),74 "\xa0Australia": (["#012169", "#FFFFFF", "#E4002B"], "UK"),75 "\xa0Great Britain": (["#00247D", "#FFFFFF", "#CF142B"], "UK"),76 "\xa0Colombia": (["#FCD116", "#003893", "#CE1126"], "up_down"), 77 }78 # Map in a single step using .apply()79 winners[["colors", "flag_type"]] = winners["nationality"].apply(80 lambda x: pd.Series(nationality_info.get(x, (["#FFFFFF", "#FFFFFF", "#FFFFFF"], "unknown")))81 )82 83 new_winners = winners["start_date"].str.split("-", n=2, expand=True)84 new_winners = new_winners.apply(pd.to_numeric, errors='coerce') 85 new_winners['TIME'] = new_winners[0]86 winners['Year'] = new_winners['TIME']87 88 winners.dropna(subset=["colors"], inplace=True)89 return nationality_info, new_winners90 91 92@app.cell93def _(df_stage_final, df_stage_winner, pd):94 new_stage_winner = df_stage_winner["Date"].str.split("-", n=2, expand=True)95 new_stage_winner = new_stage_winner.apply(pd.to_numeric, errors='coerce') 96 new_stage_winner['TIME'] = new_stage_winner[0]97 df_stage_winner['Year'] = new_stage_winner['TIME']98 99 result_tdf = df_stage_winner.merge(df_stage_final, how='left', left_on='Origin', right_on='location').drop('location', axis=1)100 result_tdf = result_tdf.merge(df_stage_final, how='left', left_on='Destination', right_on='location', suffixes=('_Origin', '_Destination')).drop('location', axis=1)101 return new_stage_winner, result_tdf102 103 104 105 106@app.cell107def _(go, pd, px):108 def plot_route_type(year, result_tdf, winners_df):109 df = result_tdf[result_tdf["Year"] == year].copy()110 df = df.sort_values("Stage")111 overall_winner = pd.unique(winners_df["Overall_Winner"][winners_df["Year"]==year])[0]112 113 fig = go.Figure()114 115 type_colors = {116 stage_type: color117 for stage_type, color in zip(118 df["Type"].unique(), px.colors.qualitative.Set1119 )120 }121 122 plotted_types = set()123 124 for stage_type in df["Type"].unique():125 fig.add_trace(go.Scattergeo(126 lon=[None], # Invisible trace127 lat=[None],128 mode="lines",129 line=dict(width=3, color=type_colors[stage_type], dash="solid"),130 name=stage_type,131 showlegend=True132 ))133 134 for idx, row in df.iterrows():135 stage_type = row["Type"]136 showlegend = False # We handle legend with dummy traces above137 138 139 140 is_overall_winner = row["Winner"] == overall_winner141 line_style = dict(142 width=3 if is_overall_winner else 2,143 color=type_colors[stage_type],144 dash="solid" if is_overall_winner else "dot"145 )146 147 fig.add_trace(go.Scattergeo(148 lon=[row["longitude_Origin"], row["longitude_Destination"]],149 lat=[row["latitude_Origin"], row["latitude_Destination"]],150 mode="lines",151 line=line_style,152 hoverinfo="text",153 text=(154 f"Stage {row['Stage']}: {row['Origin']} → {row['Destination']}<br>"155 f"Type: {row['Type']}<br>Winner: {row['Winner']}<br>Distance: {row['Distance']} km"156 ),157 name=stage_type,158 showlegend=showlegend159 ))160 161 # Origin and Destination markers (unchanged)162 for lon, lat, color in [163 (row["longitude_Origin"], row["latitude_Origin"], "gold"),164 (row["longitude_Destination"], row["latitude_Destination"], "darkorange")165 ]:166 fig.add_trace(go.Scattergeo(167 lon=[lon],168 lat=[lat],169 mode="markers",170 marker=dict(size=10, color=color),171 text=(172 f"Stage {row['Stage']}: {row['Origin']} → {row['Destination']}<br>"173 f"Type: {row['Type']}<br>Winner: {row['Winner']}<br>Distance: {row['Distance']} km"174 ),175 hoverinfo="text",176 showlegend=False177 ))178 179 plotted_types.add(stage_type)180 181 # Layout styling182 fig.update_layout(183 title=f"Tour de France Route - {year}",184 title_x=0.5,185 width=570,186 height=500,187 margin=dict(t=50, b=50, l=10, r=10), # slightly larger bottom margin188 geo=dict(189 scope="europe",190 projection_type="mollweide",191 projection_scale=5,192 center=dict(lat=46.2, lon=2.87),193 landcolor="white",194 oceancolor="lightblue",195 showocean=True,196 bgcolor="#F0F0F0"197 ),198 paper_bgcolor="rgba(0,0,0,0)",199 plot_bgcolor="rgba(0,0,0,0)",200 legend=dict(201 orientation="h",202 yanchor="top",203 y=-0.2,204 xanchor="center",205 x=0.5206 )207 )208 209 return fig210 return (plot_route_type,)211 212 213@app.cell214def _(go, pd):215 def winner_data(df_stage, df_stage_winner, winners):216 # Load your datasets (update file paths accordingly)217 stages_df = df_stage # Contains rider, stage ID, time diff218 stages_df["rank"] = pd.to_numeric(stages_df["rank"], errors='coerce')219 stages_df["Year"] = stages_df["year"]220 winners_df = df_stage_winner # Contains stage ID, Type, Distance, Winner221 overall_df = winners # Contains overall winners per year222 223 winners_df = winners_df.merge(overall_df, on="Year", how="left")224 winners_df = winners_df.merge(overall_df, on="Year", how="left")225 226 227 # Rename relevant columns for clarity228 winners_df.rename(columns={"winner_name_y": "Overall_Winner"}, inplace=True)229 230 # Keep only the relevant columns231 winners_df = winners_df[["Year", "Stage", "Type", "Winner", "Distance", "Overall_Winner", "Winner_Country"]]232 233 return winners_df234 235 def type_winner(df_stage, winners_df):236 237 stages_df = df_stage238 # Keep top 10 riders per stage239 stages_top = stages_df.loc[stages_df["rank"] <= 10].copy()240 stages_top = stages_top[stages_top["Year"]<2018]241 242 def normalize_name(name):243 return " ".join(sorted(name.strip().lower().split()))244 245 stages_top["rider_clean"] = stages_top["rider"].apply(normalize_name)246 stages_top["Year"] = stages_top["year"]247 winners_df_top = winners_df.copy()248 winners_df_top["rider_clean"] = winners_df_top["Overall_Winner"].apply(normalize_name)249 winners_df_top["winner_clean"] = winners_df_top["Overall_Winner"].apply(normalize_name)250 # Merge with winners_df (to get overall winner)251 252 # Merge on both rider name and edition253 stages_winners_merge = stages_top.merge(254 winners_df_top,255 on=["rider_clean", "Year"],256 how="inner"257 )258 259 new_stage_winner_merge = stages_winners_merge["stage_results_id"].str.split("-", n=2, expand=True)260 new_stage_winner_merge = new_stage_winner_merge.apply(pd.to_numeric, errors='coerce') 261 new_stage_winner_merge['STAGE'] = new_stage_winner_merge[1]262 stages_winners_merge['stage'] = new_stage_winner_merge['STAGE']263 stages_winners_merge["Stage"] = pd.to_numeric(stages_winners_merge["Stage"], errors='coerce')264 265 filtered_stage_wins_df = stages_winners_merge[266 stages_winners_merge["stage"] == stages_winners_merge["Stage"]267 ]268 269 270 filtered_stage_wins_df = filtered_stage_wins_df[271 filtered_stage_wins_df["rider_clean"] == filtered_stage_wins_df["winner_clean"]272 ]273 274 # Drop duplicates per edition-stage to avoid multiple rows if the winner placed top 3 more than once275 filtered_stage_wins_df = filtered_stage_wins_df.drop_duplicates(subset=["edition", "Stage"])276 filtered_stage_wins_df = filtered_stage_wins_df.drop_duplicates(subset=["edition", "Stage"])277 278 type_counts_per_year = filtered_stage_wins_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)279 280 simplified_counts_df = type_counts_per_year.copy()281 282 # Combine relevant columns into broader categories283 simplified_counts_df["Time trial"] = (284 simplified_counts_df.get("Individual time trial", 0) +285 simplified_counts_df.get("Team time trial", 0) +286 simplified_counts_df.get("Mountain time trial", 0)287 )288 289 simplified_counts_df["Flat stage"] = (290 simplified_counts_df.get("Flat Stage", 0) +291 simplified_counts_df.get("Flat stage", 0) +292 simplified_counts_df.get("Flat cobblestone stage", 0) +293 simplified_counts_df.get("Plain stage", 0) +294 simplified_counts_df.get("Plain stage with cobblestones", 0)295 )296 297 simplified_counts_df["Intermediate stage"] = (298 simplified_counts_df.get("Transition stage", 0) +299 simplified_counts_df.get("Half Stage", 0) # Optional, if it exists300 )301 302 simplified_counts_df["Mountain stage"] = (303 simplified_counts_df.get("Mountain stage", 0) +304 simplified_counts_df.get("Mountain Stage", 0) +305 simplified_counts_df["Hilly stage"] +306 simplified_counts_df.get("High mountain stage", 0) +307 simplified_counts_df.get("Medium mountain stage", 0) +308 simplified_counts_df.get("Stage with mountain", 0) +309 simplified_counts_df.get("Stage with mountain(s)", 0)310 )311 312 # Keep only the new simplified columns313 simplified_counts_df = simplified_counts_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]314 315 # Optional: Reset index if needed316 simplified_counts_df = simplified_counts_df.reset_index()317 return simplified_counts_df318 319 def simplified_stage_types_df(winners_df):320 types_per_year = winners_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)321 simplified_stage_types_df = types_per_year.copy()322 323 # === Simplified categories ===324 325 # 1. Time Trials (individual, team, mountain)326 simplified_stage_types_df["Time trial"] = (327 simplified_stage_types_df["Individual time trial"] +328 simplified_stage_types_df["Team time trial"] +329 simplified_stage_types_df.get("Mountain time trial", 0)330 )331 # 2. Flat stages (various naming variants + cobblestones)332 simplified_stage_types_df["Flat stage"] = (333 simplified_stage_types_df["Flat Stage"] +334 simplified_stage_types_df["Flat stage"] +335 simplified_stage_types_df["Flat cobblestone stage"] +336 simplified_stage_types_df["Plain stage"] +337 simplified_stage_types_df["Plain stage with cobblestones"]338 )339 340 simplified_stage_types_df["Intermediate stage"] = (341 simplified_stage_types_df["Transition stage"] +342 simplified_stage_types_df.get("Half Stage", 0)343 )344 345 simplified_stage_types_df["Mountain stage"] = (346 simplified_stage_types_df["Mountain stage"] +347 simplified_stage_types_df["High mountain stage"] +348 simplified_stage_types_df["Medium mountain stage"] +349 simplified_stage_types_df["Mountain Stage"] +350 simplified_stage_types_df["Hilly stage"] +351 simplified_stage_types_df.get("Stage with mountain", 0) +352 simplified_stage_types_df.get("Stage with mountain(s)", 0)353 )354 355 # Keep only the new simplified columns356 simplified_stage_types_df = simplified_stage_types_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]357 simplified_stage_types_df = simplified_stage_types_df.reset_index()358 simplified_stage_types_df["year"] = simplified_stage_types_df["Year"]359 360 # Optional: Reset index if needed361 simplified_stage_types_df = simplified_stage_types_df.reset_index()362 363 return simplified_stage_types_df364 365 366 def percentage_type_winner(df_stage, winners_df):367 simplified_counts_df = type_winner(df_stage, winners_df)368 types_per_year = winners_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)369 370 simplified_stage_types_df = types_per_year.copy()371 372 # === Simplified categories ===373 374 # 1. Time Trials (individual, team, mountain)375 simplified_stage_types_df["Time trial"] = (376 simplified_stage_types_df["Individual time trial"] +377 simplified_stage_types_df["Team time trial"] +378 simplified_stage_types_df.get("Mountain time trial", 0)379 )380 381 # 2. Flat stages (various naming variants + cobblestones)382 simplified_stage_types_df["Flat stage"] = (383 simplified_stage_types_df["Flat Stage"] +384 simplified_stage_types_df["Flat stage"] +385 simplified_stage_types_df["Flat cobblestone stage"] +386 simplified_stage_types_df["Plain stage"] +387 simplified_stage_types_df["Plain stage with cobblestones"]388 )389 390 simplified_stage_types_df["Intermediate stage"] = (391 simplified_stage_types_df["Transition stage"] +392 simplified_stage_types_df.get("Half Stage", 0)393 )394 395 simplified_stage_types_df["Mountain stage"] = (396 simplified_stage_types_df["Mountain stage"] +397 simplified_stage_types_df["High mountain stage"] +398 simplified_stage_types_df["Medium mountain stage"] +399 simplified_stage_types_df["Mountain Stage"] +400 simplified_stage_types_df["Hilly stage"] +401 simplified_stage_types_df.get("Stage with mountain", 0) +402 simplified_stage_types_df.get("Stage with mountain(s)", 0)403 )404 405 406 407 408 # Keep only the new simplified columns409 simplified_stage_types_df = simplified_stage_types_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]410 simplified_stage_types_df = simplified_stage_types_df.reset_index()411 simplified_stage_types_df["year"] = simplified_stage_types_df["Year"]412 413 # Optional: Reset index if needed414 simplified_stage_types_df = simplified_stage_types_df.reset_index()415 416 merged_counts = simplified_counts_df.merge(simplified_stage_types_df, on="Year", how="left")417 418 percentage_win_df = pd.DataFrame()419 percentage_win_df["year"] = merged_counts["year"]420 421 percentage_win_df["Time trial"] = (merged_counts["Time trial_x"]/merged_counts["Time trial_y"])*100422 percentage_win_df["Flat stage"] = (merged_counts["Flat stage_x"]/merged_counts["Flat stage_y"])*100423 percentage_win_df["Intermediate stage"] = (merged_counts["Intermediate stage_x"]/merged_counts["Intermediate stage_y"])*100424 percentage_win_df["Mountain stage"] = (merged_counts["Mountain stage_x"]/merged_counts["Mountain stage_y"])*100425 426 return percentage_win_df427 428 def visualize(year_selected, winners_df):429 # Filter dataset for the selected year430 filtered_stages = winners_df[winners_df["Year"] == year_selected]431 432 # Create unique labels for nodes433 stage_types = filtered_stages["Type"].unique().tolist()434 stage_winners = filtered_stages["Winner"].unique().tolist()435 overall_winner = filtered_stages["Overall_Winner"].unique().tolist()436 437 all_labels = stage_types + stage_winners438 439 # Map labels to indices440 node_indices = {label: i for i, label in enumerate(all_labels)}441 442 source_nodes = []443 target_nodes = []444 values = []445 colors = []446 447 # National colors448 country_colors = {449 "GBR": "#00247D", "GER": "#000000", "SVK": "#003897", "FRA": "#0055A4", "ITA": "#008C45",450 "COL": "#FCD116", "AUS": "#00008B", "NED": "#21468B", "SLO": "#005DA4", "NOR": "#BA0C2F",451 "POL": "#DC143C", "BEL": "#FFD700", "RUS": "#0033A0", "ESP": "#AA151B", "CZE": "#11457E",452 "LTU": "#FDB913", "IRL": "#169B62", "POR": "#006600", "SUI": "#D52B1E", "USA": "#B22234",453 "LUX": "#00A1DE", "KAZ": "#00BFFF", "DEN": "#C60C30", "RSA": "#007847", "UKR": "#0057B7",454 "AUT": "#ED2939", "EST": "#0072CE", "SWE": "#005EB8", "UZB": "#1EB53A", "LAT": "#A4343A",455 "IRE": "#169B62", "URS": "#FF0000", "BRA": "#009C3B", "MEX": "#006847", "GDR": "#FFCE00",456 "CAN": "#FF0000", "FRG": "#000000",457 'c("FRA", "FRA")': "#0055A4", 'c("BEL", "GER")': "#FFD700", 'c("BEL", "BEL")': "#FFD700",458 float("nan"): "rgba(160,160,160,0.6)",459 }460 461 winner_nationality_map = winners_df.drop_duplicates("Winner").set_index("Winner")["Winner_Country"].to_dict()462 node_colors = ["rgba(180,180,180,0.8)"] * len(all_labels)463 464 # Assign colors to winner nodes465 for winner in stage_winners:466 if winner in winner_nationality_map:467 nationality = winner_nationality_map[winner]468 color = country_colors.get(nationality, "rgba(180,180,180,0.8)")469 node_colors[node_indices[winner]] = color470 471 # Create Sankey links472 for _, row2 in filtered_stages.iterrows():473 source_nodes.append(node_indices[row2["Type"]])474 target_nodes.append(node_indices[row2["Winner"]])475 values.append(row2["Distance"])476 colors.append("rgba(255, 223, 0, 0.8)" if row2["Winner"] == row2["Overall_Winner"] else "rgba(128, 128, 128, 0.6)")477 478 # Sankey diagram479 fig = go.Figure(go.Sankey(480 node=dict(481 pad=15,482 thickness=20,483 line=dict(color="black", width=0.5),484 label=all_labels,485 color=node_colors486 ),487 link=dict(488 source=source_nodes,489 target=target_nodes,490 value=values,491 color=colors492 )493 ))494 495 # --- ADD LEGEND BELOW ---496 used_countries = {497 winner_nationality_map[w]: country_colors.get(winner_nationality_map[w], "#aaa")498 for w in stage_winners499 if w in winner_nationality_map and pd.notna(winner_nationality_map[w])500 }501 502 # Now safe to sort503 used_countries = dict(sorted(used_countries.items()))504 505 506 # Adjust layout to make room for legend507 fig.update_layout(508 autosize=True,509 margin=dict(t=50, b=20, l=10, r=10),510 width = 430,511 height=500,512 font_size=10,513 title_text="Stage types and the stage winners",514 title_x=0.5,515 )516 517 return fig, used_countries518 return (519 percentage_type_winner,520 simplified_stage_types_df,521 type_winner,522 visualize,523 winner_data,524 )525 526 527@app.cell528def _(math, pd, svg):529 # Class that creates a bike svg changing color depending on the riders nationality530 class Bike():531 def __init__(self, row, win_type, type_winner_abs, simplified_stage_types_df):532 if row is not None:# and not row.empty:533 self.flag_type = row["flag_type"]534 colors = row["colors"]535 self.color1 = colors[0]536 self.color2 = colors[1]537 self.color3 = colors[2]538 self.row = row539 win_type = win_type540 win_type["Year"] = pd.to_numeric(win_type["Year"])541 self.perc_flat = win_type[win_type["Year"]==row["Year"]]["Flat stage"]542 self.perc_mountain = win_type[win_type["Year"]==row["Year"]]["Mountain stage"]543 self.type_winner_abs = type_winner_abs544 self.simplified_stage_types_df = simplified_stage_types_df545 546 if pd.isna(row["height"]):547 self.size = 1.8548 else:549 self.size = row["height"]550 551 552 def _title(self):553 return svg.Title(elements=[f'Winner {self.row["Year"]}: {self.row["winner_name"]}, stage wins: {self.row["stage_wins"]}, team: {self.row["winner_team"]}, nationality: {self.row["nationality"]}'])554 555 def pie_chart_slices(self, cx, cy, r, data, colors, rotate_deg=0):556 paths = []557 start_angle = rotate_deg558 for label, value in data.items():559 if value == 0:560 continue561 562 if value == 1.0:563 # Full circle edge case564 paths.append(svg.Circle(565 cx=cx,566 cy=cy,567 r=r,568 fill=colors[label]569 ))570 continue571 572 end_angle = start_angle + 360 * value573 x1 = cx + r * math.cos(math.radians(start_angle))574 y1 = cy + r * math.sin(math.radians(start_angle))575 x2 = cx + r * math.cos(math.radians(end_angle))576 y2 = cy + r * math.sin(math.radians(end_angle))577 large_arc = 1 if (end_angle - start_angle) > 180 else 0578 path_d = f"M{cx},{cy} L{x1},{y1} A{r},{r} 0 {large_arc},1 {x2},{y2} Z"579 paths.append(svg.Path(d=path_d, fill=colors[label]))580 start_angle = end_angle581 return paths582 583 def front_wheel_pie(self):584 S = self.size585 if self.flag_type == "center" or self.flag_type=="UK":586 fill_color = self.color1587 stroke_color = self.color1588 elif self.flag_type == "left_right" or self.flag_type == "up_down":589 fill_color = self.color3590 stroke_color = self.color3591 592 if fill_color.lower() in ["#ffffff", "white"]:593 stroke_color = "#000000"594 595 596 cx=83.86574 * S597 cy=201.976 * S598 r = 23.692965 * S599 600 year = self.row["Year"]601 602 # Filter for the year and sum stage type columns603 winner_row = self.type_winner_abs[self.type_winner_abs["Year"] == year]604 stage_totals = self.simplified_stage_types_df[self.simplified_stage_types_df["Year"] == year]605 606 relevant_cols = ["Time trial", "Mountain stage", "Intermediate stage", "Flat stage"]607 608 if not winner_row.empty and not stage_totals.empty:609 winner_count = winner_row[relevant_cols].sum().sum()610 total_count = stage_totals[relevant_cols].sum().sum()611 if total_count > 0:612 data = {613 "Top 10": winner_count / total_count614 #"Other stages": 1 - (winner_count / total_count)615 }616 else:617 data = {"Unknown": 1}618 else:619 data = {"Unknown": 1}620 621 colors = {622 "Top 10": fill_color,623 #"Other stages": "#ffffff",624 "Unknown": "#e0e0e0"625 }626 # Draw pie slices627 # Pie chart slices628 pie_slices = self.pie_chart_slices(cx, cy, r, data, colors)629 630 # Pie + outline rotated group631 wheel_group = svg.G(632 elements=pie_slices + [633 svg.Circle(634 cx=cx,635 cy=cy,636 r=r,637 fill="none",638 stroke=fill_color,639 stroke_width=2.61407 * S640 )641 ],642 transform="rotate(-20.154323)"643 )644 645 # Legend646 legend_x = cx + 85 * S647 legend_y = cy - 65 * S648 box_size = 7 * S649 spacing = 10 * S650 651 if "Top 10" in data and data["Top 10"] > 0:652 percent = round(data["Top 10"] * 100)653 tooltip_text = f'{self.row["winner_name"]} won {percent}% of all stages'654 else:655 tooltip_text = "No stage win data available"656 657 return svg.G(elements=[658 wheel_group,659 svg.Title(elements=[tooltip_text])660 ])661 662 def head(self):663 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0664 S = self.size665 if (self.flag_type == "center" or self.flag_type == "up_down" or self.flag_type=="UK"):666 fill_color = self.color1667 stroke_color = self.color1668 elif (self.flag_type == "left_right"):669 fill_color = self.color3670 stroke_color = self.color3671 672 if fill_color.lower() in ["#ffffff", "white"]:673 stroke_color = "#000000"674 675 return svg.Circle(676 cx=152.73752 * S, cy=114.65292 * S, r=8.827405 * S,677 stroke=stroke_color,678 fill=fill_color, # Corrected color format679 stroke_width=2.61407 * S,680 elements = [self._title()]681 )682 683 def body(self):684 # center color = 0,0; UK=2,1 ;left_right=1,1 ; up_down=0,0685 S = self.size686 if self.flag_type == "center" or self.flag_type == "up_down":687 fill_color = self.color1688 stroke_color = self.color1689 elif self.flag_type == "UK":690 fill_color = self.color3691 stroke_color = self.color2692 elif self.flag_type == "left_right":693 fill_color = self.color2694 stroke_color = self.color2695 696 if fill_color.lower() in ["#ffffff", "white"]:697 stroke_color = "#000000"698 699 return svg.Path(d=[svg.M(95.986494 * S, 110.4811 * S), svg.L(140.43661 * S, 111.44741 * S), 700 svg.V(124.3315 * S), svg.H(108.87058 * S),],701 fill=fill_color,702 stroke=stroke_color,703 stroke_width=1 * S,704 elements = [self._title()]705 )706 707 def arm(self):708 # up_down =1,1; left_right=1,1 ;UK=2,1 ; center=1,0709 S = self.size710 711 if self.flag_type == "up_down" or self.flag_type == "left_right":712 fill_color = self.color2713 stroke_color = self.color2714 elif self.flag_type == "UK":715 fill_color = self.color3716 stroke_color = self.color2717 elif self.flag_type == "center":718 fill_color = self.color2719 stroke_color = self.color1720 721 if fill_color.lower() in ["#ffffff", "white"]:722 stroke_color = "#000000"723 724 return svg.Path(d=[svg.M(140.27555 * S, 124.29123 * S), svg.L(130.77354 * S, 133.02826 * S), 725 svg.L(144.94604 * S, 144.94604 * S), svg.L(139.7924 * S, 148.81127 * S),726 svg.L(121.43257 * S, 133.67246 * S), svg.L(130.12934 * S, 124.08991 * S),],727 fill=fill_color,728 stroke=stroke_color,729 stroke_width=1 * S,730 elements = [self._title()]731 )732 733 734 def left_leg(self):735 S = self.size736 if self.flag_type == "up_down" or self.flag_type == "left_right":737 fill_color = self.color2738 stroke_color = self.color2739 elif self.flag_type == "UK":740 fill_color = self.color3741 stroke_color = self.color2742 elif self.flag_type == "center":743 fill_color = self.color1744 stroke_color = self.color1745 746 if fill_color.lower() in ["#ffffff", "white"]:747 stroke_color = "#000000"748 749 return svg.Path(750 d=[751 svg.M(94.532861 * S, 112.61697 * S),752 svg.L(94.524268 * S, 172.02704 * S),753 svg.L(101.4425 * S, 172.02704 * S),754 svg.L(101.4283 * S, 126.87143 * S)755 ],756 fill=fill_color,757 stroke=stroke_color,758 stroke_width=1 * S,759 elements = [self._title()]760 )761 762 763 def right_leg(self):764 S = self.size765 766 if self.flag_type == "center":767 fill_color, stroke_color = self.color2, self.color1768 elif self.flag_type == "UK":769 fill_color, stroke_color = self.color3, self.color2770 else:771 fill_color = stroke_color = self.color2772 773 if fill_color.lower() in ["#ffffff", "white"]:774 stroke_color = "#000000"775 776 return svg.Path(777 d=[778 svg.M(94.532861 * S, 112.61697 * S),779 svg.L(121.51828 * S, 142.12337 * S),780 svg.L(101.12085 * S, 158.57267 * S),781 svg.L(101.06795 * S, 150.68512 * S),782 svg.L(111.04127 * S, 142.17389 * S),783 svg.L(94.54537 * S, 124.52411 * S)784 ],785 fill=fill_color,786 stroke=stroke_color,787 stroke_width=1 * S,788 elements = [self._title()]789 )790 791 792 def wheel1(self):793 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0794 S = self.size795 if self.flag_type == "center" or self.flag_type=="UK":796 fill_color = self.color1797 stroke_color = self.color1798 elif self.flag_type == "left_right" or self.flag_type == "up_down":799 fill_color = self.color3800 stroke_color = self.color3801 802 if fill_color.lower() in ["#ffffff", "white"]:803 stroke_color = "#000000"804 805 return svg.Circle(806 cx=148.86574 * S, cy=158.976 * S, r=23.692965 * S,807 fill=fill_color,808 stroke=stroke_color,809 stroke_width=2.61407 * S,810 elements = [self._title()]811 )812 813 814 def wheel2(self):815 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0816 S = self.size817 if (self.flag_type == "center" or self.flag_type == "left_right" or self.flag_type=="UK"):818 fill_color = self.color1819 stroke_color = self.color1820 elif (self.flag_type == "up_down"):821 fill_color = self.color3822 stroke_color = self.color3823 824 if fill_color.lower() in ["#ffffff", "white"]:825 stroke_color = "#000000"826 827 return svg.Circle(cx=67.373695 * S, cy=158.976 * S, r=23.692965 * S,828 fill=fill_color,829 stroke=stroke_color,830 stroke_width=2.61407 * S,831 elements = [self._title()]832 )833 834 835 836 def draw(self):837 S = self.size838 return svg.SVG(839 width=290 * S,840 height=250 * S,841 elements=[842 self.head(),843 self.body(),844 self.left_leg(),845 self.right_leg(),846 self.front_wheel_pie(),847 self.arm(),848 self.wheel2()849 ])850 return (Bike,)851 852 853@app.cell854def _(math, pd, svg):855 # Class that creates a bike svg changing color depending on the riders nationality856 # It also adds extra for being primarily a flat stage rider857 class BikeFast():858 def __init__(self, row, win_type, type_winner_abs, simplified_stage_types_df):859 if row is not None:# and not row.empty:860 self.flag_type = row["flag_type"]861 colors = row["colors"]862 self.color1 = colors[0]863 self.color2 = colors[1]864 self.color3 = colors[2]865 self.row = row866 win_type = win_type867 win_type["Year"] = pd.to_numeric(win_type["Year"])868 self.perc_flat = win_type[win_type["Year"]==row["Year"]]["Flat stage"]869 self.perc_mountain = win_type[win_type["Year"]==row["Year"]]["Mountain stage"]870 self.type_winner_abs = type_winner_abs871 self.simplified_stage_types_df = simplified_stage_types_df872 873 if pd.isna(row["height"]):874 self.size = 1.8875 else:876 self.size = row["height"]877 878 def _title(self):879 return svg.Title(elements=[f'Winner {self.row["Year"]}: {self.row["winner_name"]}, stage wins: {self.row["stage_wins"]}, team: {self.row["winner_team"]}, nationality: {self.row["nationality"]}'])880 881 def pie_chart_slices(self, cx, cy, r, data, colors, rotate_deg=0):882 paths = []883 start_angle = rotate_deg884 for label, value in data.items():885 if value == 0:886 continue887 888 if value == 1.0:889 # Full circle edge case890 paths.append(svg.Circle(891 cx=cx,892 cy=cy,893 r=r,894 fill=colors[label]895 ))896 continue897 898 end_angle = start_angle + 360 * value899 x1 = cx + r * math.cos(math.radians(start_angle))900 y1 = cy + r * math.sin(math.radians(start_angle))901 x2 = cx + r * math.cos(math.radians(end_angle))902 y2 = cy + r * math.sin(math.radians(end_angle))903 large_arc = 1 if (end_angle - start_angle) > 180 else 0904 path_d = f"M{cx},{cy} L{x1},{y1} A{r},{r} 0 {large_arc},1 {x2},{y2} Z"905 paths.append(svg.Path(d=path_d, fill=colors[label]))906 start_angle = end_angle907 return paths908 909 def front_wheel_pie(self):910 S = self.size911 if self.flag_type == "center" or self.flag_type=="UK":912 fill_color = self.color1913 stroke_color = self.color1914 elif self.flag_type == "left_right" or self.flag_type == "up_down":915 fill_color = self.color3916 stroke_color = self.color3917 918 if fill_color.lower() in ["#ffffff", "white"]:919 stroke_color = "#000000"920 921 922 cx=148.86574 * S923 cy=158.976 * S924 r = 23.692965 * S925 926 year = self.row["Year"]927 928 # Filter for the year and sum stage type columns929 winner_row = self.type_winner_abs[self.type_winner_abs["Year"] == year]930 stage_totals = self.simplified_stage_types_df[self.simplified_stage_types_df["Year"] == year]931 932 relevant_cols = ["Time trial", "Mountain stage", "Intermediate stage", "Flat stage"]933 934 if not winner_row.empty and not stage_totals.empty:935 winner_count = winner_row[relevant_cols].sum().sum()936 total_count = stage_totals[relevant_cols].sum().sum()937 938 if total_count > 0:939 data = {940 "Top 10": winner_count / total_count941 #"Other stages": 1 - (winner_count / total_count)942 }943 else:944 data = {"Unknown": 1}945 else:946 data = {"Unknown": 1}947 948 colors = {949 "Top 10": fill_color,950 #"Other stages": "#ffffff",951 "Unknown": "#e0e0e0"952 }953 # Draw pie slices954 # Pie chart slices955 pie_slices = self.pie_chart_slices(cx, cy, r, data, colors)956 957 # Pie + outline rotated group958 wheel_group = svg.G(959 elements=pie_slices + [960 svg.Circle(961 cx=cx,962 cy=cy,963 r=r,964 fill="none",965 stroke=fill_color,966 stroke_width=2.61407 * S967 )968 ]969 )970 971 # Legend972 legend_x = cx + 85 * S973 legend_y = cy - 65 * S974 box_size = 7 * S975 spacing = 10 * S976 977 if "Top 10" in data and data["Top 10"] > 0:978 percent = round(data["Top 10"] * 100)979 tooltip_text = f'{self.row["winner_name"]} won {percent}% of all stages'980 else:981 tooltip_text = "No stage win data available"982 983 return svg.G(elements=[984 wheel_group,985 svg.Title(elements=[tooltip_text])986 ])987 988 def head(self):989 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0990 S = self.size991 if (self.flag_type == "center" or self.flag_type == "up_down" or self.flag_type=="UK"):992 fill_color = self.color1993 stroke_color = self.color1994 elif (self.flag_type == "left_right"):995 fill_color = self.color3996 stroke_color = self.color3997 998 if fill_color.lower() in ["#ffffff", "white"]:999 stroke_color = "#000000"1000 1001 return svg.Circle(1002 cx=152.73752 * S, cy=114.65292 * S, r=8.827405 * S,1003 stroke=stroke_color,1004 fill=fill_color, # Corrected color format1005 stroke_width=2.61407 * S,1006 elements = [self._title()]1007 )1008 1009 def body(self):1010 # center color = 0,0; UK=2,1 ;left_right=1,1 ; up_down=0,01011 S = self.size1012 if self.flag_type == "center" or self.flag_type == "up_down":1013 fill_color = self.color11014 stroke_color = self.color11015 elif self.flag_type == "UK":1016 fill_color = self.color31017 stroke_color = self.color21018 elif self.flag_type == "left_right":1019 fill_color = self.color21020 stroke_color = self.color21021 1022 if fill_color.lower() in ["#ffffff", "white"]:1023 stroke_color = "#000000"1024 1025 return svg.Path(d=[svg.M(95.986494 * S, 110.4811 * S), svg.L(140.43661 * S, 111.44741 * S), 1026 svg.V(124.3315 * S), svg.H(108.87058 * S),],1027 fill=fill_color,1028 stroke=stroke_color,1029 stroke_width=1 * S,1030 elements = [self._title()])1031 1032 def arm(self):1033 # up_down =1,1; left_right=1,1 ;UK=2,1 ; center=1,01034 S = self.size1035 1036 if self.flag_type == "up_down" or self.flag_type == "left_right":1037 fill_color = self.color21038 stroke_color = self.color21039 elif self.flag_type == "UK":1040 fill_color = self.color31041 stroke_color = self.color21042 elif self.flag_type == "center":1043 fill_color = self.color21044 stroke_color = self.color11045 1046 if fill_color.lower() in ["#ffffff", "white"]:1047 stroke_color = "#000000"1048 1049 return svg.Path(d=[svg.M(140.27555 * S, 124.29123 * S), svg.L(130.77354 * S, 133.02826 * S), 1050 svg.L(144.94604 * S, 144.94604 * S), svg.L(139.7924 * S, 148.81127 * S),1051 svg.L(121.43257 * S, 133.67246 * S), svg.L(130.12934 * S, 124.08991 * S),],1052 fill=fill_color,1053 stroke=stroke_color,1054 stroke_width=1 * S,1055 elements = [self._title()]1056 )1057 1058 def left_leg(self):1059 S = self.size1060 if self.flag_type == "up_down" or self.flag_type == "left_right":1061 fill_color = self.color21062 stroke_color = self.color21063 elif self.flag_type == "UK":1064 fill_color = self.color31065 stroke_color = self.color21066 elif self.flag_type == "center":1067 fill_color = self.color11068 stroke_color = self.color11069 1070 if fill_color.lower() in ["#ffffff", "white"]:1071 stroke_color = "#000000"1072 1073 return svg.Path(1074 d=[1075 svg.M(94.532861 * S, 112.61697 * S),1076 svg.L(94.524268 * S, 172.02704 * S),1077 svg.L(101.4425 * S, 172.02704 * S),1078 svg.L(101.4283 * S, 126.87143 * S)1079 ],1080 fill=fill_color,1081 stroke=stroke_color,1082 stroke_width=1 * S,1083 elements=[self._title()],1084 )1085 1086 def right_leg(self):1087 S = self.size1088 1089 if self.flag_type == "center":1090 fill_color, stroke_color = self.color2, self.color11091 elif self.flag_type == "UK":1092 fill_color, stroke_color = self.color3, self.color21093 else:1094 fill_color = stroke_color = self.color21095 1096 if fill_color.lower() in ["#ffffff", "white"]:1097 stroke_color = "#000000"1098 1099 return svg.Path(1100 d=[1101 svg.M(94.532861 * S, 112.61697 * S),1102 svg.L(121.51828 * S, 142.12337 * S),1103 svg.L(101.12085 * S, 158.57267 * S),1104 svg.L(101.06795 * S, 150.68512 * S),1105 svg.L(111.04127 * S, 142.17389 * S),1106 svg.L(94.54537 * S, 124.52411 * S)1107 ],1108 fill=fill_color,1109 stroke=stroke_color,1110 stroke_width=1 * S,1111 elements=[self._title()],1112 )1113 1114 def wheel1(self):1115 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,01116 S = self.size1117 if self.flag_type == "center" or self.flag_type=="UK":1118 fill_color = self.color11119 stroke_color = self.color11120 elif self.flag_type == "left_right" or self.flag_type == "up_down":1121 fill_color = self.color31122 stroke_color = self.color31123 1124 if fill_color.lower() in ["#ffffff", "white"]:1125 stroke_color = "#000000"1126 1127 return svg.Circle(1128 cx=148.86574 * S, cy=158.976 * S, r=23.692965 * S,1129 fill=fill_color,1130 stroke=stroke_color,1131 stroke_width=2.61407 * S,1132 elements = [self._title()]1133 )1134 1135 def wheel2(self):1136 # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,01137 S = self.size1138 if (self.flag_type == "center" or self.flag_type == "left_right" or self.flag_type=="UK"):1139 fill_color = self.color11140 stroke_color = self.color11141 elif (self.flag_type == "up_down"):1142 fill_color = self.color31143 stroke_color = self.color31144 1145 if fill_color.lower() in ["#ffffff", "white"]:1146 stroke_color = "#000000"1147 1148 return svg.Circle(cx=67.373695 * S, cy=158.976 * S, r=23.692965 * S,1149 fill=fill_color,1150 stroke=stroke_color,1151 stroke_width=2.61407 * S,1152 elements = [self._title()])1153 1154 def fast_stripe1(self):1155 S = self.size1156 return svg.Rect(1157 width=41 * S, height=3.6441717 * S, 1158 x=35.530674 * S, y=114.33589 * S, ry=1.776534 * S,1159 fill="black",1160 stroke="black", 1161 stroke_width=1 * S,1162 elements = [self._title()])1163 1164 def fast_stripe2(self):1165 S = self.size1166 return svg.Rect(1167 width=41 * S, height=3.6441717 * S, 1168 x=35.530674 * S, y=123.36678 * S, ry=1.776534 * S,1169 fill="black",1170 stroke="black", 1171 stroke_width=1 * S,1172 elements = [self._title()])1173 1174 def fast_stripe3(self):1175 S = self.size1176 return svg.Rect(1177 width=20 * S, height=3.6441717 * S, 1178 x=35.530674 * S, y=133.12637 * S, ry=1.776534 * S,1179 fill="black",1180 stroke="black", 1181 stroke_width=1 * S,1182 elements = [self._title()])1183 1184 def draw(self):1185 S = self.size1186 return svg.SVG(1187 width=290 * S,1188 height=250 * S,1189 elements=[1190 self.head(),1191 self.body(),1192 self.left_leg(),1193 self.right_leg(),1194 self.front_wheel_pie(),1195 self.arm(),1196 self.wheel2(),1197 self.fast_stripe1(),1198 self.fast_stripe2(),1199 self.fast_stripe3()1200 ])